mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-13 10:15:22 +00:00
feat: harden block inputs (batch 2)
Finished hardening the remaining blocks by guarding content/design maps, normalizing list inputs, coercing numeric/boolean values, and sanitizing links; also tightened scroll target handling in block.html. Changes Normalized portfolio tags/tech stacks/links, made archive defaults safe, and ensured filter JS uses JSON-safe tags in block.html. Validated research area layouts and item fields, added safe media/link handling, and made CTA rendering resilient in block.html. Hardened resume blocks for profile fields, buttons/links, and dates using try with time in block.html, block.html, and block.html. Improved numeric parsing and labeling for language/skill meters in block.html and block.html. Added guarded items and predictable grid classes in block.html. Sanitized badge/title/suggestions/stats rendering in block.html, normalized tech categories and made list style exclusive in block.html, and hardened testimonials’ items/image handling in block.html. Normalized dynamic/static question data, badges, and safe links in block.html.
This commit is contained in:
1 parent
98f0d201c1
commit
09eb7f7b18
21 files changed
+2686
-648
No files matched your search
@@ -5,23 +5,200 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{/* Enable Alpine.js for copy-to-clipboard functionality */}}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
|
||||
{{ $title := $block.content.title | default (i18n "block_contact_title" | default "Contact Us") | emojify | $page.RenderString }}
|
||||
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
|
||||
{{ $default_title := i18n "block_contact_title" | default "Contact Us" }}
|
||||
{{ $title := $default_title }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if eq (printf "%T" $title_raw) "string" }}
|
||||
{{ $title_raw = strings.TrimSpace $title_raw }}
|
||||
{{ if ne $title_raw "" }}
|
||||
{{ $title = $title_raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $title = $title | emojify | $page.RenderString }}
|
||||
|
||||
{{ $subtitle := "" }}
|
||||
{{ $subtitle_raw := index $content "subtitle" }}
|
||||
{{ if eq (printf "%T" $subtitle_raw) "string" }}
|
||||
{{ $subtitle = $subtitle_raw | emojify | $page.RenderString }}
|
||||
{{ end }}
|
||||
|
||||
{{ $visit_title := i18n "block_contact_visit_title" | default "Visit Us" }}
|
||||
{{ $visit_title_raw := index $content "visit_title" }}
|
||||
{{ if eq (printf "%T" $visit_title_raw) "string" }}
|
||||
{{ $visit_title_raw = strings.TrimSpace $visit_title_raw }}
|
||||
{{ if ne $visit_title_raw "" }}
|
||||
{{ $visit_title = $visit_title_raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $connect_title_raw := "" }}
|
||||
{{ $connect_title_value := index $content "connect_title" }}
|
||||
{{ if eq (printf "%T" $connect_title_value) "string" }}
|
||||
{{ $connect_title_raw = strings.TrimSpace $connect_title_value }}
|
||||
{{ end }}
|
||||
{{ $connect_title_standard := $connect_title_raw | default (i18n "block_contact_connect_title" | default "Connect") }}
|
||||
{{ $connect_title_single := $connect_title_raw | default (i18n "block_contact_connect_title" | default "Let's Connect") }}
|
||||
|
||||
{{ $connect_text := "" }}
|
||||
{{ $connect_text_raw := index $content "text" }}
|
||||
{{ if eq (printf "%T" $connect_text_raw) "string" }}
|
||||
{{ $connect_text = strings.TrimSpace $connect_text_raw }}
|
||||
{{ end }}
|
||||
{{ if not $connect_text }}
|
||||
{{ $connect_text = "I'm always open to discussing new projects, opportunities, or just having a chat." }}
|
||||
{{ end }}
|
||||
|
||||
{{ $address_lines := slice }}
|
||||
{{ $address_raw := index $content "address" }}
|
||||
{{ if and $address_raw (reflect.IsMap $address_raw) }}
|
||||
{{ $lines_raw := index $address_raw "lines" }}
|
||||
{{ if reflect.IsSlice $lines_raw }}
|
||||
{{ range $lines_raw }}
|
||||
{{ $line := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $line }}{{ $address_lines = $address_lines | append $line }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $lines_raw) "string" }}
|
||||
{{ $line := strings.TrimSpace $lines_raw }}
|
||||
{{ if $line }}{{ $address_lines = slice $line }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $address_raw) "string" }}
|
||||
{{ $line := strings.TrimSpace $address_raw }}
|
||||
{{ if $line }}{{ $address_lines = slice $line }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $office_hours := slice }}
|
||||
{{ $office_raw := index $content "office_hours" }}
|
||||
{{ if reflect.IsSlice $office_raw }}
|
||||
{{ range $office_raw }}
|
||||
{{ $line := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $line }}{{ $office_hours = $office_hours | append $line }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $office_raw) "string" }}
|
||||
{{ $line := strings.TrimSpace $office_raw }}
|
||||
{{ if $line }}{{ $office_hours = slice $line }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $map_url := "" }}
|
||||
{{ $map_url_raw := index $content "map_url" }}
|
||||
{{ if eq (printf "%T" $map_url_raw) "string" }}
|
||||
{{ $map_url = strings.TrimSpace $map_url_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $map_embed := "" }}
|
||||
{{ $map_embed_raw := index $content "map_embed" }}
|
||||
{{ if eq (printf "%T" $map_embed_raw) "string" }}
|
||||
{{ $map_embed = $map_embed_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $show_form := partial "functions/coerce_bool" (dict "value" (index $content "show_form") "default" false) }}
|
||||
{{ $form_action := "" }}
|
||||
{{ $form_action_raw := index $content "form_action" }}
|
||||
{{ if eq (printf "%T" $form_action_raw) "string" }}
|
||||
{{ $form_action = strings.TrimSpace $form_action_raw }}
|
||||
{{ end }}
|
||||
{{ if not $form_action }}{{ $form_action = "#" }}{{ end }}
|
||||
|
||||
{{/* Check if Visit Us card has content */}}
|
||||
{{ $has_visit_content := or $block.content.address $block.content.office_hours $block.content.map_url }}
|
||||
{{ $has_visit_content := or (gt (len $address_lines) 0) (gt (len $office_hours) 0) $map_url }}
|
||||
|
||||
{{/* Check if Connect card has content - fallback to author profile if no email/social */}}
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $author := "me" }}
|
||||
{{ $author_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $author_raw) "string" }}
|
||||
{{ $author = strings.TrimSpace $author_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $email := $block.content.email | default $profile.email }}
|
||||
{{ $phone := $block.content.phone }}
|
||||
{{ $social := $block.content.social | default $profile.links }}
|
||||
{{ $has_connect_content := or $email $phone $social $block.content.prospective }}
|
||||
{{ $author_data := index site.Data.authors (urlize $author) }}
|
||||
{{ $email := "" }}
|
||||
{{ $email_raw := index $content "email" }}
|
||||
{{ if eq (printf "%T" $email_raw) "string" }}
|
||||
{{ $email = strings.TrimSpace $email_raw }}
|
||||
{{ end }}
|
||||
{{ if and (not $email) $author_data (reflect.IsMap $author_data) }}
|
||||
{{ $author_email_raw := index $author_data "email" }}
|
||||
{{ if eq (printf "%T" $author_email_raw) "string" }}
|
||||
{{ $email = strings.TrimSpace $author_email_raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $phone := "" }}
|
||||
{{ $phone_raw := index $content "phone" }}
|
||||
{{ if eq (printf "%T" $phone_raw) "string" }}
|
||||
{{ $phone = strings.TrimSpace $phone_raw }}
|
||||
{{ end }}
|
||||
{{ if and (not $phone) $author_data (reflect.IsMap $author_data) }}
|
||||
{{ $author_phone_raw := index $author_data "phone" }}
|
||||
{{ if eq (printf "%T" $author_phone_raw) "string" }}
|
||||
{{ $phone = strings.TrimSpace $author_phone_raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $social_raw := index $content "social" }}
|
||||
{{ if eq $social_raw nil }}{{ $social_raw = $profile.links }}{{ end }}
|
||||
{{ $social := slice }}
|
||||
{{ if reflect.IsSlice $social_raw }}
|
||||
{{ range $social_raw }}
|
||||
{{ $url := "" }}
|
||||
{{ $icon := "hero/link" }}
|
||||
{{ $label := "" }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $url_raw := index . "url" | default (index . "link") }}
|
||||
{{ if eq (printf "%T" $url_raw) "string" }}
|
||||
{{ $url = strings.TrimSpace $url_raw }}
|
||||
{{ end }}
|
||||
{{ $icon_raw := index . "icon" }}
|
||||
{{ if eq (printf "%T" $icon_raw) "string" }}
|
||||
{{ $icon = $icon_raw }}
|
||||
{{ end }}
|
||||
{{ $label_raw := index . "label" | default (index . "name") }}
|
||||
{{ if eq (printf "%T" $label_raw) "string" }}
|
||||
{{ $label = $label_raw }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" .) "string" }}
|
||||
{{ $url = strings.TrimSpace . }}
|
||||
{{ end }}
|
||||
{{ if $url }}
|
||||
{{ $social = $social | append (dict "url" $url "icon" $icon "label" $label) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $social_raw) "string" }}
|
||||
{{ $url := strings.TrimSpace $social_raw }}
|
||||
{{ if $url }}
|
||||
{{ $social = slice (dict "url" $url "icon" "hero/link" "label" "") }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $prospective_raw := index $content "prospective" }}
|
||||
{{ $prospective := dict }}
|
||||
{{ if and $prospective_raw (reflect.IsMap $prospective_raw) }}
|
||||
{{ $prospective = $prospective_raw }}
|
||||
{{ end }}
|
||||
{{ $prospective_title := "" }}
|
||||
{{ $prospective_title_raw := index $prospective "title" }}
|
||||
{{ if eq (printf "%T" $prospective_title_raw) "string" }}
|
||||
{{ $prospective_title = strings.TrimSpace $prospective_title_raw }}
|
||||
{{ end }}
|
||||
{{ $prospective_text := "" }}
|
||||
{{ $prospective_text_raw := index $prospective "text" }}
|
||||
{{ if eq (printf "%T" $prospective_text_raw) "string" }}
|
||||
{{ $prospective_text = strings.TrimSpace $prospective_text_raw }}
|
||||
{{ end }}
|
||||
{{ $prospective_button := index $prospective "button" }}
|
||||
{{ if not (reflect.IsMap $prospective_button) }}{{ $prospective_button = dict }}{{ end }}
|
||||
{{ $prospective_button_text := "" }}
|
||||
{{ with index $prospective_button "text" }}{{ $prospective_button_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $prospective_button_url := "" }}
|
||||
{{ with index $prospective_button "url" }}{{ $prospective_button_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $has_connect_content := or $email $phone (gt (len $social) 0) $prospective_title $prospective_text $prospective_button_text $prospective_button_url }}
|
||||
|
||||
{{/* Determine grid layout - single column centered if only one card */}}
|
||||
{{ $grid_class := "grid-cols-1 lg:grid-cols-2" }}
|
||||
@@ -65,32 +242,42 @@
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/map-pin" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
<h3 class="ml-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $block.content.visit_title | default (i18n "block_contact_visit_title" | default "Visit Us") }}
|
||||
{{ $visit_title }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{ with $block.content.address }}
|
||||
{{ if gt (len $address_lines) 0 }}
|
||||
<div class="text-gray-600 dark:text-gray-300">
|
||||
{{ range .lines }}
|
||||
{{ range $address_lines }}
|
||||
<p>{{ . }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with $block.content.office_hours }}
|
||||
{{ if gt (len $office_hours) 0 }}
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-white/10">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">{{ i18n "block_contact_office_hours" | default "Office Hours" }}</h4>
|
||||
<div class="text-gray-600 dark:text-gray-300 space-y-1">
|
||||
{{ range . }}
|
||||
{{ range $office_hours }}
|
||||
<p>{{ . }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with $block.content.map_url }}
|
||||
<a href="{{ . }}" target="_blank" rel="noopener"
|
||||
{{ if $map_url }}
|
||||
{{ $map_link := $map_url }}
|
||||
{{ $map_target := "" }}
|
||||
{{ $map_scheme := (urls.Parse $map_link).Scheme }}
|
||||
{{ if not $map_scheme }}
|
||||
{{ if not (hasPrefix $map_link "#") }}
|
||||
{{ $map_link = $map_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $map_scheme }}
|
||||
{{ $map_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $map_link | safeURL }}" {{ $map_target | safeHTMLAttr }}
|
||||
class="inline-flex items-center mt-4 px-4 py-2 bg-primary-100 dark:bg-primary-500/10 text-primary-700 dark:text-primary-400 rounded-lg border border-primary-200 dark:border-primary-500/20 hover:bg-primary-200 dark:hover:bg-primary-500/20 hover:border-primary-300 dark:hover:border-primary-500/40 transition-all">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/map" "attributes" "class=\"w-5 h-5 mr-2\"") }}
|
||||
{{ i18n "block_contact_view_on_map" | default "View on Map" }}
|
||||
@@ -107,7 +294,8 @@
|
||||
{{/* Filter social links - remove mailto if email is shown separately */}}
|
||||
{{ $filtered_social := slice }}
|
||||
{{ range $social }}
|
||||
{{ if not (hasPrefix .url "mailto:") }}
|
||||
{{ $url := index . "url" }}
|
||||
{{ if and $url (not (and $email (hasPrefix $url "mailto:"))) }}
|
||||
{{ $filtered_social = $filtered_social | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -120,10 +308,10 @@
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ $block.content.connect_title | default (i18n "block_contact_connect_title" | default "Let's Connect") }}
|
||||
{{ $connect_title_single }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{{ $block.content.text | default "I'm always open to discussing new projects, opportunities, or just having a chat." }}
|
||||
{{ $connect_text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +319,7 @@
|
||||
{{/* Email with click-to-copy */}}
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="navigator.clipboard.writeText('{{ . }}'); copied = true; setTimeout(() => copied = false, 2000)"
|
||||
@click="navigator.clipboard.writeText({{ . | jsonify }}); copied = true; setTimeout(() => copied = false, 2000)"
|
||||
class="group flex items-center gap-3 text-left"
|
||||
>
|
||||
<div class="w-12 h-12 flex items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-500/10 border border-primary-200 dark:border-primary-500/20 group-hover:bg-primary-200 dark:group-hover:bg-primary-500/20 group-hover:border-primary-300 dark:group-hover:border-primary-500/40 transition-all">
|
||||
@@ -158,7 +346,7 @@
|
||||
</div>
|
||||
|
||||
{{/* Primary CTA Button */}}
|
||||
<a href="mailto:{{ . }}"
|
||||
<a href="{{ (printf "mailto:%s" .) | safeURL }}"
|
||||
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-500 text-white font-medium rounded-xl shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/30 transition-all">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/paper-airplane" "attributes" "class=\"w-5 h-5 mr-2\"") }}
|
||||
Send a message
|
||||
@@ -166,7 +354,7 @@
|
||||
{{ end }}
|
||||
|
||||
{{ with $phone }}
|
||||
<a href="tel:{{ . }}" class="flex items-center group">
|
||||
<a href="{{ (printf "tel:%s" .) | safeURL }}" class="flex items-center group">
|
||||
<div class="w-12 h-12 flex items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-500/10 border border-primary-200 dark:border-primary-500/20 group-hover:bg-primary-200 dark:group-hover:bg-primary-500/20 group-hover:border-primary-300 dark:group-hover:border-primary-500/40 transition-all mr-3">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
@@ -179,16 +367,29 @@
|
||||
</div>
|
||||
|
||||
{{/* Right: Social Links */}}
|
||||
{{ with $filtered_social }}
|
||||
{{ if gt (len $filtered_social) 0 }}
|
||||
<div class="flex flex-col justify-center items-center md:items-start">
|
||||
<h4 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-5">{{ i18n "block_contact_follow_me" | default "Find me on" }}</h4>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{{ range . }}
|
||||
<a href="{{ .url }}" target="_blank" rel="noopener"
|
||||
{{ range $filtered_social }}
|
||||
{{ $social_url := index . "url" }}
|
||||
{{ $social_label := index . "label" }}
|
||||
{{ $social_icon := index . "icon" }}
|
||||
{{ $link := $social_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}
|
||||
class="group flex items-center justify-center w-14 h-14 rounded-xl bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 hover:bg-primary-100 dark:hover:bg-primary-500/20 hover:border-primary-300 dark:hover:border-primary-500/40 hover:scale-110 transition-all duration-300"
|
||||
{{ with .label }}title="{{ . }}"{{ end }}>
|
||||
{{ if $social_label }}title="{{ $social_label }}"{{ end }}>
|
||||
<div class="text-gray-600 dark:text-gray-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-7 h-7\"") }}
|
||||
{{ partial "functions/get_icon" (dict "name" $social_icon "attributes" "class=\"w-7 h-7\"") }}
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
@@ -204,7 +405,7 @@
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/chat-bubble-left-right" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
<h3 class="ml-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $block.content.connect_title | default (i18n "block_contact_connect_title" | default "Connect") }}
|
||||
{{ $connect_title_standard }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -212,7 +413,7 @@
|
||||
{{/* Contact Methods */}}
|
||||
<div class="space-y-3">
|
||||
{{ with $email }}
|
||||
<a href="mailto:{{ . }}" class="flex items-center group p-3 -mx-3 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 transition-colors">
|
||||
<a href="{{ (printf "mailto:%s" .) | safeURL }}" class="flex items-center group p-3 -mx-3 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 transition-colors">
|
||||
<div class="w-10 h-10 flex items-center justify-center rounded-full bg-primary-100 dark:bg-primary-500/10 border border-primary-200 dark:border-primary-500/20 mr-4">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
@@ -224,7 +425,7 @@
|
||||
{{ end }}
|
||||
|
||||
{{ with $phone }}
|
||||
<a href="tel:{{ . }}" class="flex items-center group p-3 -mx-3 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 transition-colors">
|
||||
<a href="{{ (printf "tel:%s" .) | safeURL }}" class="flex items-center group p-3 -mx-3 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 transition-colors">
|
||||
<div class="w-10 h-10 flex items-center justify-center rounded-full bg-primary-100 dark:bg-primary-500/10 border border-primary-200 dark:border-primary-500/20 mr-4">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
@@ -236,15 +437,28 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Social Media */}}
|
||||
{{ with $filtered_social }}
|
||||
{{ if gt (len $filtered_social) 0 }}
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-white/10">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">{{ i18n "block_contact_follow_me" | default "Find me on" }}</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{{ range . }}
|
||||
<a href="{{ .url }}" target="_blank" rel="noopener"
|
||||
{{ range $filtered_social }}
|
||||
{{ $social_url := index . "url" }}
|
||||
{{ $social_label := index . "label" }}
|
||||
{{ $social_icon := index . "icon" }}
|
||||
{{ $link := $social_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}
|
||||
class="w-11 h-11 flex items-center justify-center rounded-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-600 dark:text-gray-400 hover:bg-primary-100 dark:hover:bg-primary-500/20 hover:border-primary-300 dark:hover:border-primary-500/40 hover:text-primary-600 dark:hover:text-primary-400 transition-all duration-300"
|
||||
{{ with .label }}title="{{ . }}"{{ end }}>
|
||||
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
{{ if $social_label }}title="{{ $social_label }}"{{ end }}>
|
||||
{{ partial "functions/get_icon" (dict "name" $social_icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -255,15 +469,28 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Prospective Members - shown in both layouts */}}
|
||||
{{ with $block.content.prospective }}
|
||||
{{ if or $prospective_title $prospective_text $prospective_button_text $prospective_button_url }}
|
||||
{{ $prospective_heading := $prospective_title | default (i18n "block_contact_prospective_title" | default "Prospective Members") }}
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-white/10">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">{{ .title | default (i18n "block_contact_prospective_title" | default "Prospective Members") }}</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ .text }}</p>
|
||||
{{ with .button }}
|
||||
<a href="{{ .url }}"
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">{{ $prospective_heading }}</h4>
|
||||
{{ with $prospective_text }}
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ . }}</p>
|
||||
{{ end }}
|
||||
{{ if and $prospective_button_text $prospective_button_url }}
|
||||
{{ $cta_link := $prospective_button_url }}
|
||||
{{ $cta_target := "" }}
|
||||
{{ $cta_scheme := (urls.Parse $cta_link).Scheme }}
|
||||
{{ if not $cta_scheme }}
|
||||
{{ if not (hasPrefix $cta_link "#") }}
|
||||
{{ $cta_link = $cta_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $cta_scheme }}
|
||||
{{ $cta_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $cta_link | safeURL }}" {{ $cta_target | safeHTMLAttr }}
|
||||
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-500 text-white rounded-lg shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/30 transition-all">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/user-plus" "attributes" "class=\"w-5 h-5 mr-2\"") }}
|
||||
{{ .text }}
|
||||
{{ $prospective_button_text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -273,7 +500,7 @@
|
||||
</div>
|
||||
|
||||
{{/* Optional Map Embed */}}
|
||||
{{ with $block.content.map_embed }}
|
||||
{{ with $map_embed }}
|
||||
<div class="mt-12 rounded-2xl overflow-hidden border border-gray-200 dark:border-white/[0.08]">
|
||||
<div class="aspect-w-16 aspect-h-9 lg:aspect-h-6">
|
||||
{{ . | safeHTML }}
|
||||
@@ -282,11 +509,11 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Contact Form (optional) */}}
|
||||
{{ if $block.content.show_form }}
|
||||
{{ if $show_form }}
|
||||
<div class="mt-12 max-w-2xl mx-auto">
|
||||
<div class="bg-white dark:bg-white/[0.03] backdrop-blur-xl rounded-2xl border border-gray-200 dark:border-white/[0.08] p-8">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">{{ i18n "block_contact_form_title" | default "Send us a Message" }}</h3>
|
||||
<form action="{{ $block.content.form_action | default "#" }}" method="POST" class="space-y-6">
|
||||
<form action="{{ $form_action | safeURL }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">{{ i18n "block_contact_form_name" | default "Name" }}</label>
|
||||
|
||||
@@ -5,22 +5,70 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
|
||||
<div class="flex items-center flex-col mx-auto w-full justify-center mt-4 px-8 pb-5">
|
||||
|
||||
{{ range $index, $item := $block.content.buttons }}
|
||||
<a href="{{ .url }}" target="_blank" rel="noopener noreferrer" class="flex items-center p-1 w-full rounded-md hover:scale-105 transition-all bg-gray-100 mb-3 max-w-3xl">
|
||||
<div class="flex text-center w-full">
|
||||
<div class="w-10 h-10 text-gray-700">
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "height=\"40\" width=\"40\" class=\"rounded-sm\"") }}
|
||||
{{ $buttons_raw := index $content "buttons" }}
|
||||
{{ $buttons := slice }}
|
||||
{{ if reflect.IsSlice $buttons_raw }}
|
||||
{{ $buttons = $buttons_raw }}
|
||||
{{ else if and $buttons_raw (reflect.IsMap $buttons_raw) }}
|
||||
{{ $buttons = slice $buttons_raw }}
|
||||
{{ else if eq (printf "%T" $buttons_raw) "string" }}
|
||||
{{ $buttons = slice (dict "url" $buttons_raw) }}
|
||||
{{ end }}
|
||||
|
||||
{{ range $index, $item := $buttons }}
|
||||
{{ $text := "" }}
|
||||
{{ $url := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $new_tab := false }}
|
||||
{{ if reflect.IsMap $item }}
|
||||
{{ $text_raw := index $item "text" | default (index $item "label") | default (index $item "name") }}
|
||||
{{ if ne $text_raw nil }}{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}{{ end }}
|
||||
{{ $url_raw := index $item "url" | default (index $item "link") }}
|
||||
{{ if eq (printf "%T" $url_raw) "string" }}{{ $url = strings.TrimSpace $url_raw }}{{ end }}
|
||||
{{ $icon_raw := index $item "icon" }}
|
||||
{{ if eq (printf "%T" $icon_raw) "string" }}{{ $icon = $icon_raw }}{{ end }}
|
||||
{{ $new_tab = partial "functions/coerce_bool" (dict "value" (index $item "new_tab") "default" false) }}
|
||||
{{ else if eq (printf "%T" $item) "string" }}
|
||||
{{ $url = strings.TrimSpace $item }}
|
||||
{{ end }}
|
||||
{{ if and (not $text) $url }}
|
||||
{{ $text = $url }}
|
||||
{{ end }}
|
||||
{{ if $url }}
|
||||
{{ $link := $url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex justify-center items-center font-semibold w-full text-gray-700 -ml-10">
|
||||
{{ .text | markdownify | emojify }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
{{ if eq (path.Ext $link) ".pdf" }}
|
||||
{{ $new_tab = true }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $new_tab = true }}
|
||||
{{ end }}
|
||||
{{ if $new_tab }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener noreferrer\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} class="flex items-center p-1 w-full rounded-md hover:scale-105 transition-all bg-gray-100 mb-3 max-w-3xl">
|
||||
<div class="flex text-center w-full">
|
||||
<div class="w-10 h-10 text-gray-700">
|
||||
{{ with $icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "height=\"40\" width=\"40\" class=\"rounded-sm\"") }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex justify-center items-center font-semibold w-full text-gray-700 -ml-10">
|
||||
{{ $text | markdownify | emojify }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
@@ -5,14 +5,34 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
|
||||
<div>
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
{{ $title := .title | emojify | $page.RenderString }}
|
||||
{{ $text := .text | emojify | $page.RenderString }}
|
||||
{{ $image_path := path.Join "media" .image }}
|
||||
{{ $image := resources.GetMatch $image_path }}
|
||||
{{ with $image }}
|
||||
{{ $items_raw := index $content "items" }}
|
||||
{{ $items := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items = slice $items_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ range $idx, $item := $items }}
|
||||
{{ if reflect.IsMap $item }}
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $item "title" }}
|
||||
{{ if eq (printf "%T" $title_raw) "string" }}{{ $title = $title_raw | emojify | $page.RenderString }}{{ end }}
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $item "text" }}
|
||||
{{ if eq (printf "%T" $text_raw) "string" }}{{ $text = $text_raw | emojify | $page.RenderString }}{{ end }}
|
||||
{{ $image_name := "" }}
|
||||
{{ $image_raw := index $item "image" }}
|
||||
{{ if eq (printf "%T" $image_raw) "string" }}{{ $image_name = strings.TrimSpace $image_raw }}{{ end }}
|
||||
{{ $image_path := "" }}
|
||||
{{ if $image_name }}{{ $image_path = path.Join "media" $image_name }}{{ end }}
|
||||
{{ $image := "" }}
|
||||
{{ if $image_path }}{{ $image = resources.GetMatch $image_path }}{{ end }}
|
||||
{{ if $image }}
|
||||
<div class="gap-8 items-center py-8 px-4 mx-auto max-w-screen-xl xl:gap-16 md:grid md:grid-cols-2 sm:py-16 lg:px-6">
|
||||
{{ if ne $image.MediaType.SubType "gif" }}
|
||||
{{ $responsive := partial "functions/process_responsive_image.html" (dict
|
||||
@@ -35,29 +55,57 @@
|
||||
alt="{{$title | plainify}}" {{ if (modBool $idx 2)}}style="order: 1"{{end}}>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ errorf "%s uses the `cta-image-paragraph` blox which specifies a non-existent `image` at `assets/%s`. Please add the image to this folder and try again." $page.File.Path $image_path }}
|
||||
{{ warnf "%s uses the `cta-image-paragraph` blox which specifies a non-existent `image` at `assets/%s`. Skipping this item." $page.File.Path $image_path }}
|
||||
{{ end }}
|
||||
<div class="mt-4 md:mt-0">
|
||||
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white">{{$title}}</h2>
|
||||
<p class="mb-6 font-light text-gray-500 md:text-lg dark:text-gray-400">{{$text}}</p>
|
||||
{{ $icon := .feature_icon | default "check" }}
|
||||
{{ if .features }}
|
||||
{{ $icon := "check" }}
|
||||
{{ $icon_raw := index $item "feature_icon" }}
|
||||
{{ if eq (printf "%T" $icon_raw) "string" }}{{ $icon = $icon_raw }}{{ end }}
|
||||
{{ $features_raw := index $item "features" }}
|
||||
{{ $features := slice }}
|
||||
{{ if reflect.IsSlice $features_raw }}
|
||||
{{ $features = $features_raw }}
|
||||
{{ else if eq (printf "%T" $features_raw) "string" }}
|
||||
{{ $features = slice $features_raw }}
|
||||
{{ end }}
|
||||
{{ if gt (len $features) 0 }}
|
||||
<ul>
|
||||
{{ range .features }}
|
||||
{{ range $features }}
|
||||
<li class="relative mb-4 pl-6">
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" `class="inline-block pr-3" style="height: 1em;"`) }}
|
||||
{{ . | markdownify }}
|
||||
{{ printf "%v" . | markdownify }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ with .button }}
|
||||
<a href="{{.url}}" class="mt-3 inline-flex items-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900">
|
||||
{{.text}}
|
||||
{{ $button_raw := index $item "button" }}
|
||||
{{ if and $button_raw (reflect.IsMap $button_raw) }}
|
||||
{{ $button_text := "" }}
|
||||
{{ $button_url := "" }}
|
||||
{{ with index $button_raw "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button_raw "url" }}{{ $button_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if and $button_text $button_url }}
|
||||
{{ $link := $button_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ 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="mt-3 inline-flex items-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900">
|
||||
{{ $button_text }}
|
||||
<svg class="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -5,28 +5,146 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{/* Enable animations if typewriter or scroll animations are configured */}}
|
||||
{{ $typewriter := $block.content.typewriter | default dict }}
|
||||
{{ $has_animations := or $typewriter.enable $block.design.animations }}
|
||||
{{ $typewriter := index $content "typewriter" }}
|
||||
{{ if not (reflect.IsMap $typewriter) }}{{ $typewriter = dict }}{{ end }}
|
||||
{{ $typewriter_enabled := partial "functions/coerce_bool" (dict "value" (index $typewriter "enable") "default" false) }}
|
||||
{{ $animations_enabled := partial "functions/coerce_bool" (dict "value" (index $design "animations") "default" false) }}
|
||||
{{ $has_animations := or $typewriter_enabled $animations_enabled }}
|
||||
{{ if $has_animations }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Get author data from YAML */}}
|
||||
{{ $username := $block.content.username | default "me" }}
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $author := partial "functions/get_author_profile" $username }}
|
||||
|
||||
{{/* Get avatar */}}
|
||||
{{ $avatar := "" }}
|
||||
{{ $avatar_shape := $block.design.avatar_shape | default "circle" }}
|
||||
{{ $avatar_shape := "circle" }}
|
||||
{{ $avatar_shape_raw := index $design "avatar_shape" }}
|
||||
{{ if eq (printf "%T" $avatar_shape_raw) "string" }}
|
||||
{{ $avatar_shape = lower (strings.TrimSpace $avatar_shape_raw) }}
|
||||
{{ end }}
|
||||
{{ with $author.avatar }}
|
||||
{{ $avatar = .Fill "300x300 webp Center" }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Layout style */}}
|
||||
{{ $style := $block.design.style | default "centered" }}
|
||||
{{ $style := "centered" }}
|
||||
{{ $style_raw := index $design "style" }}
|
||||
{{ if eq (printf "%T" $style_raw) "string" }}
|
||||
{{ $style_candidate := lower (strings.TrimSpace $style_raw) }}
|
||||
{{ if in (slice "centered" "split") $style_candidate }}
|
||||
{{ $style = $style_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $show_status := partial "functions/coerce_bool" (dict "value" (index $content "show_status") "default" false) }}
|
||||
{{ $show_scroll := partial "functions/coerce_bool" (dict "value" (index $content "show_scroll_indicator") "default" false) }}
|
||||
|
||||
{{ $scroll_target := "#projects" }}
|
||||
{{ $scroll_target_raw := index $content "scroll_target" }}
|
||||
{{ if eq (printf "%T" $scroll_target_raw) "string" }}
|
||||
{{ $scroll_target = strings.TrimSpace $scroll_target_raw }}
|
||||
{{ end }}
|
||||
{{ if not $scroll_target }}{{ $scroll_target = "#projects" }}{{ end }}
|
||||
|
||||
{{ $greeting := "" }}
|
||||
{{ $greeting_raw := index $content "greeting" }}
|
||||
{{ if eq (printf "%T" $greeting_raw) "string" }}{{ $greeting = $greeting_raw }}{{ end }}
|
||||
{{ $name_prefix := "" }}
|
||||
{{ $name_prefix_raw := index $content "name_prefix" }}
|
||||
{{ if eq (printf "%T" $name_prefix_raw) "string" }}{{ $name_prefix = $name_prefix_raw }}{{ end }}
|
||||
|
||||
{{ $title_override := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if eq (printf "%T" $title_raw) "string" }}{{ $title_override = strings.TrimSpace $title_raw }}{{ end }}
|
||||
|
||||
{{ $tagline := "" }}
|
||||
{{ $tagline_raw := index $content "tagline" }}
|
||||
{{ if eq (printf "%T" $tagline_raw) "string" }}{{ $tagline = $tagline_raw }}{{ end }}
|
||||
|
||||
{{ $bio_override := "" }}
|
||||
{{ $bio_raw := index $content "bio" }}
|
||||
{{ if eq (printf "%T" $bio_raw) "string" }}{{ $bio_override = $bio_raw }}{{ end }}
|
||||
|
||||
{{ $role := $author.role | default $tagline }}
|
||||
{{ $bio := $author.bio | default $bio_override }}
|
||||
|
||||
{{ $typewriter_strings := slice }}
|
||||
{{ $strings_raw := index $typewriter "strings" }}
|
||||
{{ if reflect.IsSlice $strings_raw }}
|
||||
{{ range $strings_raw }}
|
||||
{{ $val := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $val }}{{ $typewriter_strings = $typewriter_strings | append $val }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $strings_raw) "string" }}
|
||||
{{ $val := strings.TrimSpace $strings_raw }}
|
||||
{{ if $val }}{{ $typewriter_strings = slice $val }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $typewriter_prefix := "" }}
|
||||
{{ $typewriter_prefix_raw := index $typewriter "prefix" }}
|
||||
{{ if eq (printf "%T" $typewriter_prefix_raw) "string" }}{{ $typewriter_prefix = $typewriter_prefix_raw }}{{ end }}
|
||||
{{ $type_speed := partial "functions/coerce_int" (dict "value" (index $typewriter "type_speed") "default" 80 "min" 10 "max" 500) }}
|
||||
{{ $delete_speed := partial "functions/coerce_int" (dict "value" (index $typewriter "delete_speed") "default" 50 "min" 10 "max" 500) }}
|
||||
{{ $pause_time := partial "functions/coerce_int" (dict "value" (index $typewriter "pause_time") "default" 2000 "min" 200 "max" 10000) }}
|
||||
|
||||
{{ $links_raw := $author.links | default (index $content "links") }}
|
||||
{{ $links := slice }}
|
||||
{{ if reflect.IsSlice $links_raw }}
|
||||
{{ range $links_raw }}
|
||||
{{ $url := "" }}
|
||||
{{ $icon := "hero/link" }}
|
||||
{{ $label := "" }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $url_raw := index . "url" | default (index . "link") }}
|
||||
{{ if eq (printf "%T" $url_raw) "string" }}{{ $url = strings.TrimSpace $url_raw }}{{ end }}
|
||||
{{ $icon_raw := index . "icon" }}
|
||||
{{ if eq (printf "%T" $icon_raw) "string" }}{{ $icon = $icon_raw }}{{ end }}
|
||||
{{ $label_raw := index . "label" | default (index . "name") }}
|
||||
{{ if eq (printf "%T" $label_raw) "string" }}{{ $label = $label_raw }}{{ end }}
|
||||
{{ else if eq (printf "%T" .) "string" }}
|
||||
{{ $url = strings.TrimSpace . }}
|
||||
{{ end }}
|
||||
{{ if $url }}
|
||||
{{ $links = $links | append (dict "url" $url "icon" $icon "label" $label) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $links_raw) "string" }}
|
||||
{{ $url := strings.TrimSpace $links_raw }}
|
||||
{{ if $url }}
|
||||
{{ $links = slice (dict "url" $url "icon" "hero/link" "label" "") }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $cta_buttons_raw := index $content "cta_buttons" }}
|
||||
{{ $cta_buttons := slice }}
|
||||
{{ if reflect.IsSlice $cta_buttons_raw }}
|
||||
{{ $cta_buttons = $cta_buttons_raw }}
|
||||
{{ else if and $cta_buttons_raw (reflect.IsMap $cta_buttons_raw) }}
|
||||
{{ $cta_buttons = slice $cta_buttons_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $status_icon := "" }}
|
||||
{{ $status := $author.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 }}
|
||||
|
||||
<div class="relative min-h-[80vh] flex items-center justify-center overflow-hidden">
|
||||
|
||||
@@ -61,8 +179,8 @@
|
||||
<img src="{{ .RelPermalink }}" alt="{{ $author.title }}" class="w-full h-full object-cover">
|
||||
</div>
|
||||
{{/* Status emoji badge */}}
|
||||
{{ if $block.content.show_status }}
|
||||
{{ with $author.status.icon }}
|
||||
{{ if $show_status }}
|
||||
{{ with $status_icon }}
|
||||
<span class="absolute bottom-2 right-2 w-12 h-12 flex items-center justify-center text-2xl bg-white dark:bg-gray-900 rounded-full border-2 border-gray-200 dark:border-white/10 shadow-lg">
|
||||
{{ . | emojify }}
|
||||
</span>
|
||||
@@ -77,7 +195,7 @@
|
||||
{{/* Content */}}
|
||||
<div class="text-center md:text-left">
|
||||
{{/* Greeting */}}
|
||||
{{ with $block.content.greeting }}
|
||||
{{ with $greeting }}
|
||||
<p class="text-primary-600 dark:text-primary-400 font-mono text-sm mb-4 flex items-center {{ if eq $style "centered" }}justify-center{{ end }} gap-2">
|
||||
<span class="inline-block w-8 h-px bg-primary-500"></span>
|
||||
{{ . }}
|
||||
@@ -86,19 +204,18 @@
|
||||
|
||||
{{/* Name */}}
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl font-bold mb-4">
|
||||
{{ with $block.content.name_prefix }}<span class="text-gray-500 dark:text-gray-400 font-normal">{{ . }}</span> {{ end }}
|
||||
{{ with $name_prefix }}<span class="text-gray-500 dark:text-gray-400 font-normal">{{ . }}</span> {{ end }}
|
||||
<span class="bg-gradient-to-r from-gray-900 via-gray-800 to-gray-700 dark:from-white dark:via-gray-100 dark:to-gray-300 bg-clip-text text-transparent">
|
||||
{{ $author.title | default $block.content.title | default (i18n "developer" | default "Developer") }}
|
||||
{{ $author.title | default $title_override | default (i18n "developer" | default "Developer") }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{{/* Role/Tagline with optional typewriter */}}
|
||||
{{ $role := $author.role | default $block.content.tagline }}
|
||||
{{ if and $typewriter.enable $typewriter.strings }}
|
||||
{{ if and $typewriter_enabled (gt (len $typewriter_strings) 0) }}
|
||||
<p class="text-xl sm:text-2xl text-gray-600 dark:text-gray-400 font-light mb-6 h-8">
|
||||
{{ with $typewriter.prefix }}<span>{{ . }}</span> {{ end }}
|
||||
{{ with $typewriter_prefix }}<span>{{ . }}</span> {{ end }}
|
||||
<span
|
||||
x-data="typewriter({{ $typewriter.strings | jsonify }}, { typeSpeed: {{ $typewriter.type_speed | default 80 }}, deleteSpeed: {{ $typewriter.delete_speed | default 50 }}, pauseTime: {{ $typewriter.pause_time | default 2000 }} })"
|
||||
x-data="typewriter({{ $typewriter_strings | jsonify }}, { typeSpeed: {{ $type_speed }}, deleteSpeed: {{ $delete_speed }}, pauseTime: {{ $pause_time }} })"
|
||||
x-text="currentText"
|
||||
class="text-primary-600 dark:text-primary-400"
|
||||
></span><span class="typewriter-cursor"></span>
|
||||
@@ -110,26 +227,34 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Bio */}}
|
||||
{{ with $author.bio | default $block.content.bio }}
|
||||
{{ with $bio }}
|
||||
<p class="text-gray-600 dark:text-gray-400 max-w-xl {{ if eq $style "centered" }}mx-auto{{ end }} mb-8 leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* Social Links */}}
|
||||
{{ $links := $author.links | default $block.content.links }}
|
||||
{{ with $links }}
|
||||
{{ if gt (len $links) 0 }}
|
||||
<div class="flex items-center gap-4 mb-8 {{ if eq $style "centered" }}justify-center{{ end }}">
|
||||
{{ range . }}
|
||||
{{ $icon := .icon | default "link" }}
|
||||
{{ $url := .url }}
|
||||
{{ range $links }}
|
||||
{{ $icon := index . "icon" | default "hero/link" }}
|
||||
{{ $url := index . "url" }}
|
||||
{{ $link := $url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener noreferrer\"" }}
|
||||
{{ end }}
|
||||
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
target="{{ if hasPrefix $url "mailto:" }}_self{{ else }}_blank{{ end }}"
|
||||
rel="noopener noreferrer"
|
||||
href="{{ $link | safeURL }}"
|
||||
{{ $target | safeHTMLAttr }}
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-white hover:bg-primary-50 dark:hover:bg-primary-500/20 hover:border-primary-300 dark:hover:border-primary-500/50 transition-all duration-300"
|
||||
{{ with .label }}title="{{ . }}"{{ end }}
|
||||
{{ $label := index . "label" }}{{ if $label }}title="{{ $label }}"{{ end }}
|
||||
>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
</a>
|
||||
@@ -138,19 +263,45 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* CTA Buttons */}}
|
||||
{{ with $block.content.cta_buttons }}
|
||||
{{ if gt (len $cta_buttons) 0 }}
|
||||
<div class="flex flex-wrap gap-4 {{ if eq $style "centered" }}justify-center{{ end }}">
|
||||
{{ range $idx, $btn := . }}
|
||||
{{ range $idx, $btn := $cta_buttons }}
|
||||
{{ if reflect.IsMap $btn }}
|
||||
{{ $is_primary := eq $idx 0 }}
|
||||
{{ $btn_text := "" }}
|
||||
{{ $btn_url := "" }}
|
||||
{{ $btn_icon := "" }}
|
||||
{{ $btn_new_tab := partial "functions/coerce_bool" (dict "value" (index $btn "new_tab") "default" false) }}
|
||||
{{ with index $btn "text" }}{{ $btn_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $btn "url" }}{{ $btn_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $btn "icon" }}{{ $btn_icon = printf "%v" . }}{{ end }}
|
||||
{{ if and $btn_text $btn_url }}
|
||||
{{ $link := $btn_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $link) ".pdf" }}{{ $btn_new_tab = true }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $btn_new_tab = true }}
|
||||
{{ end }}
|
||||
{{ if $btn_new_tab }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
href="{{ $link | safeURL }}"
|
||||
{{ $target | safeHTMLAttr }}
|
||||
class="{{ if $is_primary }}bg-primary-600 hover:bg-primary-500 text-white shadow-lg shadow-primary-500/25{{ else }}bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 text-gray-800 dark:text-white border border-gray-200 dark:border-white/20 hover:border-gray-300 dark:hover:border-white/30{{ end }} px-6 py-3 rounded-full font-semibold transition-all duration-300 inline-flex items-center gap-2"
|
||||
>
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-5 h-5\"") }}
|
||||
{{ if $btn_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $btn_icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
{{ end }}
|
||||
{{ .text }}
|
||||
{{ $btn_text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -168,8 +319,8 @@
|
||||
<img src="{{ .RelPermalink }}" alt="{{ $author.title }}" class="w-full h-full object-cover">
|
||||
</div>
|
||||
{{/* Status emoji badge */}}
|
||||
{{ if $block.content.show_status }}
|
||||
{{ with $author.status.icon }}
|
||||
{{ if $show_status }}
|
||||
{{ with $status_icon }}
|
||||
<span class="absolute bottom-0 right-0 w-10 h-10 flex items-center justify-center text-xl bg-white dark:bg-gray-900 rounded-full border-2 border-gray-200 dark:border-white/10 shadow-lg">
|
||||
{{ . | emojify }}
|
||||
</span>
|
||||
@@ -180,7 +331,7 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Greeting */}}
|
||||
{{ with $block.content.greeting }}
|
||||
{{ with $greeting }}
|
||||
<p class="text-primary-600 dark:text-primary-400 font-mono text-sm mb-4 inline-flex items-center gap-2">
|
||||
<span class="inline-block w-8 h-px bg-primary-500"></span>
|
||||
{{ . }}
|
||||
@@ -190,19 +341,18 @@
|
||||
|
||||
{{/* Name */}}
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-6">
|
||||
{{ with $block.content.name_prefix }}<span class="text-gray-500 font-normal">{{ . }}</span> {{ end }}
|
||||
{{ with $name_prefix }}<span class="text-gray-500 font-normal">{{ . }}</span> {{ end }}
|
||||
<span class="bg-gradient-to-r from-gray-900 via-gray-800 to-gray-700 dark:from-white dark:via-gray-100 dark:to-gray-300 bg-clip-text text-transparent">
|
||||
{{ $author.title | default $block.content.title | default (i18n "developer" | default "Developer") }}
|
||||
{{ $author.title | default $title_override | default (i18n "developer" | default "Developer") }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{{/* Role/Tagline with optional typewriter */}}
|
||||
{{ $role := $author.role | default $block.content.tagline }}
|
||||
{{ if and $typewriter.enable $typewriter.strings }}
|
||||
{{ if and $typewriter_enabled (gt (len $typewriter_strings) 0) }}
|
||||
<p class="text-xl sm:text-2xl md:text-3xl text-gray-600 dark:text-gray-400 font-light mb-6 min-h-[2em]">
|
||||
{{ with $typewriter.prefix }}<span>{{ . }}</span> {{ end }}
|
||||
{{ with $typewriter_prefix }}<span>{{ . }}</span> {{ end }}
|
||||
<span
|
||||
x-data="typewriter({{ $typewriter.strings | jsonify }}, { typeSpeed: {{ $typewriter.type_speed | default 80 }}, deleteSpeed: {{ $typewriter.delete_speed | default 50 }}, pauseTime: {{ $typewriter.pause_time | default 2000 }} })"
|
||||
x-data="typewriter({{ $typewriter_strings | jsonify }}, { typeSpeed: {{ $type_speed }}, deleteSpeed: {{ $delete_speed }}, pauseTime: {{ $pause_time }} })"
|
||||
x-text="currentText"
|
||||
class="text-primary-600 dark:text-primary-400"
|
||||
></span><span class="typewriter-cursor"></span>
|
||||
@@ -214,26 +364,34 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Bio */}}
|
||||
{{ with $author.bio | default $block.content.bio }}
|
||||
{{ with $bio }}
|
||||
<p class="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-8 text-lg leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* Social Links */}}
|
||||
{{ $links := $author.links | default $block.content.links }}
|
||||
{{ with $links }}
|
||||
{{ if gt (len $links) 0 }}
|
||||
<div class="flex items-center justify-center gap-4 mb-10">
|
||||
{{ range . }}
|
||||
{{ $icon := .icon | default "link" }}
|
||||
{{ $url := .url }}
|
||||
{{ range $links }}
|
||||
{{ $icon := index . "icon" | default "hero/link" }}
|
||||
{{ $url := index . "url" }}
|
||||
{{ $link := $url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener noreferrer\"" }}
|
||||
{{ end }}
|
||||
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
target="{{ if hasPrefix $url "mailto:" }}_self{{ else }}_blank{{ end }}"
|
||||
rel="noopener noreferrer"
|
||||
href="{{ $link | safeURL }}"
|
||||
{{ $target | safeHTMLAttr }}
|
||||
class="w-12 h-12 flex items-center justify-center rounded-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-white hover:bg-primary-50 dark:hover:bg-primary-500/20 hover:border-primary-300 dark:hover:border-primary-500/50 transition-all duration-300"
|
||||
{{ with .label }}title="{{ . }}"{{ end }}
|
||||
{{ $label := index . "label" }}{{ if $label }}title="{{ $label }}"{{ end }}
|
||||
>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
</a>
|
||||
@@ -242,19 +400,45 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* CTA Buttons */}}
|
||||
{{ with $block.content.cta_buttons }}
|
||||
{{ if gt (len $cta_buttons) 0 }}
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{{ range $idx, $btn := . }}
|
||||
{{ range $idx, $btn := $cta_buttons }}
|
||||
{{ if reflect.IsMap $btn }}
|
||||
{{ $is_primary := eq $idx 0 }}
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
class="{{ if $is_primary }}bg-primary-600 hover:bg-primary-500 text-white shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/30{{ else }}bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 text-gray-800 dark:text-white border border-gray-200 dark:border-white/20 hover:border-gray-300 dark:hover:border-white/30{{ end }} px-8 py-4 rounded-full font-semibold transition-all duration-300 inline-flex items-center gap-2 text-lg"
|
||||
>
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-5 h-5\"") }}
|
||||
{{ $btn_text := "" }}
|
||||
{{ $btn_url := "" }}
|
||||
{{ $btn_icon := "" }}
|
||||
{{ $btn_new_tab := partial "functions/coerce_bool" (dict "value" (index $btn "new_tab") "default" false) }}
|
||||
{{ with index $btn "text" }}{{ $btn_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $btn "url" }}{{ $btn_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $btn "icon" }}{{ $btn_icon = printf "%v" . }}{{ end }}
|
||||
{{ if and $btn_text $btn_url }}
|
||||
{{ $link := $btn_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $link) ".pdf" }}{{ $btn_new_tab = true }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $btn_new_tab = true }}
|
||||
{{ end }}
|
||||
{{ .text }}
|
||||
</a>
|
||||
{{ if $btn_new_tab }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a
|
||||
href="{{ $link | safeURL }}"
|
||||
{{ $target | safeHTMLAttr }}
|
||||
class="{{ if $is_primary }}bg-primary-600 hover:bg-primary-500 text-white shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/30{{ else }}bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 text-gray-800 dark:text-white border border-gray-200 dark:border-white/20 hover:border-gray-300 dark:hover:border-white/30{{ end }} px-8 py-4 rounded-full font-semibold transition-all duration-300 inline-flex items-center gap-2 text-lg"
|
||||
>
|
||||
{{ if $btn_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $btn_icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
{{ end }}
|
||||
{{ $btn_text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -262,10 +446,20 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Scroll indicator - clicks to scroll to next section */}}
|
||||
{{ if $block.content.show_scroll_indicator }}
|
||||
{{ $scroll_target := $block.content.scroll_target | default "#projects" }}
|
||||
{{ if $show_scroll }}
|
||||
{{ $scroll_link := $scroll_target }}
|
||||
{{ $scroll_target_attr := "" }}
|
||||
{{ $scroll_scheme := (urls.Parse $scroll_link).Scheme }}
|
||||
{{ if not $scroll_scheme }}
|
||||
{{ if not (hasPrefix $scroll_link "#") }}
|
||||
{{ $scroll_link = $scroll_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scroll_scheme }}
|
||||
{{ $scroll_target_attr = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a
|
||||
href="{{ $scroll_target }}"
|
||||
href="{{ $scroll_link | safeURL }}"
|
||||
{{ $scroll_target_attr | safeHTMLAttr }}
|
||||
class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce cursor-pointer group"
|
||||
aria-label="{{ i18n "scroll_to_content" | default "Scroll to content" }}"
|
||||
>
|
||||
|
||||
@@ -1,27 +1,64 @@
|
||||
{{- $wcPage := .wcPage -}}
|
||||
{{- $wcBlock := .wcBlock -}}
|
||||
{{- $wcIdentifier := .wcIdentifier -}}
|
||||
{{- $content := $wcBlock.content -}}
|
||||
{{- if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end -}}
|
||||
{{- $design := $wcBlock.design -}}
|
||||
{{- if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end -}}
|
||||
|
||||
{{/* Enable Alpine.js for interactive accordion */}}
|
||||
{{ $wcPage.Page.Store.Set "has_alpine" true }}
|
||||
|
||||
{{- $st := $wcBlock.design.spacing | default dict -}}
|
||||
{{- $pad := $st.padding | default (slice "20px" "0" "20px" "0") -}}
|
||||
{{- $st := index $design "spacing" -}}
|
||||
{{- if not (reflect.IsMap $st) }}{{ $st = dict }}{{ end -}}
|
||||
{{- $pad := slice "20px" "0" "20px" "0" -}}
|
||||
{{- $pad_raw := index $st "padding" -}}
|
||||
{{- if and (reflect.IsSlice $pad_raw) (ge (len $pad_raw) 4) }}{{ $pad = $pad_raw }}{{ end -}}
|
||||
{{- $pad_sm := slice "3rem" "0" -}}
|
||||
{{- $mar := $st.margin | default (slice "0" "0" "0" "0") -}}
|
||||
{{- $pad_sm_raw := index $st "padding_sm" -}}
|
||||
{{- if and (reflect.IsSlice $pad_sm_raw) (ge (len $pad_sm_raw) 2) }}{{ $pad_sm = $pad_sm_raw }}{{ end -}}
|
||||
{{- $mar := slice "0" "0" "0" "0" -}}
|
||||
{{- $mar_raw := index $st "margin" -}}
|
||||
{{- if and (reflect.IsSlice $mar_raw) (ge (len $mar_raw) 4) }}{{ $mar = $mar_raw }}{{ end -}}
|
||||
|
||||
{{- $bg := $wcBlock.design.background | default dict -}}
|
||||
{{- $bg_color_val := $bg.color | default "" -}}
|
||||
{{- $bg := index $design "background" -}}
|
||||
{{- if not (reflect.IsMap $bg) }}{{ $bg = dict }}{{ end -}}
|
||||
{{- $bg_color_val := "" -}}
|
||||
{{- $bg_color_raw := index $bg "color" -}}
|
||||
{{- if eq (printf "%T" $bg_color_raw) "string" }}{{ $bg_color_val = $bg_color_raw }}{{ end -}}
|
||||
|
||||
{{- $title := "" -}}
|
||||
{{- $title_raw := index $content "title" -}}
|
||||
{{- if eq (printf "%T" $title_raw) "string" }}{{ $title = $title_raw }}{{ end -}}
|
||||
{{- $subtitle := "" -}}
|
||||
{{- $subtitle_raw := index $content "subtitle" -}}
|
||||
{{- if eq (printf "%T" $subtitle_raw) "string" }}{{ $subtitle = $subtitle_raw }}{{ end -}}
|
||||
{{- $text := "" -}}
|
||||
{{- $text_raw := index $content "text" -}}
|
||||
{{- if eq (printf "%T" $text_raw) "string" }}{{ $text = $text_raw }}{{ end -}}
|
||||
|
||||
{{- $items_raw := index $content "items" -}}
|
||||
{{- $items := slice -}}
|
||||
{{- if reflect.IsSlice $items_raw }}{{ $items = $items_raw }}{{ end -}}
|
||||
|
||||
{{- $button := index $content "button" -}}
|
||||
{{- if not (reflect.IsMap $button) }}{{ $button = dict }}{{ end -}}
|
||||
{{- $button_text := "" -}}
|
||||
{{- with index $button "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end -}}
|
||||
{{- $button_url := "" -}}
|
||||
{{- with index $button "url" }}{{ $button_url = strings.TrimSpace (printf "%v" .) }}{{ end -}}
|
||||
{{- $button_icon := "" -}}
|
||||
{{- with index $button "icon" }}{{ $button_icon = printf "%v" . }}{{ end -}}
|
||||
|
||||
<section id="{{ $wcIdentifier }}" class="hb-section {{ $bg_color_val }}" style="padding: {{ delimit $pad " " }}; padding-top: clamp({{ index $pad_sm 0 }}, {{ index $pad 0 }}, {{ index $pad 0 }}); padding-bottom: clamp({{ index $pad_sm 1 }}, {{ index $pad 2 }}, {{ index $pad 2 }}); margin: {{ delimit $mar " " }};">
|
||||
<div class="mx-auto max-w-screen-xl px-6 lg:px-8">
|
||||
|
||||
{{- with $wcBlock.content.title -}}
|
||||
{{- with $title -}}
|
||||
<div class="mb-8 text-center">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
|
||||
{{ . | markdownify | emojify }}
|
||||
</h2>
|
||||
{{- with $wcBlock.content.subtitle -}}
|
||||
{{- with $subtitle -}}
|
||||
<p class="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
{{ . | markdownify | emojify }}
|
||||
</p>
|
||||
@@ -29,18 +66,24 @@
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
{{- with $wcBlock.content.text -}}
|
||||
{{- with $text -}}
|
||||
<div class="prose prose-slate dark:prose-invert max-w-3xl mx-auto mb-8">
|
||||
{{ . | markdownify | emojify }}
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
{{- $items := $wcBlock.content.items | default slice -}}
|
||||
|
||||
{{- if $items -}}
|
||||
{{- if gt (len $items) 0 -}}
|
||||
<div class="max-w-3xl mx-auto" x-data="{ openItem: null }">
|
||||
<div class="space-y-4">
|
||||
{{- range $index, $item := $items -}}
|
||||
{{- if reflect.IsMap $item -}}
|
||||
{{- $question := "" -}}
|
||||
{{- $question_raw := index $item "question" | default (index $item "title") -}}
|
||||
{{- if ne $question_raw nil }}{{ $question = printf "%v" $question_raw }}{{ end -}}
|
||||
{{- $answer := "" -}}
|
||||
{{- $answer_raw := index $item "answer" | default (index $item "text") -}}
|
||||
{{- if ne $answer_raw nil }}{{ $answer = printf "%v" $answer_raw }}{{ end -}}
|
||||
{{- if or $question $answer -}}
|
||||
<div class="faq-item border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
@@ -49,7 +92,7 @@
|
||||
:aria-expanded="openItem === {{ $index }}"
|
||||
>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100 pr-8">
|
||||
{{ $item.question | markdownify | emojify }}
|
||||
{{ $question | markdownify | emojify }}
|
||||
</span>
|
||||
<svg
|
||||
class="w-5 h-5 text-gray-500 transition-transform flex-shrink-0"
|
||||
@@ -72,21 +115,34 @@
|
||||
class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div class="prose prose-slate dark:prose-invert max-w-none">
|
||||
{{ $item.answer | markdownify | emojify }}
|
||||
{{ $answer | markdownify | emojify }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
{{- with $wcBlock.content.button -}}
|
||||
{{- if and $button_text $button_url -}}
|
||||
{{- $link := $button_url -}}
|
||||
{{- $target := "" -}}
|
||||
{{- $scheme := (urls.Parse $link).Scheme -}}
|
||||
{{- 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 -}}
|
||||
<div class="mt-10 text-center">
|
||||
<a href="{{ .url }}" class="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600">
|
||||
{{ .text }}
|
||||
{{- with .icon -}}
|
||||
<span class="ml-2">{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"inline-block h-5 w-5\"") }}</span>
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} class="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600">
|
||||
{{ $button_text }}
|
||||
{{- if $button_icon -}}
|
||||
<span class="ml-2">{{ partial "functions/get_icon" (dict "name" $button_icon "attributes" "class=\"inline-block h-5 w-5\"") }}</span>
|
||||
{{- end -}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -94,4 +150,3 @@
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,8 +5,15 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $title := $block.content.title | emojify | $page.RenderString }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $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 }}{{ end }}
|
||||
{{ $items_raw := index $content "items" }}
|
||||
{{ $items := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}{{ $items = $items_raw }}{{ end }}
|
||||
|
||||
<section>
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
||||
@@ -19,16 +26,26 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="space-y-8 md:grid md:grid-cols-2 lg:grid-cols-3 md:gap-12 md:space-y-0">
|
||||
{{ range $block.content.items }}
|
||||
{{ range $items }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $icon := "" }}
|
||||
{{ with index . "icon" }}
|
||||
{{ if eq (printf "%T" .) "string" }}{{ $icon = . }}{{ end }}
|
||||
{{ end }}
|
||||
{{ $name := "" }}
|
||||
{{ with index . "name" }}{{ $name = printf "%v" . }}{{ end }}
|
||||
{{ $description := "" }}
|
||||
{{ with index . "description" }}{{ $description = printf "%v" . }}{{ end }}
|
||||
<div>
|
||||
{{ with .icon }}
|
||||
{{ with $icon }}
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded-full bg-primary-100 lg:h-12 lg:w-12 dark:bg-primary-900 text-primary-600 dark:text-primary-300">
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" `class="w-5 h-5 lg:w-6 lg:h-6"`) }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">{{ .name | $page.RenderString }}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ .description | $page.RenderString }}</p>
|
||||
{{ with $name }}<h3 class="mb-2 text-xl font-bold dark:text-white">{{ . | $page.RenderString }}</h3>{{ end }}
|
||||
{{ with $description }}<p class="text-gray-500 dark:text-gray-400">{{ . | $page.RenderString }}</p>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,30 +5,56 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
{{ $no_padding := partial "functions/coerce_bool" (dict "value" (index $design "no_padding") "default" false) }}
|
||||
|
||||
{{ $announcement := index $content "announcement" }}
|
||||
{{ if not (reflect.IsMap $announcement) }}{{ $announcement = dict }}{{ end }}
|
||||
{{ $announcement_text := "" }}
|
||||
{{ with index $announcement "text" }}{{ $announcement_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $announcement_link := index $announcement "link" }}
|
||||
{{ if not (reflect.IsMap $announcement_link) }}{{ $announcement_link = dict }}{{ end }}
|
||||
{{ $announcement_link_text := "" }}
|
||||
{{ with index $announcement_link "text" }}{{ $announcement_link_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $announcement_link_url := "" }}
|
||||
{{ with index $announcement_link "url" }}{{ $announcement_link_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $primary_action := index $content "primary_action" }}
|
||||
{{ if not (reflect.IsMap $primary_action) }}{{ $primary_action = dict }}{{ end }}
|
||||
{{ $primary_text := "" }}
|
||||
{{ with index $primary_action "text" }}{{ $primary_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $primary_url := "" }}
|
||||
{{ with index $primary_action "url" }}{{ $primary_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $primary_icon := "" }}
|
||||
{{ with index $primary_action "icon" }}{{ $primary_icon = printf "%v" . }}{{ end }}
|
||||
|
||||
{{ $secondary_action := index $content "secondary_action" }}
|
||||
{{ if not (reflect.IsMap $secondary_action) }}{{ $secondary_action = dict }}{{ end }}
|
||||
{{ $secondary_text := "" }}
|
||||
{{ with index $secondary_action "text" }}{{ $secondary_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $secondary_url := "" }}
|
||||
{{ with index $secondary_action "url" }}{{ $secondary_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
<div class="relative isolate px-6 pt-14 lg:px-8">
|
||||
|
||||
<div class="mx-auto max-w-2xl {{if not ($block.design.no_padding | default false) }}py-32 sm:py-48 lg:py-56{{end}}">
|
||||
<div class="mx-auto max-w-2xl {{if not $no_padding }}py-32 sm:py-48 lg:py-56{{end}}">
|
||||
|
||||
{{ if $block.content.announcement.text }}
|
||||
{{ $announcement := $block.content.announcement }}
|
||||
{{ if $announcement_text }}
|
||||
<div class="hidden sm:mb-8 sm:flex sm:justify-center">
|
||||
<div class="relative rounded-full px-3 py-1 text-sm leading-6 text-gray-600 dark:text-gray-300 ring-1 ring-gray-900/10 dark:ring-gray-300 hover:ring-gray-900/20 dark:hover:ring-gray-400">
|
||||
{{$announcement.text | markdownify}}
|
||||
{{if $announcement.link.text}}
|
||||
{{ $announcement_link := $announcement.link.url }}
|
||||
{{ $announcement_scheme := (urls.Parse $announcement_link).Scheme }}
|
||||
{{ if not $announcement_scheme }}
|
||||
{{/* Check if it's a hash fragment (page anchor) */}}
|
||||
{{ if hasPrefix $announcement_link "#" }}
|
||||
{{/* Keep hash fragments as-is */}}
|
||||
{{ $announcement_link = $announcement_link }}
|
||||
{{ else }}
|
||||
{{/* Apply relLangURL for relative paths */}}
|
||||
{{ $announcement_link = $announcement_link | relLangURL }}
|
||||
{{ $announcement_text | markdownify }}
|
||||
{{ if and $announcement_link_text $announcement_link_url }}
|
||||
{{ $link := $announcement_link_url }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<a href="{{$announcement_link | safeURL}}" class="font-semibold text-primary-600 dark:text-primary-300">
|
||||
<span class="absolute inset-0" aria-hidden="true"></span>{{$announcement.link.text}} <span aria-hidden="true">→</span>
|
||||
<a href="{{ $link | safeURL }}" class="font-semibold text-primary-600 dark:text-primary-300">
|
||||
<span class="absolute inset-0" aria-hidden="true"></span>{{ $announcement_link_text }} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -36,11 +62,11 @@
|
||||
{{end}}
|
||||
|
||||
<div class="text-center">
|
||||
{{ with $block.content.title }}<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-6xl">{{ . | markdownify }}</h1>{{end}}
|
||||
{{ with $block.content.text }}<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">{{ . | $page.RenderString | emojify }}</p>{{end}}
|
||||
{{ with index $content "title" }}<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-6xl">{{ printf "%v" . | markdownify }}</h1>{{end}}
|
||||
{{ with index $content "text" }}<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">{{ printf "%v" . | $page.RenderString | emojify }}</p>{{end}}
|
||||
|
||||
{{ if $block.content.primary_action.url }}
|
||||
{{ $link := $block.content.primary_action.url }}
|
||||
{{ if and $primary_url $primary_text }}
|
||||
{{ $link := $primary_url }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ $target := "" }}
|
||||
{{ if not $scheme }}
|
||||
@@ -57,15 +83,15 @@
|
||||
{{ end }}
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} class="rounded-md bg-primary-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600">
|
||||
{{ $block.content.primary_action.text | markdownify | emojify }}
|
||||
{{ with $block.content.primary_action.icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "style=\"height: 1em\" class='inline-block pl-2'") }}
|
||||
{{ $primary_text | markdownify | emojify }}
|
||||
{{ if $primary_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $primary_icon "attributes" "style=\"height: 1em\" class='inline-block pl-2'") }}
|
||||
{{ end }}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{ if $block.content.secondary_action.url }}
|
||||
{{ $link_alt := $block.content.secondary_action.url }}
|
||||
{{ if and $secondary_url $secondary_text }}
|
||||
{{ $link_alt := $secondary_url }}
|
||||
{{ $scheme_alt := (urls.Parse $link_alt).Scheme }}
|
||||
{{ $target_alt := "" }}
|
||||
{{ if not $scheme_alt }}
|
||||
@@ -80,7 +106,7 @@
|
||||
{{ else if in (slice "http" "https") $scheme_alt }}
|
||||
{{ $target_alt = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link_alt | safeURL }}" {{ $target_alt | safeHTMLAttr }} class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 hover:dark:text-gray-200 hover:text-gray-800">{{ $block.content.secondary_action.text | markdownify | emojify }} <span aria-hidden="true">→</span></a>
|
||||
<a href="{{ $link_alt | safeURL }}" {{ $target_alt | safeHTMLAttr }} class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 hover:dark:text-gray-200 hover:text-gray-800">{{ $secondary_text | markdownify | emojify }} <span aria-hidden="true">→</span></a>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
{{- $wcPage := .wcPage -}}
|
||||
{{- $wcBlock := .wcBlock -}}
|
||||
{{- $wcIdentifier := .wcIdentifier -}}
|
||||
{{- $content := $wcBlock.content -}}
|
||||
{{- if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end -}}
|
||||
{{- $design := $wcBlock.design -}}
|
||||
{{- if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end -}}
|
||||
|
||||
{{- $st := $wcBlock.design.spacing | default dict -}}
|
||||
{{- $pad := $st.padding | default (slice "4rem" "0" "4rem" "0") -}}
|
||||
{{- $st := index $design "spacing" -}}
|
||||
{{- if not (reflect.IsMap $st) }}{{ $st = dict }}{{ end -}}
|
||||
{{- $pad := slice "4rem" "0" "4rem" "0" -}}
|
||||
{{- $pad_raw := index $st "padding" -}}
|
||||
{{- if and (reflect.IsSlice $pad_raw) (ge (len $pad_raw) 4) }}{{ $pad = $pad_raw }}{{ end -}}
|
||||
|
||||
{{- $title := "" -}}
|
||||
{{- $title_raw := index $content "title" -}}
|
||||
{{- if eq (printf "%T" $title_raw) "string" }}{{ $title = $title_raw }}{{ end -}}
|
||||
{{- $subtitle := "" -}}
|
||||
{{- $subtitle_raw := index $content "subtitle" -}}
|
||||
{{- if eq (printf "%T" $subtitle_raw) "string" }}{{ $subtitle = $subtitle_raw }}{{ end -}}
|
||||
|
||||
{{- $categories_raw := index $content "categories" -}}
|
||||
{{- $categories := slice -}}
|
||||
{{- if reflect.IsSlice $categories_raw }}{{ $categories = $categories_raw }}{{ end -}}
|
||||
|
||||
{{- $view_all := index $content "view_all" -}}
|
||||
{{- if not (reflect.IsMap $view_all) }}{{ $view_all = dict }}{{ end -}}
|
||||
{{- $view_all_text := "" -}}
|
||||
{{- with index $view_all "text" }}{{ $view_all_text = strings.TrimSpace (printf "%v" .) }}{{ end -}}
|
||||
{{- $view_all_link := "" -}}
|
||||
{{- with index $view_all "link" }}{{ $view_all_link = strings.TrimSpace (printf "%v" .) }}{{ end -}}
|
||||
|
||||
<section id="{{ $wcIdentifier }}" class="relative" style="padding: {{ delimit $pad " " }};">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
{{/* Section header */}}
|
||||
{{ if or $wcBlock.content.title $wcBlock.content.subtitle }}
|
||||
{{ if or $title $subtitle }}
|
||||
<div class="mx-auto max-w-2xl text-center mb-12">
|
||||
{{ with $wcBlock.content.title }}
|
||||
{{ with $title }}
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
|
||||
{{ . | markdownify }}
|
||||
</h2>
|
||||
{{ end }}
|
||||
{{ with $wcBlock.content.subtitle }}
|
||||
{{ with $subtitle }}
|
||||
<p class="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
{{ . | markdownify }}
|
||||
</p>
|
||||
@@ -27,35 +52,63 @@
|
||||
{{/* Categories grid - with dynamic counts from Hugo taxonomy */}}
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
|
||||
{{ range $wcBlock.content.categories }}
|
||||
{{ $category_name := .category | default .title }}
|
||||
{{ range $categories }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $category_name := "" }}
|
||||
{{ $category_raw := index . "category" | default (index . "title") }}
|
||||
{{ if ne $category_raw nil }}{{ $category_name = strings.TrimSpace (printf "%v" $category_raw) }}{{ end }}
|
||||
|
||||
{{/* Get actual count from Hugo taxonomy - proper Hugo syntax */}}
|
||||
{{ $category_url := "" }}
|
||||
{{ $actual_count := 0 }}
|
||||
{{ with site.Taxonomies.categories.Get $category_name }}
|
||||
{{ $actual_count = .Count }}
|
||||
{{ $category_url = .Page.RelPermalink }}
|
||||
{{ if $category_name }}
|
||||
{{ with site.Taxonomies.categories.Get $category_name }}
|
||||
{{ $actual_count = .Count }}
|
||||
{{ $category_url = .Page.RelPermalink }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $manual_count := partial "functions/coerce_int" (dict "value" (index . "count") "default" 0 "min" 0) }}
|
||||
{{ $item_link := "" }}
|
||||
{{ $link_raw := index . "link" }}
|
||||
{{ if eq (printf "%T" $link_raw) "string" }}{{ $item_link = strings.TrimSpace $link_raw }}{{ end }}
|
||||
{{ if not $item_link }}{{ $item_link = $category_url }}{{ end }}
|
||||
|
||||
{{/* Only show category if it has content OR if count is explicitly set */}}
|
||||
{{ if or (gt $actual_count 0) (.count) }}
|
||||
<a href="{{ $category_url }}" class="group relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-sm ring-1 ring-gray-900/5 transition-all duration-300 hover:shadow-xl hover:ring-primary-500/50 hover:-translate-y-1">
|
||||
{{ if or (gt $actual_count 0) (gt $manual_count 0) }}
|
||||
{{ if $item_link }}
|
||||
{{ $link := $item_link }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" class="group relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-sm ring-1 ring-gray-900/5 transition-all duration-300 hover:shadow-xl hover:ring-primary-500/50 hover:-translate-y-1">
|
||||
{{ else }}
|
||||
<div class="group relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-sm ring-1 ring-gray-900/5 transition-all duration-300 hover:shadow-xl hover:ring-primary-500/50 hover:-translate-y-1">
|
||||
{{ end }}
|
||||
|
||||
{{/* Icon */}}
|
||||
{{ with .icon }}
|
||||
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl {{ .bg_color | default "bg-primary-100 dark:bg-primary-900/50" }} {{ .text_color | default "text-primary-600 dark:text-primary-400" }} group-hover:scale-110 transition-transform">
|
||||
{{ partial "functions/get_icon" (dict "name" .name "attributes" "class=\"h-6 w-6\"") }}
|
||||
{{ $icon := index . "icon" }}
|
||||
{{ if and $icon (reflect.IsMap $icon) }}
|
||||
{{ $icon_name := index $icon "name" }}
|
||||
{{ $icon_bg := index $icon "bg_color" | default "bg-primary-100 dark:bg-primary-900/50" }}
|
||||
{{ $icon_text := index $icon "text_color" | default "text-primary-600 dark:text-primary-400" }}
|
||||
{{ if eq (printf "%T" $icon_name) "string" }}
|
||||
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl {{ $icon_bg }} {{ $icon_text }} group-hover:scale-110 transition-transform">
|
||||
{{ partial "functions/get_icon" (dict "name" $icon_name "attributes" "class=\"h-6 w-6\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Title */}}
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{{ .title }}
|
||||
{{ index . "title" | default $category_name }}
|
||||
</h3>
|
||||
|
||||
{{/* Description */}}
|
||||
{{ with .description }}
|
||||
{{ with index . "description" }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{ . }}
|
||||
</p>
|
||||
@@ -63,7 +116,8 @@
|
||||
|
||||
{{/* Stats - use actual count from taxonomy, fallback to manual count */}}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
{{ $display_count := default .count $actual_count }}
|
||||
{{ $display_count := $actual_count }}
|
||||
{{ if gt $manual_count 0 }}{{ $display_count = $manual_count }}{{ end }}
|
||||
{{ if $display_count }}
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
{{ $display_count }} {{ cond (eq $display_count 1) (T "article" | default "article") (T "articles" | default "articles") }}
|
||||
@@ -76,16 +130,24 @@
|
||||
|
||||
{{/* Hover gradient effect */}}
|
||||
<div class="absolute inset-0 -z-10 bg-gradient-to-br from-primary-500/0 via-primary-500/0 to-primary-500/0 opacity-0 transition-opacity duration-300 group-hover:opacity-10"></div>
|
||||
</a>
|
||||
{{ if $item_link }}</a>{{ else }}</div>{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* View all link */}}
|
||||
{{ with $wcBlock.content.view_all }}
|
||||
{{ if and $view_all_text $view_all_link }}
|
||||
{{ $link := $view_all_link }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="mt-10 text-center">
|
||||
<a href="{{ .link }}" class="inline-flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
|
||||
{{ .text }}
|
||||
<a href="{{ $link | safeURL }}" class="inline-flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
|
||||
{{ $view_all_text }}
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
|
||||
@@ -5,15 +5,48 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $display_mode := $block.design.display_mode | default "grid" }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $title := $block.content.title | emojify | $page.RenderString }}
|
||||
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $display_mode := "grid" }}
|
||||
{{ $display_raw := index $design "display_mode" }}
|
||||
{{ if eq (printf "%T" $display_raw) "string" }}
|
||||
{{ $display_mode = lower (strings.TrimSpace $display_raw) }}
|
||||
{{ end }}
|
||||
{{ if not (in (slice "grid" "carousel" "marquee") $display_mode) }}
|
||||
{{ $display_mode = "grid" }}
|
||||
{{ end }}
|
||||
|
||||
{{ $show_pattern := partial "functions/coerce_bool" (dict "value" (index $design "show_pattern") "default" false) }}
|
||||
|
||||
{{ $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 }}
|
||||
|
||||
{{ $logos_raw := index $content "logos" }}
|
||||
{{ $logos := slice }}
|
||||
{{ if reflect.IsSlice $logos_raw }}{{ $logos = $logos_raw }}{{ end }}
|
||||
{{ $logo_folder := "" }}
|
||||
{{ $logo_folder_raw := index $content "logo_folder" }}
|
||||
{{ if eq (printf "%T" $logo_folder_raw) "string" }}{{ $logo_folder = strings.TrimSpace $logo_folder_raw }}{{ end }}
|
||||
|
||||
{{ $cta := index $content "cta" }}
|
||||
{{ if not (reflect.IsMap $cta) }}{{ $cta = dict }}{{ end }}
|
||||
{{ $cta_text := "" }}
|
||||
{{ with index $cta "text" }}{{ $cta_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $cta_url := "" }}
|
||||
{{ with index $cta "url" }}{{ $cta_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $cta_icon := "" }}
|
||||
{{ with index $cta "icon" }}{{ $cta_icon = printf "%v" . }}{{ end }}
|
||||
|
||||
<div class="relative py-16 sm:py-20 lg:py-24 overflow-hidden">
|
||||
{{/* Background Pattern */}}
|
||||
{{ if $block.design.show_pattern }}
|
||||
{{ if $show_pattern }}
|
||||
<div class="absolute inset-0 opacity-5">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-100 to-transparent dark:from-primary-900 dark:to-transparent"></div>
|
||||
</div>
|
||||
@@ -47,25 +80,47 @@
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-8 lg:gap-12 items-center">
|
||||
|
||||
{{/* Logo Items from Data */}}
|
||||
{{ if $block.content.logos }}
|
||||
{{ range $block.content.logos }}
|
||||
{{ if gt (len $logos) 0 }}
|
||||
{{ range $logos }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $name := "" }}
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $description := "" }}
|
||||
{{ with index . "description" }}{{ $description = printf "%v" . }}{{ end }}
|
||||
{{ $image_name := "" }}
|
||||
{{ with index . "image" }}{{ $image_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $link := "" }}
|
||||
{{ with index . "url" }}{{ $link = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $external := partial "functions/coerce_bool" (dict "value" (index . "external") "default" false) }}
|
||||
{{ if $link }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $external = true }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="group relative">
|
||||
{{ $link := .url | default "#" }}
|
||||
{{ $target := cond .external "_blank" "_self" }}
|
||||
<a href="{{ $link }}" target="{{ $target }}" rel="{{ if .external }}noopener{{ end }}"
|
||||
{{ if $link }}
|
||||
<a href="{{ $link | safeURL }}" {{ if $external }}target="_blank" rel="noopener"{{ end }}
|
||||
class="block p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-300"
|
||||
{{ with .name }}title="{{ . }}"{{ end }}>
|
||||
{{ if $name }}title="{{ $name }}"{{ end }}>
|
||||
{{ else }}
|
||||
<div class="block p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-300">
|
||||
{{ end }}
|
||||
<div class="relative">
|
||||
{{/* Logo Image */}}
|
||||
{{ if .image }}
|
||||
{{ $img := resources.Get (printf "media/%s" .image) }}
|
||||
{{ if $image_name }}
|
||||
{{ $img := resources.Get (printf "media/%s" $image_name) }}
|
||||
{{ if $img }}
|
||||
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
|
||||
{{ if not $isSVG }}
|
||||
{{ $img = $img.Fit "280x140 webp" }}
|
||||
{{ end }}
|
||||
<img src="{{ $img.RelPermalink }}"
|
||||
alt="{{ .name | default "Partner logo" }}"
|
||||
alt="{{ $name | default "Partner logo" }}"
|
||||
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
|
||||
class="h-12 sm:h-14 lg:h-16 w-auto mx-auto object-contain filter grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-300 group-hover:scale-110"
|
||||
loading="lazy">
|
||||
@@ -73,20 +128,25 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Tooltip on Hover */}}
|
||||
{{ with .description }}
|
||||
{{ with $description }}
|
||||
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none whitespace-nowrap z-10">
|
||||
{{ . }}
|
||||
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-l-transparent border-r-4 border-r-transparent border-t-4 border-t-gray-900 dark:border-t-gray-700"></div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if $link }}
|
||||
</a>
|
||||
{{ else }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Logo Images from Folder (Legacy Support) */}}
|
||||
{{ else if $block.content.logo_folder }}
|
||||
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $block.content.logo_folder) }}
|
||||
{{ else if $logo_folder }}
|
||||
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $logo_folder) }}
|
||||
{{ range $images }}
|
||||
<div class="group relative">
|
||||
<div class="block p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-300">
|
||||
@@ -112,10 +172,17 @@
|
||||
<div class="flex animate-scroll space-x-12 lg:space-x-16">
|
||||
{{/* Duplicate items for continuous scroll */}}
|
||||
{{ $items := slice }}
|
||||
{{ if $block.content.logos }}
|
||||
{{ $items = $block.content.logos }}
|
||||
{{ else if $block.content.logo_folder }}
|
||||
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $block.content.logo_folder) }}
|
||||
{{ if gt (len $logos) 0 }}
|
||||
{{ range $logos }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $img_name := index . "image" }}
|
||||
{{ if eq (printf "%T" $img_name) "string" }}
|
||||
{{ $items = $items | append (dict "image" (strings.TrimSpace $img_name) "name" (index . "name")) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ else if $logo_folder }}
|
||||
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $logo_folder) }}
|
||||
{{ range $images }}
|
||||
{{ $items = $items | append (dict "image" .Name) }}
|
||||
{{ end }}
|
||||
@@ -168,18 +235,23 @@
|
||||
<div class="relative">
|
||||
<div class="overflow-hidden">
|
||||
<div class="flex animate-marquee whitespace-nowrap">
|
||||
{{ if $block.content.logos }}
|
||||
{{ range $block.content.logos }}
|
||||
{{ if gt (len $logos) 0 }}
|
||||
{{ range $logos }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $image_name := "" }}
|
||||
{{ with index . "image" }}{{ $image_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $name := "" }}
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
<div class="mx-8 inline-flex items-center">
|
||||
{{ if .image }}
|
||||
{{ $img := resources.Get (printf "media/%s" .image) }}
|
||||
{{ if $image_name }}
|
||||
{{ $img := resources.Get (printf "media/%s" $image_name) }}
|
||||
{{ if $img }}
|
||||
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
|
||||
{{ if not $isSVG }}
|
||||
{{ $img = $img.Fit "280x140 webp" }}
|
||||
{{ end }}
|
||||
<img src="{{ $img.RelPermalink }}"
|
||||
alt="{{ .name | default "Partner logo" }}"
|
||||
alt="{{ $name | default "Partner logo" }}"
|
||||
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
|
||||
class="h-14 lg:h-16 w-auto object-contain opacity-70"
|
||||
loading="lazy">
|
||||
@@ -187,6 +259,7 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,13 +267,24 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Call to Action */}}
|
||||
{{ with $block.content.cta }}
|
||||
{{ if and $cta_text $cta_url }}
|
||||
{{ $link := $cta_url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ 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 }}
|
||||
<div class="mt-12 text-center">
|
||||
<a href="{{ .url }}"
|
||||
<a href="{{ $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 hover:shadow-lg transition-all duration-300">
|
||||
{{ .text }}
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
|
||||
{{ $cta_text }}
|
||||
{{ if $cta_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $cta_icon "attributes" "class=\"ml-2 w-5 h-5\"") }}
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,37 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{/* Enable Alpine.js and animations for this block */}}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
{{ if ne $block.design.animations false }}
|
||||
{{ $animations_enabled := true }}
|
||||
{{ if isset $design "animations" }}
|
||||
{{ $animations_enabled = partial "functions/coerce_bool" (dict "value" (index $design "animations") "default" true) }}
|
||||
{{ end }}
|
||||
{{ if $animations_enabled }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Query projects */}}
|
||||
{{ $query := site.Pages }}
|
||||
{{ $folders := $block.content.filters.folders | default (slice "projects") }}
|
||||
{{ $filters := index $content "filters" }}
|
||||
{{ if not (reflect.IsMap $filters) }}{{ $filters = dict }}{{ 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 eq (len $folders) 0 }}{{ $folders = slice "projects" }}{{ end }}
|
||||
{{ $query = where $query "Section" "in" $folders }}
|
||||
{{ $query = where $query "Kind" "eq" "page" }}
|
||||
|
||||
@@ -24,9 +45,14 @@
|
||||
{{ $archive_page = site.GetPage "Section" $main_folder }}
|
||||
|
||||
{{/* 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 false }}
|
||||
{{ $sort_ascending := partial "functions/coerce_bool" (dict "value" (index $content "sort_ascending") "default" false) }}
|
||||
{{ $sort_order := cond $sort_ascending "asc" "desc" }}
|
||||
{{ $query = sort $query $sort_by $sort_order }}
|
||||
|
||||
@@ -34,21 +60,34 @@
|
||||
{{ $total_count := len $query }}
|
||||
|
||||
{{/* Limit */}}
|
||||
{{ $count := $block.content.count | default 6 }}
|
||||
{{ $count := partial "functions/coerce_int" (dict "value" (index $content "count") "default" 6 "min" 0) }}
|
||||
{{ if gt $count 0 }}
|
||||
{{ $query = first $count $query }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Get filter buttons */}}
|
||||
{{ $buttons := $block.content.buttons | default (slice (dict "name" "All" "tag" "*")) }}
|
||||
{{ $default_idx := $block.content.default_button_index | default 0 }}
|
||||
{{ $buttons_raw := index $content "buttons" }}
|
||||
{{ $buttons := slice }}
|
||||
{{ if reflect.IsSlice $buttons_raw }}
|
||||
{{ $buttons = $buttons_raw }}
|
||||
{{ else if and $buttons_raw (reflect.IsMap $buttons_raw) }}
|
||||
{{ $buttons = slice $buttons_raw }}
|
||||
{{ end }}
|
||||
{{ if eq (len $buttons) 0 }}
|
||||
{{ $buttons = slice (dict "name" "All" "tag" "*") }}
|
||||
{{ end }}
|
||||
{{ $default_idx := partial "functions/coerce_int" (dict "value" (index $content "default_button_index") "default" 0 "min" 0) }}
|
||||
{{ $default_tag := "*" }}
|
||||
{{ with index $buttons $default_idx }}
|
||||
{{ $default_tag = .tag }}
|
||||
{{ if lt $default_idx (len $buttons) }}
|
||||
{{ with index $buttons $default_idx }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ with index . "tag" }}{{ $default_tag = printf "%v" . }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Columns */}}
|
||||
{{ $columns := $block.design.columns | default 3 }}
|
||||
{{ $columns := partial "functions/coerce_int" (dict "value" (index $design "columns") "default" 3 "min" 1 "max" 4) }}
|
||||
{{ $grid_class := "md:grid-cols-2 lg:grid-cols-3" }}
|
||||
{{ if eq $columns 2 }}
|
||||
{{ $grid_class = "md:grid-cols-2" }}
|
||||
@@ -56,18 +95,24 @@
|
||||
{{ $grid_class = "md:grid-cols-2 lg:grid-cols-4" }}
|
||||
{{ end }}
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16" x-data="{ activeFilter: '{{ $default_tag }}' }">
|
||||
{{ $fallback_icon := "code-bracket" }}
|
||||
{{ $fallback_raw := index $design "fallback_icon" }}
|
||||
{{ if eq (printf "%T" $fallback_raw) "string" }}
|
||||
{{ $fallback_icon = $fallback_raw }}
|
||||
{{ end }}
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16" x-data="{ activeFilter: {{ $default_tag | jsonify }} }">
|
||||
|
||||
{{/* Header */}}
|
||||
<div class="text-center mb-12">
|
||||
{{ with $block.content.title }}
|
||||
{{ with index $content "title" }}
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
{{ printf "%v" . | emojify | $page.RenderString }}
|
||||
</h2>
|
||||
{{ end }}
|
||||
{{ with $block.content.subtitle }}
|
||||
{{ with index $content "subtitle" }}
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
{{ printf "%v" . | emojify | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -76,16 +121,22 @@
|
||||
{{ if gt (len $buttons) 1 }}
|
||||
<div class="flex flex-wrap justify-center gap-2 mb-10">
|
||||
{{ range $idx, $button := $buttons }}
|
||||
{{ if reflect.IsMap $button }}
|
||||
{{ $button_name := "" }}
|
||||
{{ with index $button "name" }}{{ $button_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $button_tag := "*" }}
|
||||
{{ with index $button "tag" }}{{ $button_tag = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
<button
|
||||
@click="activeFilter = '{{ $button.tag }}'"
|
||||
:class="activeFilter === '{{ $button.tag }}'
|
||||
@click="activeFilter = {{ $button_tag | jsonify }}"
|
||||
:class="activeFilter === {{ $button_tag | jsonify }}
|
||||
? 'bg-primary-600 text-white border-primary-600 shadow-lg shadow-primary-500/25'
|
||||
: 'bg-gray-100 dark:bg-white/5 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-white/10 hover:bg-gray-200 dark:hover:bg-white/10 hover:border-gray-300 dark:hover:border-white/20'"
|
||||
class="px-5 py-2.5 rounded-full text-sm font-medium border backdrop-blur-sm transition-all duration-300 ease-out cursor-pointer"
|
||||
>
|
||||
{{ $button.name }}
|
||||
{{ $button_name | default "All" }}
|
||||
</button>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -93,22 +144,45 @@
|
||||
<div class="grid grid-cols-1 {{ $grid_class }} gap-6" data-stagger="100">
|
||||
{{ range $index, $item := $query }}
|
||||
{{/* Get tags as comma-separated string */}}
|
||||
{{ $item_tags := "" }}
|
||||
{{ with $item.Params.tags }}
|
||||
{{ $item_tags = delimit . "," }}
|
||||
{{ $tags := slice }}
|
||||
{{ $tags_raw := $item.Params.tags }}
|
||||
{{ 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_string := strings.TrimSpace $tags_raw }}
|
||||
{{ if $tag_string }}
|
||||
{{ if strings.Contains $tag_string "," }}
|
||||
{{ range (split $tag_string ",") }}
|
||||
{{ $tag := strings.TrimSpace . }}
|
||||
{{ if $tag }}{{ $tags = $tags | append $tag }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ $tags = slice $tag_string }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $item_tags := delimit $tags "," }}
|
||||
{{ $item_tags_json := $item_tags | jsonify }}
|
||||
|
||||
{{/* Get status */}}
|
||||
{{ $status := $item.Params.status | default "" }}
|
||||
{{ $status := "" }}
|
||||
{{ $status_raw := $item.Params.status }}
|
||||
{{ if eq (printf "%T" $status_raw) "string" }}
|
||||
{{ $status = strings.TrimSpace $status_raw }}
|
||||
{{ end }}
|
||||
{{ $status_norm := upper $status }}
|
||||
{{ $status_class := "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border-emerald-500/30" }}
|
||||
{{ if eq $status "WIP" }}
|
||||
{{ if eq $status_norm "WIP" }}
|
||||
{{ $status_class = "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30" }}
|
||||
{{ else if eq $status "Archived" }}
|
||||
{{ else if eq $status_norm "ARCHIVED" }}
|
||||
{{ $status_class = "bg-gray-500/20 text-gray-600 dark:text-gray-400 border-gray-500/30" }}
|
||||
{{ end }}
|
||||
|
||||
<article
|
||||
x-show="activeFilter === '*' || '{{ $item_tags }}'.toLowerCase().includes(activeFilter.toLowerCase())"
|
||||
x-show="activeFilter === '*' || {{ $item_tags_json }}.toLowerCase().includes(activeFilter.toLowerCase())"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-95 translate-y-4"
|
||||
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
||||
@@ -137,7 +211,6 @@
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{/* Gradient placeholder with configurable icon */}}
|
||||
{{ $fallback_icon := $block.design.fallback_icon | default "code-bracket" }}
|
||||
<div class="w-full h-full bg-gradient-to-br from-primary-600/40 via-secondary-600/30 to-primary-800/50 flex items-center justify-center">
|
||||
{{ partial "functions/get_icon" (dict "name" $fallback_icon "attributes" "class=\"w-16 h-16 text-white/30\"") }}
|
||||
</div>
|
||||
@@ -166,9 +239,14 @@
|
||||
</h3>
|
||||
|
||||
{{/* Summary */}}
|
||||
{{ with $item.Params.summary }}
|
||||
{{ $summary := "" }}
|
||||
{{ $summary_raw := $item.Params.summary }}
|
||||
{{ if $summary_raw }}
|
||||
{{ $summary = strings.TrimSpace (printf "%v" $summary_raw) }}
|
||||
{{ end }}
|
||||
{{ if $summary }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
|
||||
{{ . }}
|
||||
{{ $summary | $page.RenderString }}
|
||||
</p>
|
||||
{{ else }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
|
||||
@@ -177,56 +255,122 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Tech Stack Tags - fixed 2-line height for consistency */}}
|
||||
{{ with $item.Params.tech_stack }}
|
||||
{{ $tech_stack := slice }}
|
||||
{{ $tech_raw := $item.Params.tech_stack }}
|
||||
{{ if reflect.IsSlice $tech_raw }}
|
||||
{{ range $tech_raw }}
|
||||
{{ $tech := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $tech }}{{ $tech_stack = $tech_stack | append $tech }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $tech_raw) "string" }}
|
||||
{{ $tech_string := strings.TrimSpace $tech_raw }}
|
||||
{{ if $tech_string }}
|
||||
{{ if strings.Contains $tech_string "," }}
|
||||
{{ range (split $tech_string ",") }}
|
||||
{{ $tech := strings.TrimSpace . }}
|
||||
{{ if $tech }}{{ $tech_stack = $tech_stack | append $tech }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ $tech_stack = slice $tech_string }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if gt (len $tech_stack) 0 }}
|
||||
<div class="flex flex-wrap gap-1.5 mb-4 min-h-[3.5rem]">
|
||||
{{ range first 4 . }}
|
||||
{{ range first 4 $tech_stack }}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-md bg-primary-100 dark:bg-primary-500/10 text-primary-700 dark:text-primary-300 border border-primary-200 dark:border-primary-500/20 h-fit">
|
||||
{{ . }}
|
||||
</span>
|
||||
{{ end }}
|
||||
{{ if gt (len .) 4 }}
|
||||
{{ if gt (len $tech_stack) 4 }}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-md bg-gray-100 dark:bg-gray-500/10 text-gray-600 dark:text-gray-400 h-fit">
|
||||
+{{ sub (len .) 4 }}
|
||||
+{{ sub (len $tech_stack) 4 }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Action Links - pushed to bottom with mt-auto */}}
|
||||
{{ with $item.Params.links }}
|
||||
<div class="flex items-center gap-3 pt-3 border-t border-gray-100 dark:border-white/5 mt-auto">
|
||||
{{ range . }}
|
||||
{{ $icon := "link" }}
|
||||
{{ $label := .label | default .name }}
|
||||
{{/* Fallback to i18n if no custom label provided */}}
|
||||
{{ if not $label }}
|
||||
{{ if eq .type "github" }}
|
||||
{{ $icon = "brands/github" }}
|
||||
{{ $label = i18n "portfolio_link_code" | default "Code" }}
|
||||
{{ else if eq .type "live" }}
|
||||
{{ $icon = "globe-alt" }}
|
||||
{{ $label = i18n "portfolio_link_live" | default "Live" }}
|
||||
{{ else if eq .type "demo" }}
|
||||
{{ $icon = "play" }}
|
||||
{{ $label = i18n "portfolio_link_demo" | default "Demo" }}
|
||||
{{ else }}
|
||||
{{ $label = i18n "portfolio_link_default" | default "Link" }}
|
||||
{{ $links := slice }}
|
||||
{{ $links_raw := $item.Params.links }}
|
||||
{{ $links_iter := slice }}
|
||||
{{ if reflect.IsSlice $links_raw }}
|
||||
{{ $links_iter = $links_raw }}
|
||||
{{ else if and $links_raw (reflect.IsMap $links_raw) }}
|
||||
{{ $links_iter = slice $links_raw }}
|
||||
{{ end }}
|
||||
{{ if gt (len $links_iter) 0 }}
|
||||
{{ range $links_iter }}
|
||||
{{ $url := "" }}
|
||||
{{ $label := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $type := "" }}
|
||||
{{ $new_tab := false }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ with index . "url" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $url }}
|
||||
{{ with index . "link" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{/* Custom label provided, still infer icon from type if not specified */}}
|
||||
{{ if eq .type "github" }}
|
||||
{{ $icon = "brands/github" }}
|
||||
{{ else if eq .type "live" }}
|
||||
{{ $icon = "globe-alt" }}
|
||||
{{ else if eq .type "demo" }}
|
||||
{{ $icon = "play" }}
|
||||
{{ with index . "label" }}{{ $label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $label }}
|
||||
{{ with index . "name" }}{{ $label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
{{ with index . "type" }}{{ $type = lower (strings.TrimSpace (printf "%v" .)) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = printf "%v" . }}{{ end }}
|
||||
{{ $new_tab = partial "functions/coerce_bool" (dict "value" (index . "new_tab") "default" false) }}
|
||||
{{ else if eq (printf "%T" .) "string" }}
|
||||
{{ $url = strings.TrimSpace . }}
|
||||
{{ end }}
|
||||
{{ if $url }}
|
||||
{{ if not $label }}
|
||||
{{ if eq $type "github" }}
|
||||
{{ $label = i18n "portfolio_link_code" | default "Code" }}
|
||||
{{ else if eq $type "live" }}
|
||||
{{ $label = i18n "portfolio_link_live" | default "Live" }}
|
||||
{{ else if eq $type "demo" }}
|
||||
{{ $label = i18n "portfolio_link_demo" | default "Demo" }}
|
||||
{{ else }}
|
||||
{{ $label = i18n "portfolio_link_default" | default "Link" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if not $icon }}
|
||||
{{ if eq $type "github" }}
|
||||
{{ $icon = "brands/github" }}
|
||||
{{ else if eq $type "live" }}
|
||||
{{ $icon = "globe-alt" }}
|
||||
{{ else if eq $type "demo" }}
|
||||
{{ $icon = "play" }}
|
||||
{{ else }}
|
||||
{{ $icon = "link" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $links = $links | append (dict "url" $url "label" $label "icon" $icon "new_tab" $new_tab) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if gt (len $links) 0 }}
|
||||
<div class="flex items-center gap-3 pt-3 border-t border-gray-100 dark:border-white/5 mt-auto">
|
||||
{{ range $links }}
|
||||
{{ $icon := index . "icon" | default "link" }}
|
||||
{{ $label := index . "label" | default (i18n "portfolio_link_default" | default "Link") }}
|
||||
{{ $link_url := index . "url" }}
|
||||
{{ $new_tab := partial "functions/coerce_bool" (dict "value" (index . "new_tab") "default" false) }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link_url).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link_url "#") }}
|
||||
{{ $link_url = $link_url | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $link_url) ".pdf" }}{{ $new_tab = true }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $new_tab = true }}
|
||||
{{ end }}
|
||||
{{ if $new_tab }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener noreferrer\"" }}
|
||||
{{ end }}
|
||||
{{ $icon = .icon | default $icon }}
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="{{ $link_url | safeURL }}"
|
||||
{{ $target | safeHTMLAttr }}
|
||||
class="relative z-10 inline-flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
|
||||
onclick="event.stopPropagation();"
|
||||
>
|
||||
@@ -243,20 +387,43 @@
|
||||
|
||||
{{/* View All Link */}}
|
||||
{{/* Smart default: show link if there are more items than displayed, unless explicitly disabled */}}
|
||||
{{ $show_archive_link := $block.content.archive.enable | default (gt $total_count $count) }}
|
||||
{{ if $show_archive_link | and $archive_page }}
|
||||
{{ $archive := index $content "archive" }}
|
||||
{{ if not (reflect.IsMap $archive) }}{{ $archive = dict }}{{ end }}
|
||||
{{ $default_show_archive := false }}
|
||||
{{ if gt $count 0 }}
|
||||
{{ $default_show_archive = gt $total_count $count }}
|
||||
{{ end }}
|
||||
{{ $show_archive_link := partial "functions/coerce_bool" (dict "value" (index $archive "enable") "default" $default_show_archive) }}
|
||||
{{ if and $show_archive_link $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 }}
|
||||
{{ $archive_target := "" }}
|
||||
{{ $archive_scheme := (urls.Parse $archive_link).Scheme }}
|
||||
{{ if not $archive_scheme }}
|
||||
{{ if not (hasPrefix $archive_link "#") }}
|
||||
{{ $archive_link = $archive_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $archive_scheme }}
|
||||
{{ $archive_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
|
||||
{{ $archive_text := $block.content.archive.text | default (i18n "portfolio_view_all" | default "View All Projects") }}
|
||||
{{ $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 "portfolio_view_all" | default "View All Projects" }}{{ end }}
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a
|
||||
href="{{ $archive_link }}"
|
||||
href="{{ $archive_link | safeURL }}"
|
||||
{{ $archive_target | safeHTMLAttr }}
|
||||
class="inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-gray-800 dark:text-white bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-full hover:bg-gray-200 dark:hover:bg-white/10 hover:border-gray-300 dark:hover:border-white/20 transition-all duration-300"
|
||||
>
|
||||
{{ $archive_text | emojify }}
|
||||
|
||||
@@ -5,11 +5,122 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $layout := $block.design.layout | default "cards" }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $title := $block.content.title | emojify | $page.RenderString }}
|
||||
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $layout := "cards" }}
|
||||
{{ $layout_raw := index $design "layout" }}
|
||||
{{ if eq (printf "%T" $layout_raw) "string" }}
|
||||
{{ $layout_candidate := lower (strings.TrimSpace $layout_raw) }}
|
||||
{{ if in (slice "cards" "hexagon" "timeline") $layout_candidate }}
|
||||
{{ $layout = $layout_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}
|
||||
{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}
|
||||
{{ end }}
|
||||
{{ if $title }}{{ $title = $title | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $subtitle := "" }}
|
||||
{{ $subtitle_raw := index $content "subtitle" }}
|
||||
{{ if $subtitle_raw }}
|
||||
{{ $subtitle = strings.TrimSpace (printf "%v" $subtitle_raw) }}
|
||||
{{ end }}
|
||||
{{ if $subtitle }}{{ $subtitle = $subtitle | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}
|
||||
{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}
|
||||
{{ end }}
|
||||
{{ if $text }}{{ $text = $text | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $items_raw := index $content "items" }}
|
||||
{{ $items_iter := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items_iter = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items_iter = slice $items_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $items := slice }}
|
||||
{{ if gt (len $items_iter) 0 }}
|
||||
{{ range $items_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $name := "" }}
|
||||
{{ $description := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $image := "" }}
|
||||
{{ $gradient := "" }}
|
||||
{{ $url := "" }}
|
||||
{{ $status := "" }}
|
||||
{{ $topics := slice }}
|
||||
{{ $team_size := "" }}
|
||||
{{ $publications := "" }}
|
||||
{{ $funding := "" }}
|
||||
{{ $cta_url := "" }}
|
||||
{{ $cta_text := "" }}
|
||||
{{ $cta_icon := "" }}
|
||||
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "description" }}{{ $description = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = printf "%v" . }}{{ end }}
|
||||
{{ with index . "image" }}{{ $image = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "gradient" }}{{ $gradient = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "url" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "status" }}{{ $status = lower (strings.TrimSpace (printf "%v" .)) }}{{ end }}
|
||||
|
||||
{{ $topics_raw := index . "topics" }}
|
||||
{{ if reflect.IsSlice $topics_raw }}
|
||||
{{ range $topics_raw }}
|
||||
{{ $topic := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $topic }}{{ $topics = $topics | append $topic }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $topics_raw) "string" }}
|
||||
{{ $topic_string := strings.TrimSpace $topics_raw }}
|
||||
{{ if $topic_string }}
|
||||
{{ if strings.Contains $topic_string "," }}
|
||||
{{ range (split $topic_string ",") }}
|
||||
{{ $topic := strings.TrimSpace . }}
|
||||
{{ if $topic }}{{ $topics = $topics | append $topic }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ $topics = slice $topic_string }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ with index . "team_size" }}{{ $team_size = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "publications" }}{{ $publications = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "funding" }}{{ $funding = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $cta_raw := index . "cta" }}
|
||||
{{ if reflect.IsMap $cta_raw }}
|
||||
{{ with index $cta_raw "url" }}{{ $cta_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $cta_raw "text" }}{{ $cta_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $cta_raw "icon" }}{{ $cta_icon = printf "%v" . }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if or $name $description }}
|
||||
{{ $items = $items | append (dict "name" $name "description" $description "icon" $icon "image" $image "gradient" $gradient "url" $url "status" $status "topics" $topics "team_size" $team_size "publications" $publications "funding" $funding "cta" (dict "url" $cta_url "text" $cta_text "icon" $cta_icon)) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $cta := index $content "cta" }}
|
||||
{{ if not (reflect.IsMap $cta) }}{{ $cta = dict }}{{ end }}
|
||||
{{ $cta_text := "" }}
|
||||
{{ $cta_link := "" }}
|
||||
{{ $cta_icon := "" }}
|
||||
{{ with index $cta "text" }}{{ $cta_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $cta "url" }}{{ $cta_link = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $cta "icon" }}{{ $cta_icon = printf "%v" . }}{{ end }}
|
||||
|
||||
<div class="py-16 sm:py-20 lg:py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
@@ -38,36 +149,72 @@
|
||||
{{/* Cards Layout */}}
|
||||
{{ if eq $layout "cards" }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
{{ range $idx, $item := $items }}
|
||||
{{ $name := index $item "name" }}
|
||||
{{ $description := index $item "description" }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ $image := index $item "image" }}
|
||||
{{ $gradient := index $item "gradient" | default "from-primary-400 to-secondary-400" }}
|
||||
{{ $item_url := index $item "url" }}
|
||||
{{ $status := index $item "status" }}
|
||||
{{ $topics := index $item "topics" }}
|
||||
{{ $team_size := index $item "team_size" }}
|
||||
{{ $publications := index $item "publications" }}
|
||||
{{ $funding := index $item "funding" }}
|
||||
{{ $item_cta := index $item "cta" }}
|
||||
{{ $item_cta_url := "" }}
|
||||
{{ $item_cta_text := "" }}
|
||||
{{ $item_cta_icon := "" }}
|
||||
{{ if reflect.IsMap $item_cta }}
|
||||
{{ with index $item_cta "url" }}{{ $item_cta_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $item_cta "text" }}{{ $item_cta_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $item_cta "icon" }}{{ $item_cta_icon = printf "%v" . }}{{ end }}
|
||||
{{ end }}
|
||||
<div class="group relative">
|
||||
<div class="h-full bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 overflow-hidden flex flex-col">
|
||||
|
||||
{{/* Card Image/Icon Header */}}
|
||||
{{ $header_content := "" }}
|
||||
{{ if $item.image }}
|
||||
{{ $img := resources.Get (printf "media/%s" $item.image) }}
|
||||
{{ if $image }}
|
||||
{{ $image_path := strings.TrimPrefix $image "/" }}
|
||||
{{ $img := resources.Get (printf "media/%s" $image_path) }}
|
||||
{{ if $img }}
|
||||
{{ $img = $img.Fill "600x400 Center webp" }}
|
||||
{{ $header_content = printf "<img src=\"%s\" alt=\"%s\" class=\"w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500\">" $img.RelPermalink $item.name }}
|
||||
{{ $alt := $name | plainify }}
|
||||
{{ $header_content = printf "<img src=\"%s\" alt=\"%s\" class=\"w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500\">" $img.RelPermalink $alt }}
|
||||
{{ end }}
|
||||
{{ else if $item.icon }}
|
||||
{{ $icon_html := partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-24 h-24 text-white/80 group-hover:scale-110 transition-transform duration-300\"") }}
|
||||
{{ else if $icon }}
|
||||
{{ $icon_html := partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-24 h-24 text-white/80 group-hover:scale-110 transition-transform duration-300\"") }}
|
||||
{{ $header_content = printf "<div class=\"absolute inset-0 flex items-center justify-center\">%s</div>" $icon_html }}
|
||||
{{ end }}
|
||||
|
||||
{{ if and $item.cta.url $header_content }}
|
||||
<a href="{{ $item.cta.url }}" class="block relative h-48 bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} overflow-hidden">
|
||||
{{ $item_cta_link := "" }}
|
||||
{{ $item_cta_target := "" }}
|
||||
{{ if $item_cta_url }}
|
||||
{{ $item_cta_link = $item_cta_url }}
|
||||
{{ $item_cta_scheme := (urls.Parse $item_cta_link).Scheme }}
|
||||
{{ if not $item_cta_scheme }}
|
||||
{{ if not (hasPrefix $item_cta_link "#") }}
|
||||
{{ $item_cta_link = $item_cta_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $item_cta_scheme }}
|
||||
{{ $item_cta_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if and $item_cta_link $header_content }}
|
||||
<a href="{{ $item_cta_link | safeURL }}" {{ $item_cta_target | safeHTMLAttr }} class="block relative h-48 bg-gradient-to-br {{ $gradient }} overflow-hidden">
|
||||
{{ $header_content | safeHTML }}
|
||||
</a>
|
||||
{{ else }}
|
||||
<div class="relative h-48 bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} overflow-hidden">
|
||||
<div class="relative h-48 bg-gradient-to-br {{ $gradient }} overflow-hidden">
|
||||
{{ $header_content | safeHTML }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{/* Status Badge */}}
|
||||
{{ with $item.status }}
|
||||
{{ with $status }}
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<span class="px-3 py-1 bg-white/90 dark:bg-gray-900/90 backdrop-blur text-xs font-semibold rounded-full
|
||||
{{ if eq . "active" }}text-green-600 dark:text-green-400
|
||||
@@ -84,22 +231,32 @@
|
||||
{{/* Main Content */}}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
|
||||
{{ if $item.url }}
|
||||
<a href="{{ $item.url }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ $item.name }}
|
||||
{{ if $item_url }}
|
||||
{{ $item_link := $item_url }}
|
||||
{{ $item_target := "" }}
|
||||
{{ $item_scheme := (urls.Parse $item_link).Scheme }}
|
||||
{{ if not $item_scheme }}
|
||||
{{ if not (hasPrefix $item_link "#") }}
|
||||
{{ $item_link = $item_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $item_scheme }}
|
||||
{{ $item_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $item_link | safeURL }}" {{ $item_target | safeHTMLAttr }} class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ $name }}
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/arrow-top-right-on-square" "attributes" "class=\"inline-block w-4 h-4 ml-1\"") }}
|
||||
</a>
|
||||
{{ else }}
|
||||
{{ $item.name }}
|
||||
{{ $name }}
|
||||
{{ end }}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{ $item.description }}
|
||||
{{ $description | $page.RenderString }}
|
||||
</p>
|
||||
|
||||
{{/* Key Topics/Keywords */}}
|
||||
{{ with $item.topics }}
|
||||
{{ with $topics }}
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{{ range first 3 . }}
|
||||
<span class="inline-block px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded">
|
||||
@@ -118,23 +275,23 @@
|
||||
{{/* Footer Section - Always at bottom */}}
|
||||
<div class="mt-auto">
|
||||
{{/* Stats/Metrics */}}
|
||||
{{ if or $item.team_size $item.publications $item.funding }}
|
||||
{{ if or $team_size $publications $funding }}
|
||||
<div class="grid grid-cols-3 gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 mb-4">
|
||||
{{ with $item.team_size }}
|
||||
{{ with $team_size }}
|
||||
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/user-group" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
|
||||
<span class="text-xs leading-tight">{{ . | replaceRE " researchers" "" }}</span>
|
||||
<span class="text-xs text-gray-400">team</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ with $item.publications }}
|
||||
{{ with $publications }}
|
||||
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/document-text" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
|
||||
<span class="text-xs leading-tight">{{ . | replaceRE " papers" "" }}</span>
|
||||
<span class="text-xs text-gray-400">papers</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ with $item.funding }}
|
||||
{{ with $funding }}
|
||||
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/currency-dollar" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
|
||||
<span class="text-xs leading-tight">{{ . }}</span>
|
||||
@@ -145,10 +302,10 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Call to Action */}}
|
||||
{{ with $item.cta }}
|
||||
<a href="{{ .url }}"
|
||||
{{ if and $item_cta_link $item_cta_text }}
|
||||
<a href="{{ $item_cta_link | safeURL }}" {{ $item_cta_target | safeHTMLAttr }}
|
||||
class="inline-flex items-center w-full justify-center px-4 py-2 bg-primary-50 dark:bg-primary-900/20 text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium text-sm rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-all duration-300">
|
||||
{{ .text }}
|
||||
{{ $item_cta_text }}
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/arrow-right" "attributes" "class=\"ml-1 w-4 h-4\"") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
@@ -162,15 +319,19 @@
|
||||
{{/* Hexagon Layout */}}
|
||||
{{ else if eq $layout "hexagon" }}
|
||||
<div class="flex flex-wrap justify-center gap-8 lg:gap-4">
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
{{ range $idx, $item := $items }}
|
||||
{{ $name := index $item "name" }}
|
||||
{{ $description := index $item "description" }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ $gradient := index $item "gradient" | default "from-primary-400 to-secondary-400" }}
|
||||
<div class="hexagon-container group">
|
||||
<div class="hexagon bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||
<div class="hexagon bg-gradient-to-br {{ $gradient }} shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||
<div class="hexagon-content">
|
||||
{{ if $item.icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-12 h-12 text-white mb-3\"") }}
|
||||
{{ if $icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-12 h-12 text-white mb-3\"") }}
|
||||
{{ end }}
|
||||
<h3 class="text-lg font-bold text-white mb-2">{{ $item.name }}</h3>
|
||||
<p class="text-white/90 text-sm">{{ $item.description | truncate 60 }}</p>
|
||||
<h3 class="text-lg font-bold text-white mb-2">{{ $name }}</h3>
|
||||
<p class="text-white/90 text-sm">{{ $description | truncate 60 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +345,10 @@
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-gradient-to-b from-primary-400 to-secondary-400"></div>
|
||||
|
||||
<div class="space-y-12">
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
{{ range $idx, $item := $items }}
|
||||
{{ $name := index $item "name" }}
|
||||
{{ $description := index $item "description" }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
<div class="relative flex items-center {{ if eq (mod $idx 2) 0 }}justify-start{{ else }}justify-end{{ end }}">
|
||||
{{/* Timeline dot */}}
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2 w-8 h-8 bg-white dark:bg-gray-800 border-4 border-primary-500 rounded-full z-10"></div>
|
||||
@@ -192,14 +356,14 @@
|
||||
<div class="w-5/12 {{ if eq (mod $idx 2) 1 }}mr-auto{{ end }}">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
{{ if $item.icon }}
|
||||
{{ if $icon }}
|
||||
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{{ $item.name }}</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{{ $name }}</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">{{ $item.description }}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">{{ $description | $page.RenderString }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,12 +373,21 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Global Call to Action */}}
|
||||
{{ with $block.content.cta }}
|
||||
{{ if and $cta_link $cta_text }}
|
||||
{{ $cta_target := "" }}
|
||||
{{ $cta_scheme := (urls.Parse $cta_link).Scheme }}
|
||||
{{ if not $cta_scheme }}
|
||||
{{ if not (hasPrefix $cta_link "#") }}
|
||||
{{ $cta_link = $cta_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $cta_scheme }}
|
||||
{{ $cta_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<div class="mt-12 text-center">
|
||||
<a href="{{ .url }}"
|
||||
<a href="{{ $cta_link | safeURL }}" {{ $cta_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 hover:shadow-lg transition-all duration-300">
|
||||
{{ .text }}
|
||||
{{ with .icon }}
|
||||
{{ $cta_text }}
|
||||
{{ with $cta_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
|
||||
{{ end }}
|
||||
</a>
|
||||
|
||||
@@ -5,51 +5,157 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $awards := $profile.awards }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $username }}
|
||||
{{ $awards_raw := $profile.awards }}
|
||||
|
||||
{{ $date_format := "January 2006" }}
|
||||
{{ $date_format_raw := index $design "date_format" }}
|
||||
{{ if eq (printf "%T" $date_format_raw) "string" }}
|
||||
{{ $date_format = strings.TrimSpace $date_format_raw | default "January 2006" }}
|
||||
{{ end }}
|
||||
|
||||
{{ $awards_iter := slice }}
|
||||
{{ if reflect.IsSlice $awards_raw }}
|
||||
{{ $awards_iter = $awards_raw }}
|
||||
{{ else if reflect.IsMap $awards_raw }}
|
||||
{{ range $awards_raw }}
|
||||
{{ $awards_iter = $awards_iter | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $awards := slice }}
|
||||
{{ if gt (len $awards_iter) 0 }}
|
||||
{{ range $awards_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $title := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $url := "" }}
|
||||
{{ $awarder := "" }}
|
||||
{{ $date := "" }}
|
||||
{{ $date_start := "" }}
|
||||
{{ $summary := "" }}
|
||||
{{ $certificate_url := "" }}
|
||||
|
||||
{{ with index . "title" }}{{ $title = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "url" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "awarder" }}{{ $awarder = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "date" }}{{ $date = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "date_start" }}{{ $date_start = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "summary" }}{{ $summary = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "certificate_url" }}{{ $certificate_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ if $title }}
|
||||
{{ $date_sort := $date | default $date_start }}
|
||||
{{ $awards = $awards | append (dict "title" $title "icon" $icon "url" $url "awarder" $awarder "date" $date "date_start" $date_start "summary" $summary "certificate_url" $certificate_url "date_sort" $date_sort) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}
|
||||
{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}
|
||||
{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}
|
||||
{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}
|
||||
{{ end }}
|
||||
|
||||
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
|
||||
|
||||
<div class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $block.content.title | markdownify | emojify }}
|
||||
{{ $title | markdownify | emojify }}
|
||||
</div>
|
||||
|
||||
{{ with $block.content.text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
{{ with $text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
|
||||
<div class="w-full flex flex-col gap-6">
|
||||
{{ if $awards }}
|
||||
{{ range $idx, $key := sort $awards ".date" "desc" }}
|
||||
{{ if gt (len $awards) 0 }}
|
||||
{{ range $idx, $award := sort $awards "date_sort" "desc" }}
|
||||
<div class="w-full p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
|
||||
{{- $icon := .icon }}
|
||||
{{with .url}}<a href={{.}} target="_blank" rel="noopener">{{end}}
|
||||
{{- $icon := index $award "icon" }}
|
||||
{{ $award_url := index $award "url" }}
|
||||
{{ $award_link := "" }}
|
||||
{{ $award_target := "" }}
|
||||
{{ if $award_url }}
|
||||
{{ $award_link = $award_url }}
|
||||
{{ $scheme := (urls.Parse $award_link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $award_link "#") }}
|
||||
{{ $award_link = $award_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $award_link) ".pdf" }}{{ $award_target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $award_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if $award_link }}<a href="{{ $award_link | safeURL }}" {{ $award_target | safeHTMLAttr }}>{{ end }}
|
||||
<h5 class="mb-2 text-2xl font-semibold tracking-tight text-gray-900 dark:text-white flex items-center gap-2">
|
||||
{{ with $icon }}
|
||||
<span class="inline-flex items-center justify-center w-7 h-7 text-primary-600 dark:text-primary-300">
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-6 h-6'") }}
|
||||
</span>
|
||||
{{ end }}
|
||||
<span>{{.title | markdownify | emojify}}</span>
|
||||
<span>{{ index $award "title" | markdownify | emojify }}</span>
|
||||
</h5>
|
||||
{{with .url}}</a>{{end}}
|
||||
{{ if $award_link }}</a>{{ end }}
|
||||
|
||||
<div class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
|
||||
{{ .awarder }} ∙
|
||||
{{ if .date }}
|
||||
{{ (time .date) | time.Format ($block.design.date_format | default "January 2006") }}
|
||||
{{ else if .date_start }}
|
||||
{{ (time .date_start) | time.Format ($block.design.date_format | default "January 2006") }}
|
||||
{{ $awarder := index $award "awarder" }}
|
||||
{{ if $awarder }}{{ $awarder }} ∙{{ end }}
|
||||
{{ $date_value := index $award "date" }}
|
||||
{{ $date_start_value := index $award "date_start" }}
|
||||
{{ if $date_value }}
|
||||
{{ $parsed := try (time $date_value) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }}
|
||||
{{ else }}
|
||||
{{ $date_value }}
|
||||
{{ end }}
|
||||
{{ else if $date_start_value }}
|
||||
{{ $parsed := try (time $date_start_value) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }}
|
||||
{{ else }}
|
||||
{{ $date_start_value }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ i18n "present" | default "Present" }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{with .summary}}
|
||||
<div class="mb-3 font-normal text-gray-500 dark:text-gray-400 prose">{{. | markdownify | emojify}}</div>
|
||||
{{ with index $award "summary" }}
|
||||
<div class="mb-3 font-normal text-gray-500 dark:text-gray-400 prose">{{ . | markdownify | emojify }}</div>
|
||||
{{end}}
|
||||
|
||||
{{ with .certificate_url }}
|
||||
<a href="{{.}}" target="_blank" rel="noopener" class="inline-flex items-center text-primary-600 dark:text-primary-300 hover:underline">
|
||||
{{ $certificate_url := index $award "certificate_url" }}
|
||||
{{ if $certificate_url }}
|
||||
{{ $certificate_link := $certificate_url }}
|
||||
{{ $certificate_target := "" }}
|
||||
{{ $certificate_scheme := (urls.Parse $certificate_link).Scheme }}
|
||||
{{ if not $certificate_scheme }}
|
||||
{{ if not (hasPrefix $certificate_link "#") }}
|
||||
{{ $certificate_link = $certificate_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $certificate_link) ".pdf" }}{{ $certificate_target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $certificate_scheme }}
|
||||
{{ $certificate_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $certificate_link | safeURL }}" {{ $certificate_target | safeHTMLAttr }} class="inline-flex items-center text-primary-600 dark:text-primary-300 hover:underline">
|
||||
{{ i18n "see_certificate" | default "See certificate" }}
|
||||
<svg class="w-3 h-3 ms-2.5 rtl:rotate-[270deg]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11v4.833A1.166 1.166 0 0 1 13.833 17H2.167A1.167 1.167 0 0 1 1 15.833V4.167A1.166 1.166 0 0 1 2.167 3h4.618m4.447-2H17v5.768M9.111 8.889l7.778-7.778"/>
|
||||
|
||||
@@ -6,13 +6,39 @@
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $username }}
|
||||
{{ $avatar := $profile.avatar }}
|
||||
|
||||
{{/* Avatar customization parameters */}}
|
||||
{{ $avatar_size := $block.design.avatar.size | default "medium" }}
|
||||
{{ $avatar_shape := $block.design.avatar.shape | default "circle" }}
|
||||
{{ $avatar_size := "medium" }}
|
||||
{{ $avatar_shape := "circle" }}
|
||||
{{ $avatar_design := index $design "avatar" }}
|
||||
{{ if reflect.IsMap $avatar_design }}
|
||||
{{ $size_raw := index $avatar_design "size" }}
|
||||
{{ if eq (printf "%T" $size_raw) "string" }}
|
||||
{{ $size_candidate := lower (strings.TrimSpace $size_raw) }}
|
||||
{{ if in (slice "small" "medium" "large" "xl" "xxl") $size_candidate }}
|
||||
{{ $avatar_size = $size_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $shape_raw := index $avatar_design "shape" }}
|
||||
{{ if eq (printf "%T" $shape_raw) "string" }}
|
||||
{{ $shape_candidate := lower (strings.TrimSpace $shape_raw) }}
|
||||
{{ if in (slice "circle" "square" "rounded") $shape_candidate }}
|
||||
{{ $avatar_shape = $shape_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Size mappings optimized for 2025 standards: display_size -> [display_px, generation_px] */}}
|
||||
{{ $size_map := dict "small" (slice "150" "300") "medium" (slice "200" "400") "large" (slice "320" "640") "xl" (slice "400" "800") "xxl" (slice "500" "1000") }}
|
||||
@@ -25,111 +51,230 @@
|
||||
{{ $shape_class := index $shape_classes $avatar_shape | default "rounded-full" }}
|
||||
|
||||
{{ $img := "" }}
|
||||
{{with $block.design.banner.filename}}
|
||||
{{- $img = resources.Get (path.Join "media" .) -}}
|
||||
{{ if $img }}
|
||||
{{ 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)
|
||||
) }}
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-900 flex flex-wrap items-center justify-center">
|
||||
<div class="w-full bg-white rounded dark:bg-gray-800">
|
||||
<div class="h-2/4 sm:h-64 overflow-hidden w-full">
|
||||
<img class="w-full object-cover"
|
||||
srcset="{{ $responsive.srcset }}"
|
||||
sizes="100vw"
|
||||
src="{{ $responsive.fallback.RelPermalink }}"
|
||||
width="{{ $responsive.fallback.Width }}"
|
||||
height="{{ $responsive.fallback.Height }}"
|
||||
alt="" />
|
||||
{{ $banner := index $design "banner" }}
|
||||
{{ $banner_filename := "" }}
|
||||
{{ if reflect.IsMap $banner }}
|
||||
{{ with index $banner "filename" }}{{ $banner_filename = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
{{ if $banner_filename }}
|
||||
{{ $banner_path := strings.TrimPrefix $banner_filename "/" }}
|
||||
{{- $img = resources.Get (path.Join "media" $banner_path) -}}
|
||||
{{ if $img }}
|
||||
{{ 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)
|
||||
) }}
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-900 flex flex-wrap items-center justify-center">
|
||||
<div class="w-full bg-white rounded dark:bg-gray-800">
|
||||
<div class="h-2/4 sm:h-64 overflow-hidden w-full">
|
||||
<img class="w-full object-cover"
|
||||
srcset="{{ $responsive.srcset }}"
|
||||
sizes="100vw"
|
||||
src="{{ $responsive.fallback.RelPermalink }}"
|
||||
width="{{ $responsive.fallback.Width }}"
|
||||
height="{{ $responsive.fallback.Height }}"
|
||||
alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{/* Handle GIF without processing */}}
|
||||
{{- $img = $img.Process "webp" -}}
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-900 flex flex-wrap items-center justify-center">
|
||||
<div class="w-full bg-white rounded dark:bg-gray-800">
|
||||
<div class="h-2/4 sm:h-64 overflow-hidden w-full">
|
||||
<img class="w-full object-cover"
|
||||
src="{{$img.RelPermalink}}"
|
||||
width="{{$img.Width}}" height="{{$img.Height}}"
|
||||
alt="" />
|
||||
{{ else }}
|
||||
{{/* Handle GIF without processing */}}
|
||||
{{- $img = $img.Process "webp" -}}
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-900 flex flex-wrap items-center justify-center">
|
||||
<div class="w-full bg-white rounded dark:bg-gray-800">
|
||||
<div class="h-2/4 sm:h-64 overflow-hidden w-full">
|
||||
<img class="w-full object-cover"
|
||||
src="{{$img.RelPermalink}}"
|
||||
width="{{$img.Width}}" height="{{$img.Height}}"
|
||||
alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $profile_title := "" }}
|
||||
{{ with $profile.title }}{{ $profile_title = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $profile_pronouns := "" }}
|
||||
{{ with $profile.pronouns }}{{ $profile_pronouns = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $profile_role := "" }}
|
||||
{{ with $profile.role }}{{ $profile_role = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $profile_bio := "" }}
|
||||
{{ with $profile.bio }}{{ $profile_bio = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $status_icon := "" }}
|
||||
{{ with $profile.status }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ with index . "icon" }}{{ $status_icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $affiliations := slice }}
|
||||
{{ $affiliations_raw := $profile.affiliations }}
|
||||
{{ if reflect.IsSlice $affiliations_raw }}
|
||||
{{ range $affiliations_raw }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $name := "" }}
|
||||
{{ $url := "" }}
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "url" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if $name }}
|
||||
{{ $affiliations = $affiliations | append (dict "name" $name "url" $url) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $links := slice }}
|
||||
{{ $links_raw := $profile.links }}
|
||||
{{ $links_iter := slice }}
|
||||
{{ if reflect.IsSlice $links_raw }}
|
||||
{{ $links_iter = $links_raw }}
|
||||
{{ else if and $links_raw (reflect.IsMap $links_raw) }}
|
||||
{{ $links_iter = slice $links_raw }}
|
||||
{{ end }}
|
||||
{{ if gt (len $links_iter) 0 }}
|
||||
{{ range $links_iter }}
|
||||
{{ $url := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $label := "" }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ with index . "url" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $url }}
|
||||
{{ with index . "link" }}{{ $url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "label" }}{{ $label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ else if eq (printf "%T" .) "string" }}
|
||||
{{ $url = strings.TrimSpace . }}
|
||||
{{ end }}
|
||||
{{ if $url }}
|
||||
{{ if not $icon }}{{ $icon = "hero/link" }}{{ end }}
|
||||
{{ $links = $links | append (dict "url" $url "icon" $icon "label" $label) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $bio_style := "" }}
|
||||
{{ $bio_design := index $design "biography" }}
|
||||
{{ if reflect.IsMap $bio_design }}
|
||||
{{ with index $bio_design "style" }}{{ $bio_style = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $bio_override := "" }}
|
||||
{{ $bio_raw := index $content "text" }}
|
||||
{{ if $bio_raw }}
|
||||
{{ $bio_override = strings.TrimSpace (printf "%v" $bio_raw) }}
|
||||
{{ end }}
|
||||
{{ $bio_content := $profile_bio }}
|
||||
{{ if $bio_override }}{{ $bio_content = $bio_override }}{{ end }}
|
||||
|
||||
{{ $button := index $content "button" }}
|
||||
{{ if not (reflect.IsMap $button) }}{{ $button = dict }}{{ end }}
|
||||
{{ $button_text := "" }}
|
||||
{{ $button_link := "" }}
|
||||
{{ with index $button "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button "url" }}{{ $button_link = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
<div id="profile" class="resume-biography flex justify-center items-center flex-col dark:text-white">
|
||||
{{ if $avatar }}
|
||||
|
||||
<div class="avatar-wrapper {{ if $img }}-mt-[105px]{{else}}mt-10{{end}}" style="width: {{$display_size}}px; height: {{$display_size}}px;">
|
||||
{{ $avatar_image := $avatar.Fill (printf "%sx%s Center" $generation_size $generation_size) }}
|
||||
<img class="avatar {{$shape_class}} bg-white dark:bg-gray-800 p-1" src="{{ $avatar_image.RelPermalink }}" alt="{{$profile.title}}"
|
||||
<img class="avatar {{$shape_class}} bg-white dark:bg-gray-800 p-1" src="{{ $avatar_image.RelPermalink }}" alt="{{ $profile_title }}"
|
||||
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 }}
|
||||
|
||||
<div class="text-3xl font-bold mb-2 mt-6">
|
||||
{{- $profile.title -}}
|
||||
{{- $profile_title -}}
|
||||
</div>
|
||||
{{- with $profile.pronouns -}}
|
||||
{{- with $profile_pronouns -}}
|
||||
<div class="text-lg font-normal text-gray-600 dark:text-gray-400 -mt-1 mb-2">({{ . }})</div>
|
||||
{{- end -}}
|
||||
|
||||
{{ with $profile.role }}<h3 class="font-semibold mb-1">{{ . | markdownify | emojify }}</h3>{{ end }}
|
||||
{{ with $profile_role }}<h3 class="font-semibold mb-1">{{ . | markdownify | emojify }}</h3>{{ end }}
|
||||
|
||||
{{ range $profile.affiliations }}
|
||||
{{ range $affiliations }}
|
||||
<div class="mb-2">
|
||||
{{ with .url }}<a href="{{ . }}" target="_blank" rel="noopener">{{ end }}
|
||||
<div>{{ .name }}</div>
|
||||
{{ if .url }}</a>{{ end }}
|
||||
{{ $name := index . "name" }}
|
||||
{{ $url := index . "url" }}
|
||||
{{ if $url }}
|
||||
{{ $link := $url }}
|
||||
{{ $target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}>
|
||||
<div>{{ $name }}</div>
|
||||
</a>
|
||||
{{ else }}
|
||||
<div>{{ $name }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $profile.links }}
|
||||
{{ if gt (len $links) 0 }}
|
||||
<ul class="network-icon dark:text-zinc-100">
|
||||
{{ 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\"" }}
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ .icon }}"
|
||||
{{ with .label }} data-toggle="tooltip" data-placement="top" title="{{.}}"{{ end }}>
|
||||
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "style=\"height: 1.5rem;\"") }}
|
||||
</a>
|
||||
</li>
|
||||
{{ range $links }}
|
||||
{{ $link := index . "url" }}
|
||||
{{ $icon := index . "icon" | default "hero/link" }}
|
||||
{{ $label := index . "label" }}
|
||||
{{ $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 }}
|
||||
{{ $aria := $label | default $icon | default "link" }}
|
||||
<li>
|
||||
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ $aria }}"
|
||||
{{ if $label }} data-toggle="tooltip" data-placement="top" title="{{ $label | htmlEscape }}"{{ end }}>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "style=\"height: 1.5rem;\"") }}
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ $bio_content := ($block.content.text | emojify | $page.RenderString) | default $profile.bio }}
|
||||
{{ with $bio_content }}
|
||||
{{ if $bio_content }}
|
||||
<div class="pt-2 d-flex justify-content-center prose prose-slate lg:prose-xl dark:prose-invert">
|
||||
<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 }}>
|
||||
{{ $bio_content | emojify | $page.RenderString }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with $block.content.button }}
|
||||
<a href="{{.url}}" target="_blank" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700"><svg class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
{{ if and $button_text $button_link }}
|
||||
{{ $button_href := $button_link }}
|
||||
{{ $button_target := "" }}
|
||||
{{ $button_scheme := (urls.Parse $button_href).Scheme }}
|
||||
{{ if not $button_scheme }}
|
||||
{{ if not (hasPrefix $button_href "#") }}
|
||||
{{ $button_href = $button_href | relLangURL }}
|
||||
{{ end }}
|
||||
{{ if eq (path.Ext $button_href) ".pdf" }}{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $button_scheme }}
|
||||
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $button_href | safeURL }}" {{ $button_target | safeHTMLAttr }} class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700"><svg class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M14.707 7.793a1 1 0 0 0-1.414 0L11 10.086V1.5a1 1 0 0 0-2 0v8.586L6.707 7.793a1 1 0 1 0-1.414 1.414l4 4a1 1 0 0 0 1.416 0l4-4a1 1 0 0 0-.002-1.414Z"/>
|
||||
<path d="M18 12h-2.55l-2.975 2.975a3.5 3.5 0 0 1-4.95 0L4.55 12H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
|
||||
</svg> {{.text}}</a>
|
||||
</svg> {{ $button_text }}</a>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
@@ -5,18 +5,149 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $is_education_first := $block.design.is_education_first | default false }}
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}{{ end }}
|
||||
{{ if $text }}{{ $text = $text | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $username }}
|
||||
|
||||
{{ $is_education_first := partial "functions/coerce_bool" (dict "value" (index $design "is_education_first") "default" false) }}
|
||||
{{ $date_format := "Jan 2006" }}
|
||||
{{ $date_format_raw := index $design "date_format" }}
|
||||
{{ if eq (printf "%T" $date_format_raw) "string" }}
|
||||
{{ $date_format = strings.TrimSpace $date_format_raw | default "Jan 2006" }}
|
||||
{{ end }}
|
||||
|
||||
{{ $experience_raw := or $profile.experience $profile.work }}
|
||||
{{ $experience_iter := slice }}
|
||||
{{ if reflect.IsSlice $experience_raw }}
|
||||
{{ $experience_iter = $experience_raw }}
|
||||
{{ else if and $experience_raw (reflect.IsMap $experience_raw) }}
|
||||
{{ $experience_iter = slice $experience_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $experience := slice }}
|
||||
{{ if gt (len $experience_iter) 0 }}
|
||||
{{ range $experience_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $role := "" }}
|
||||
{{ $org := "" }}
|
||||
{{ $start := "" }}
|
||||
{{ $end := "" }}
|
||||
{{ $summary := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $tags := slice }}
|
||||
{{ $button_url := "" }}
|
||||
{{ $button_text := "" }}
|
||||
{{ $button_icon := "" }}
|
||||
|
||||
{{ with index . "role" }}{{ $role = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $role }}{{ with index . "position" }}{{ $role = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "org" }}{{ $org = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $org }}{{ with index . "company_name" }}{{ $org = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "start" }}{{ $start = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $start }}{{ with index . "date_start" }}{{ $start = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "end" }}{{ $end = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $end }}{{ with index . "date_end" }}{{ $end = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "summary" }}{{ $summary = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $tags_raw := index . "tags" }}
|
||||
{{ 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_string := strings.TrimSpace $tags_raw }}
|
||||
{{ if $tag_string }}
|
||||
{{ if strings.Contains $tag_string "," }}
|
||||
{{ range (split $tag_string ",") }}
|
||||
{{ $tag := strings.TrimSpace . }}
|
||||
{{ if $tag }}{{ $tags = $tags | append $tag }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ $tags = slice $tag_string }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $button_raw := index . "button" }}
|
||||
{{ if reflect.IsMap $button_raw }}
|
||||
{{ with index $button_raw "url" }}{{ $button_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button_raw "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button_raw "icon" }}{{ $button_icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if or $role $org }}
|
||||
{{ $experience = $experience | append (dict "role" $role "org" $org "start" $start "end" $end "summary" $summary "icon" $icon "tags" $tags "button" (dict "url" $button_url "text" $button_text "icon" $button_icon)) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $education_raw := $profile.education }}
|
||||
{{ $education_iter := slice }}
|
||||
{{ if reflect.IsSlice $education_raw }}
|
||||
{{ $education_iter = $education_raw }}
|
||||
{{ else if and $education_raw (reflect.IsMap $education_raw) }}
|
||||
{{ $education_iter = slice $education_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $education := slice }}
|
||||
{{ if gt (len $education_iter) 0 }}
|
||||
{{ range $education_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $degree := "" }}
|
||||
{{ $institution := "" }}
|
||||
{{ $start := "" }}
|
||||
{{ $end := "" }}
|
||||
{{ $summary := "" }}
|
||||
{{ $icon := "" }}
|
||||
{{ $button_url := "" }}
|
||||
{{ $button_text := "" }}
|
||||
{{ $button_icon := "" }}
|
||||
|
||||
{{ with index . "degree" }}{{ $degree = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $degree }}{{ with index . "area" }}{{ $degree = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "institution" }}{{ $institution = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "start" }}{{ $start = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $start }}{{ with index . "date_start" }}{{ $start = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "end" }}{{ $end = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if not $end }}{{ with index . "date_end" }}{{ $end = strings.TrimSpace (printf "%v" .) }}{{ end }}{{ end }}
|
||||
{{ with index . "summary" }}{{ $summary = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $button_raw := index . "button" }}
|
||||
{{ if reflect.IsMap $button_raw }}
|
||||
{{ with index $button_raw "url" }}{{ $button_url = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button_raw "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $button_raw "icon" }}{{ $button_icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if or $degree $institution }}
|
||||
{{ $education = $education | append (dict "degree" $degree "institution" $institution "start" $start "end" $end "summary" $summary "icon" $icon "button" (dict "url" $button_url "text" $button_text "icon" $button_icon)) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<div class="mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<div class="flex flex-col gap-16 {{ if $is_education_first }}flex-col-reverse{{end}}">
|
||||
|
||||
{{ $experience := or $profile.experience $profile.work }}
|
||||
{{ if $experience }}
|
||||
{{ if gt (len $experience) 0 }}
|
||||
<div class="w-full">
|
||||
<h2 class="mb-10 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "experience" | default "Experience" }}</h2>
|
||||
|
||||
@@ -37,8 +168,9 @@
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 w-8 h-8 bg-primary-500/30 rounded-full blur-md group-hover:bg-primary-500/50 transition-all duration-300"></div>
|
||||
<div class="relative w-8 h-8 bg-white dark:bg-gray-900 border-2 border-primary-500 rounded-full flex items-center justify-center group-hover:border-primary-400 group-hover:scale-110 transition-all duration-300">
|
||||
{{ if $item.icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class='w-4 h-4 text-primary-600 dark:text-primary-400'") }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ if $icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class='w-4 h-4 text-primary-600 dark:text-primary-400'") }}
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-4 h-4 text-primary-600 dark:text-primary-400"><path d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z"></path></svg>
|
||||
{{end}}
|
||||
@@ -54,11 +186,11 @@
|
||||
<div>
|
||||
{{/* Role/Position */}}
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ $item.role | default $item.position }}
|
||||
{{ index $item "role" }}
|
||||
</h3>
|
||||
{{/* Company */}}
|
||||
<p class="text-primary-600 dark:text-primary-400 font-medium mt-1">
|
||||
{{ $item.org | default $item.company_name }}
|
||||
{{ index $item "org" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -68,13 +200,23 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<time>
|
||||
{{ $start := or $item.start $item.date_start }}
|
||||
{{ $end := or $item.end $item.date_end }}
|
||||
{{ $start := index $item "start" }}
|
||||
{{ $end := index $item "end" }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "Jan 2006") }} –
|
||||
{{ $parsed := try (time $start) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }} –
|
||||
{{ else }}
|
||||
{{ $start }} –
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "Jan 2006") }}
|
||||
{{ $parsed := try (time $end) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }}
|
||||
{{ else }}
|
||||
{{ $end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<span class="text-primary-600 dark:text-primary-400 font-medium">{{ i18n "present" | default "Present" }}</span>
|
||||
{{end}}
|
||||
@@ -83,14 +225,14 @@
|
||||
</div>
|
||||
|
||||
{{/* Description */}}
|
||||
{{ with $item.summary }}
|
||||
{{ with index $item "summary" }}
|
||||
<div class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Tags/Skills */}}
|
||||
{{ with $item.tags }}
|
||||
{{ with index $item "tags" }}
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
{{ range . }}
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-full bg-primary-100 dark:bg-primary-500/10 text-primary-700 dark:text-primary-300 border border-primary-200 dark:border-primary-500/20">
|
||||
@@ -101,18 +243,35 @@
|
||||
{{ end }}
|
||||
|
||||
{{/* Action button */}}
|
||||
{{ with $item.button }}
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
||||
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{ $button := index $item "button" }}
|
||||
{{ if reflect.IsMap $button }}
|
||||
{{ $button_url := index $button "url" }}
|
||||
{{ $button_text := index $button "text" | default "Learn more" }}
|
||||
{{ $button_icon := index $button "icon" }}
|
||||
{{ if $button_url }}
|
||||
{{ $button_link := $button_url }}
|
||||
{{ $button_target := "" }}
|
||||
{{ $button_scheme := (urls.Parse $button_link).Scheme }}
|
||||
{{ if not $button_scheme }}
|
||||
{{ if not (hasPrefix $button_link "#") }}
|
||||
{{ $button_link = $button_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ .text | default "Learn more" }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{{ if eq (path.Ext $button_link) ".pdf" }}{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $button_scheme }}
|
||||
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
||||
<a href="{{ $button_link | safeURL }}" {{ $button_target | safeHTMLAttr }} class="inline-flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ with $button_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{ end }}
|
||||
{{ $button_text }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,9 +281,9 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $profile.education }}
|
||||
{{ if gt (len $education) 0 }}
|
||||
<div class="w-full">
|
||||
<h2 class="mb-10 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "education" }}</h2>
|
||||
<h2 class="mb-10 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "education" | default "Education" }}</h2>
|
||||
|
||||
{{/* Timeline container */}}
|
||||
<div class="relative">
|
||||
@@ -133,7 +292,7 @@
|
||||
|
||||
{{/* Education items */}}
|
||||
<div class="space-y-8">
|
||||
{{ range $idx, $item := $profile.education }}
|
||||
{{ range $idx, $item := $education }}
|
||||
{{/* Card wrapper */}}
|
||||
<div class="group relative pl-12 sm:pl-16">
|
||||
|
||||
@@ -143,8 +302,9 @@
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 w-8 h-8 bg-secondary-500/30 rounded-full blur-md group-hover:bg-secondary-500/50 transition-all duration-300"></div>
|
||||
<div class="relative w-8 h-8 bg-white dark:bg-gray-900 border-2 border-secondary-500 rounded-full flex items-center justify-center group-hover:border-secondary-400 group-hover:scale-110 transition-all duration-300">
|
||||
{{ if $item.icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class='w-4 h-4 text-secondary-600 dark:text-secondary-400'") }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ if $icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class='w-4 h-4 text-secondary-600 dark:text-secondary-400'") }}
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-4 h-4 text-secondary-600 dark:text-secondary-400"><path d="M251.76,88.94l-120-64a8,8,0,0,0-7.52,0l-120,64a8,8,0,0,0,0,14.12L32,117.87v48.42a15.91,15.91,0,0,0,4.06,10.65C49.16,191.53,78.51,216,128,216a130,130,0,0,0,48-8.76V240a8,8,0,0,0,16,0V199.51a115.63,115.63,0,0,0,27.94-22.57A15.91,15.91,0,0,0,224,166.29V117.87l27.76-14.81a8,8,0,0,0,0-14.12ZM128,200c-43.27,0-68.72-21.14-80-33.71V126.4l76.24,40.66a8,8,0,0,0,7.52,0L176,143.47v46.34C163.4,195.69,147.52,200,128,200Zm80-33.75a97.83,97.83,0,0,1-16,14.25V134.93l16-8.53ZM188,118.94l-.22-.13-56-29.87a8,8,0,0,0-7.52,14.12L171,128l-43,22.93L25,96,128,41.07,231,96Z"></path></svg>
|
||||
{{end}}
|
||||
@@ -160,11 +320,11 @@
|
||||
<div>
|
||||
{{/* Degree */}}
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-secondary-600 dark:group-hover:text-secondary-400 transition-colors">
|
||||
{{ $item.degree | default $item.area }}
|
||||
{{ index $item "degree" }}
|
||||
</h3>
|
||||
{{/* Institution */}}
|
||||
<p class="text-secondary-600 dark:text-secondary-400 font-medium mt-1">
|
||||
{{ $item.institution }}
|
||||
{{ index $item "institution" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -174,13 +334,23 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<time>
|
||||
{{ $start := or $item.start $item.date_start }}
|
||||
{{ $end := or $item.end $item.date_end }}
|
||||
{{ $start := index $item "start" }}
|
||||
{{ $end := index $item "end" }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "Jan 2006") }} –
|
||||
{{ $parsed := try (time $start) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }} –
|
||||
{{ else }}
|
||||
{{ $start }} –
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "Jan 2006") }}
|
||||
{{ $parsed := try (time $end) }}
|
||||
{{ if not $parsed.Err }}
|
||||
{{ $parsed.Value | time.Format $date_format }}
|
||||
{{ else }}
|
||||
{{ $end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<span class="text-primary-600 dark:text-primary-400 font-medium">{{ i18n "present" | default "Present" }}</span>
|
||||
{{end}}
|
||||
@@ -189,25 +359,42 @@
|
||||
</div>
|
||||
|
||||
{{/* Description */}}
|
||||
{{ with $item.summary }}
|
||||
{{ with index $item "summary" }}
|
||||
<div class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Action button */}}
|
||||
{{ with $item.button }}
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
||||
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-secondary-600 dark:hover:text-secondary-400 transition-colors">
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{ $button := index $item "button" }}
|
||||
{{ if reflect.IsMap $button }}
|
||||
{{ $button_url := index $button "url" }}
|
||||
{{ $button_text := index $button "text" | default "Learn more" }}
|
||||
{{ $button_icon := index $button "icon" }}
|
||||
{{ if $button_url }}
|
||||
{{ $button_link := $button_url }}
|
||||
{{ $button_target := "" }}
|
||||
{{ $button_scheme := (urls.Parse $button_link).Scheme }}
|
||||
{{ if not $button_scheme }}
|
||||
{{ if not (hasPrefix $button_link "#") }}
|
||||
{{ $button_link = $button_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ .text | default "Learn more" }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{{ if eq (path.Ext $button_link) ".pdf" }}{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
|
||||
{{ else if in (slice "http" "https") $button_scheme }}
|
||||
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
||||
<a href="{{ $button_link | safeURL }}" {{ $button_target | safeHTMLAttr }} class="inline-flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-secondary-600 dark:hover:text-secondary-400 transition-colors">
|
||||
{{ with $button_icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{ end }}
|
||||
{{ $button_text }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,35 +5,82 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $langs := $profile.languages }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $username }}
|
||||
|
||||
{{ $langs_raw := $profile.languages }}
|
||||
{{ $langs_iter := slice }}
|
||||
{{ if reflect.IsSlice $langs_raw }}
|
||||
{{ $langs_iter = $langs_raw }}
|
||||
{{ else if and $langs_raw (reflect.IsMap $langs_raw) }}
|
||||
{{ $langs_iter = slice $langs_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $langs := slice }}
|
||||
{{ if gt (len $langs_iter) 0 }}
|
||||
{{ range $langs_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $name := "" }}
|
||||
{{ $level_text := "" }}
|
||||
{{ $label := "" }}
|
||||
{{ $percent_raw := "" }}
|
||||
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "level" }}{{ $level_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "label" }}{{ $label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "percent" }}{{ $percent_raw = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $percent := 100.0 }}
|
||||
{{ $numeric_level := gt (len (findRE "^[0-9]+(?:\\.[0-9]+)?$" $level_text)) 0 }}
|
||||
{{ if $numeric_level }}
|
||||
{{ $percent = mul (float $level_text) 20 }}
|
||||
{{ else if $percent_raw }}
|
||||
{{ if gt (len (findRE "^[0-9]+(?:\\.[0-9]+)?$" $percent_raw)) 0 }}
|
||||
{{ $percent = float $percent_raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if gt $percent 100 }}{{ $percent = 100 }}{{ end }}
|
||||
{{ if lt $percent 0 }}{{ $percent = 0 }}{{ end }}
|
||||
|
||||
{{ if $name }}
|
||||
{{ $langs = $langs | append (dict "name" $name "level" $level_text "label" $label "percent" $percent "numeric_level" $numeric_level) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}{{ end }}
|
||||
|
||||
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
|
||||
|
||||
<div class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $block.content.title | markdownify | emojify }}
|
||||
{{ $title | markdownify | emojify }}
|
||||
</div>
|
||||
|
||||
{{ with $block.content.text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
{{ with $text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
|
||||
<div class="flex items-center flex-col lg:flex-row gap-y-10 lg:gap-y-0 lg:gap-x-8">
|
||||
{{ range $langs }}
|
||||
{{ $percent := 100.0 }}
|
||||
{{ $levelText := .level }}
|
||||
{{ $label := .label }}
|
||||
|
||||
{{ $levelStr := printf "%v" ($levelText | default "") }}
|
||||
{{ $numericLevel := gt (len (findRE "^[0-9]+(?:\\.[0-9]+)?$" $levelStr)) 0 }}
|
||||
|
||||
{{ if $numericLevel }}
|
||||
{{ $percent = mul (float $levelStr) 20 }}
|
||||
{{ else if .percent }}
|
||||
{{ $percent = float .percent }}
|
||||
{{ end }}
|
||||
|
||||
{{ if gt $percent 100 }}{{ $percent = 100 }}{{ end }}
|
||||
{{ if lt $percent 0 }}{{ $percent = 0 }}{{ end }}
|
||||
{{ $percent := index . "percent" }}
|
||||
{{ $level_text := index . "level" }}
|
||||
{{ $label := index . "label" }}
|
||||
{{ $numeric_level := index . "numeric_level" }}
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center w-28 h-28">
|
||||
@@ -46,11 +93,11 @@
|
||||
</svg>
|
||||
<span class="absolute text-3xl">{{ $percent }}%</span>
|
||||
</div>
|
||||
<span class="text-1xl">{{.name}}</span>
|
||||
<span class="text-1xl">{{ index . "name" }}</span>
|
||||
{{ if $label }}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ $label }}</span>
|
||||
{{ else if and $levelText (not $numericLevel) }}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ . }}</span>
|
||||
{{ else if and $level_text (not $numeric_level) }}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ $level_text }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -5,52 +5,128 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $skills := $profile.skills | default (slice) }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $username := "me" }}
|
||||
{{ $username_raw := index $content "username" }}
|
||||
{{ if eq (printf "%T" $username_raw) "string" }}
|
||||
{{ $username = strings.TrimSpace $username_raw }}
|
||||
{{ end }}
|
||||
{{ $profile := partial "functions/get_author_profile" $username }}
|
||||
|
||||
{{ $skills_raw := $profile.skills }}
|
||||
{{ $skills_iter := slice }}
|
||||
{{ if reflect.IsSlice $skills_raw }}
|
||||
{{ $skills_iter = $skills_raw }}
|
||||
{{ else if and $skills_raw (reflect.IsMap $skills_raw) }}
|
||||
{{ $skills_iter = slice $skills_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $skills := slice }}
|
||||
{{ if gt (len $skills_iter) 0 }}
|
||||
{{ range $skills_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $name := "" }}
|
||||
{{ $description := "" }}
|
||||
{{ $color := "" }}
|
||||
{{ $color_border := "" }}
|
||||
{{ $items_raw := index . "items" }}
|
||||
{{ $items_iter := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items_iter = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items_iter = slice $items_raw }}
|
||||
{{ end }}
|
||||
{{ $items := slice }}
|
||||
|
||||
{{ with index . "name" }}{{ $name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "description" }}{{ $description = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "color" }}{{ $color = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "color_border" }}{{ $color_border = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ if gt (len $items_iter) 0 }}
|
||||
{{ range $items_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $item_label := "" }}
|
||||
{{ $item_name := "" }}
|
||||
{{ $item_desc := "" }}
|
||||
{{ $item_icon := "" }}
|
||||
{{ $level := "" }}
|
||||
|
||||
{{ with index . "label" }}{{ $item_label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "name" }}{{ $item_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "description" }}{{ $item_desc = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $item_icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "level" }}{{ $level = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ if or $item_label $item_name }}
|
||||
{{ $items = $items | append (dict "label" $item_label "name" $item_name "description" $item_desc "icon" $item_icon "level" $level) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if and $name (gt (len $items) 0) }}
|
||||
{{ $skills = $skills | append (dict "name" $name "description" $description "color" $color "color_border" $color_border "items" $items) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}{{ end }}
|
||||
|
||||
{{ $columns := partial "functions/coerce_int" (dict "value" (index $design "columns") "default" 2 "min" 1 "max" 5) }}
|
||||
|
||||
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
|
||||
|
||||
<div class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $block.content.title | markdownify | emojify }}
|
||||
{{ $title | markdownify | emojify }}
|
||||
</div>
|
||||
|
||||
{{ with $block.content.text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
{{ with $text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
</div>
|
||||
|
||||
{{ $columns := $block.design.columns | default 2 }}
|
||||
<div class="grid grid-cols-1 {{ if ge $columns 2 }}md:grid-cols-2{{ end }} {{ if ge $columns 3 }}lg:grid-cols-3{{ end }} {{ if ge $columns 4 }}xl:grid-cols-4{{ end }} {{ if ge $columns 5 }}2xl:grid-cols-5{{ end }} items-start max-w-prose mx-auto gap-3 px-6 md:px-0">
|
||||
|
||||
{{ range $skills }}
|
||||
{{ $color := .color | default "" }}
|
||||
{{ $color_border := .color_border | default "" }}
|
||||
{{ $color := index . "color" | default "" }}
|
||||
{{ $color_border := index . "color_border" | default "" }}
|
||||
<div class="w-full">
|
||||
<div class="mb-5 text-xl font-bold text-gray-900 dark:text-white">
|
||||
{{ .name | markdownify | emojify }}
|
||||
{{ with .description }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
{{ index . "name" | markdownify | emojify }}
|
||||
{{ with index . "description" }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
</div>
|
||||
{{ range (.items | default (slice)) }}
|
||||
{{ range (index . "items") }}
|
||||
|
||||
<div class="skills-content">
|
||||
|
||||
{{ with .icon }}
|
||||
{{ with index . "icon" }}
|
||||
<span class="skills-icon inline-block">
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "style=\"height: 1em;\"") }}
|
||||
</span>
|
||||
{{ end }}
|
||||
|
||||
{{ $label := .label | default .name }}
|
||||
{{ $label := index . "label" | default (index . "name") }}
|
||||
<span class="skills-name text-gray-700 dark:text-gray-300">
|
||||
{{ $label | markdownify | emojify }}
|
||||
{{ with .description }}<p class="skills-description">{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
{{ with index . "description" }}<p class="skills-description">{{ . | markdownify | emojify }}</p>{{ end }}
|
||||
</span>
|
||||
{{/* Numeric level (1-5) only. */}}
|
||||
{{ $level := 0.0 }}
|
||||
{{ if isset . "level" }}
|
||||
{{ $raw := .level }}
|
||||
{{ $type := printf "%T" $raw }}
|
||||
{{ if or (eq $type "int") (eq $type "int64") (eq $type "int32") (eq $type "int16") (eq $type "int8") (eq $type "uint") (eq $type "uint64") (eq $type "uint32") (eq $type "uint16") (eq $type "uint8") (eq $type "float64") (eq $type "float32") }}
|
||||
{{ $level = float $raw }}
|
||||
{{ $level_raw := index . "level" }}
|
||||
{{ if $level_raw }}
|
||||
{{ $level_str := strings.TrimSpace (printf "%v" $level_raw) }}
|
||||
{{ if gt (len (findRE "^[0-9]+(?:\\.[0-9]+)?$" $level_str)) 0 }}
|
||||
{{ $level = float $level_str }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if gt $level 5 }}{{ $level = 5 }}{{ end }}
|
||||
|
||||
@@ -5,35 +5,93 @@
|
||||
{{/* Enable Alpine.js for interactivity */}}
|
||||
{{ $wcPage.Page.Store.Set "has_alpine" true }}
|
||||
|
||||
{{ $content := $wcBlock.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
|
||||
{{ $badge := index $content "badge" }}
|
||||
{{ if not (reflect.IsMap $badge) }}{{ $badge = dict }}{{ end }}
|
||||
{{ $badge_text := "" }}
|
||||
{{ with index $badge "text" }}{{ $badge_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $show_pulse := partial "functions/coerce_bool" (dict "value" (index $badge "show_pulse") "default" false) }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}{{ end }}
|
||||
{{ if $title }}
|
||||
{{ $title = replaceRE "==([^=]+)==" "<span class=\"bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent\">$1</span>" $title }}
|
||||
{{ end }}
|
||||
|
||||
{{ $subtitle := "" }}
|
||||
{{ $subtitle_raw := index $content "subtitle" }}
|
||||
{{ if $subtitle_raw }}{{ $subtitle = strings.TrimSpace (printf "%v" $subtitle_raw) }}{{ end }}
|
||||
|
||||
{{ $search_placeholder := "" }}
|
||||
{{ $placeholder_raw := index $content "search_placeholder" }}
|
||||
{{ if eq (printf "%T" $placeholder_raw) "string" }}
|
||||
{{ $search_placeholder = strings.TrimSpace $placeholder_raw }}
|
||||
{{ end }}
|
||||
{{ if not $search_placeholder }}{{ $search_placeholder = "Search for answers..." }}{{ end }}
|
||||
|
||||
{{ $suggestions_raw := index $content "suggestions" }}
|
||||
{{ $suggestions := slice }}
|
||||
{{ if reflect.IsSlice $suggestions_raw }}
|
||||
{{ range $suggestions_raw }}
|
||||
{{ $suggestion := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $suggestion }}{{ $suggestions = $suggestions | append $suggestion }}{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq (printf "%T" $suggestions_raw) "string" }}
|
||||
{{ $suggestion := strings.TrimSpace $suggestions_raw }}
|
||||
{{ if $suggestion }}{{ $suggestions = slice $suggestion }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $stats_raw := index $content "stats" }}
|
||||
{{ $stats_iter := slice }}
|
||||
{{ if reflect.IsSlice $stats_raw }}
|
||||
{{ $stats_iter = $stats_raw }}
|
||||
{{ else if and $stats_raw (reflect.IsMap $stats_raw) }}
|
||||
{{ $stats_iter = slice $stats_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $stats := slice }}
|
||||
{{ if gt (len $stats_iter) 0 }}
|
||||
{{ range $stats_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $value := "" }}
|
||||
{{ $label := "" }}
|
||||
{{ with index . "value" }}{{ $value = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "label" }}{{ $label = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if or $value $label }}
|
||||
{{ $stats = $stats | append (dict "value" $value "label" $label) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Background is now handled by parse_block_v3.html gradient_mesh system */}}
|
||||
{{/* Content only - wrapper and styling applied by framework */}}
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
|
||||
{{/* Badge */}}
|
||||
{{ with $wcBlock.content.badge }}
|
||||
{{ if $badge_text }}
|
||||
<div class="mb-8 inline-flex items-center gap-2 rounded-full bg-primary-100 dark:bg-primary-900/30 px-4 py-2 text-sm font-medium text-primary-800 dark:text-primary-200">
|
||||
{{ with .show_pulse }}
|
||||
{{ if $show_pulse }}
|
||||
<span class="flex h-2 w-2 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-primary-500"></span>
|
||||
</span>
|
||||
{{ end }}
|
||||
{{ .text | markdownify }}
|
||||
{{ $badge_text | markdownify }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Title - supports ==highlight== syntax for gradient text */}}
|
||||
{{ $title := $wcBlock.content.title }}
|
||||
{{/* Replace ==text== with gradient-highlighted spans - supports multiple highlights */}}
|
||||
{{ $title = replaceRE "==([^=]+)==" "<span class=\"bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent\">$1</span>" $title }}
|
||||
|
||||
<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl mb-6">
|
||||
{{ $title | markdownify | emojify | safeHTML }}
|
||||
</h1>
|
||||
|
||||
{{/* Subtitle */}}
|
||||
{{ with $wcBlock.content.subtitle }}
|
||||
{{ with $subtitle }}
|
||||
<p class="text-lg leading-8 text-gray-600 dark:text-gray-300 mb-12">
|
||||
{{ . | markdownify | emojify }}
|
||||
</p>
|
||||
@@ -49,7 +107,7 @@
|
||||
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-xl ring-1 ring-gray-900/5 transition-all duration-300 hover:ring-2 hover:ring-primary-500 hover:scale-105"
|
||||
:class="{ 'ring-2 ring-primary-500 scale-105': searchFocus }">
|
||||
<div class="w-full px-6 py-5 pl-14 pr-32 text-left">
|
||||
<span class="text-gray-400 dark:text-gray-500 text-lg">{{ $wcBlock.content.search_placeholder | default "Search for answers..." }}</span>
|
||||
<span class="text-gray-400 dark:text-gray-500 text-lg">{{ $search_placeholder }}</span>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-5 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -66,13 +124,13 @@
|
||||
</button>
|
||||
|
||||
{{/* Quick search suggestions - Now trigger Pagefind with pre-filled query */}}
|
||||
{{ with $wcBlock.content.suggestions }}
|
||||
{{ if gt (len $suggestions) 0 }}
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2 text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">Popular:</span>
|
||||
{{ range . }}
|
||||
{{ range $suggestions }}
|
||||
<button
|
||||
data-search-toggle
|
||||
data-search-query="{{ . }}"
|
||||
data-search-query="{{ . | htmlEscape }}"
|
||||
type="button"
|
||||
class="px-3 py-1 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors cursor-pointer">
|
||||
{{ . }}
|
||||
@@ -83,12 +141,12 @@
|
||||
</div>
|
||||
|
||||
{{/* Stats */}}
|
||||
{{ if $wcBlock.content.stats }}
|
||||
{{ if gt (len $stats) 0 }}
|
||||
<div class="mt-16 grid grid-cols-3 gap-8 text-center">
|
||||
{{ range $wcBlock.content.stats }}
|
||||
{{ range $stats }}
|
||||
<div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white">{{ .value }}</div>
|
||||
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ .label }}</div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white">{{ index . "value" }}</div>
|
||||
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ index . "label" }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,74 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $title := $block.content.title | emojify | $page.RenderString }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $layout := $block.design.layout | default "cards" }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}
|
||||
{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}
|
||||
{{ end }}
|
||||
{{ if $title }}{{ $title = $title | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}
|
||||
{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}
|
||||
{{ end }}
|
||||
{{ if $text }}{{ $text = $text | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $layout := "cards" }}
|
||||
{{ $layout_raw := index $design "layout" }}
|
||||
{{ if eq (printf "%T" $layout_raw) "string" }}
|
||||
{{ $layout_candidate := lower (strings.TrimSpace $layout_raw) }}
|
||||
{{ if in (slice "cards" "compact" "minimal") $layout_candidate }}
|
||||
{{ $layout = $layout_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $items_raw := index $content "items" }}
|
||||
{{ $items_iter := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items_iter = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items_iter = slice $items_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $items := slice }}
|
||||
{{ if gt (len $items_iter) 0 }}
|
||||
{{ range $items_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $icon := "" }}
|
||||
{{ $statistic := "" }}
|
||||
{{ $description := "" }}
|
||||
{{ $sub_metric := "" }}
|
||||
{{ with index . "icon" }}{{ $icon = printf "%v" . }}{{ end }}
|
||||
{{ with index . "statistic" }}{{ $statistic = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "description" }}{{ $description = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "sub_metric" }}{{ $sub_metric = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if or $statistic $description }}
|
||||
{{ $items = $items | append (dict "icon" $icon "statistic" $statistic "description" $description "sub_metric" $sub_metric) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $item_count := len $items }}
|
||||
{{ if lt $item_count 1 }}{{ $item_count = 1 }}{{ end }}
|
||||
{{ if gt $item_count 4 }}{{ $item_count = 4 }}{{ end }}
|
||||
{{ $grid_cols_class := "lg:grid-cols-3" }}
|
||||
{{ if eq $item_count 1 }}
|
||||
{{ $grid_cols_class = "lg:grid-cols-1" }}
|
||||
{{ else if eq $item_count 2 }}
|
||||
{{ $grid_cols_class = "lg:grid-cols-2" }}
|
||||
{{ else if eq $item_count 4 }}
|
||||
{{ $grid_cols_class = "lg:grid-cols-4" }}
|
||||
{{ end }}
|
||||
{{ $base_cols_class := "grid-cols-2" }}
|
||||
{{ if eq $item_count 1 }}{{ $base_cols_class = "grid-cols-1" }}{{ end }}
|
||||
|
||||
<div class="py-12 sm:py-16 lg:py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -30,8 +95,8 @@
|
||||
|
||||
{{/* Cards Layout */}}
|
||||
{{ if eq $layout "cards" }}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{{ len $block.content.items }} gap-6 lg:gap-8">
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 {{ $grid_cols_class }} gap-6 lg:gap-8">
|
||||
{{ range $idx, $item := $items }}
|
||||
<div class="group relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 overflow-hidden border border-gray-100 dark:border-gray-700 hover:border-primary-200 dark:hover:border-primary-800">
|
||||
|
||||
{{/* Background Gradient */}}
|
||||
@@ -39,26 +104,28 @@
|
||||
|
||||
<div class="relative p-8 text-center">
|
||||
{{/* Icon */}}
|
||||
{{ if $item.icon }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ if $icon }}
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 mb-6 bg-primary-100 dark:bg-primary-900/50 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-8 h-8 text-primary-600 dark:text-primary-400\"") }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-8 h-8 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Statistic */}}
|
||||
<div class="mb-3">
|
||||
<h3 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $item.statistic | $page.RenderString }}
|
||||
{{ $statistic := index $item "statistic" }}
|
||||
<h3 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300 counter" data-target="{{ $statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $statistic | $page.RenderString }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{/* Description */}}
|
||||
<p class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-300 leading-relaxed group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors duration-300">
|
||||
{{ $item.description | $page.RenderString }}
|
||||
{{ (index $item "description") | $page.RenderString }}
|
||||
</p>
|
||||
|
||||
{{/* Optional Sub-metric */}}
|
||||
{{ with $item.sub_metric }}
|
||||
{{ with index $item "sub_metric" }}
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ . }}
|
||||
</p>
|
||||
@@ -71,21 +138,23 @@
|
||||
{{/* Compact Layout */}}
|
||||
{{ else if eq $layout "compact" }}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{{ len $block.content.items }} divide-y sm:divide-y-0 sm:divide-x divide-gray-200 dark:divide-gray-700">
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 {{ $grid_cols_class }} divide-y sm:divide-y-0 sm:divide-x divide-gray-200 dark:divide-gray-700">
|
||||
{{ range $idx, $item := $items }}
|
||||
<div class="group p-8 text-center hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-300">
|
||||
{{ if $item.icon }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ if $icon }}
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-lg group-hover:scale-110 transition-transform duration-300">
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $item.statistic | $page.RenderString }}
|
||||
{{ $statistic := index $item "statistic" }}
|
||||
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $statistic | $page.RenderString }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{{ $item.description | $page.RenderString }}
|
||||
{{ (index $item "description") | $page.RenderString }}
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -94,21 +163,23 @@
|
||||
|
||||
{{/* Minimal Layout */}}
|
||||
{{ else }}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-{{ len $block.content.items }} gap-8 lg:gap-12">
|
||||
{{ range $item := $block.content.items }}
|
||||
<div class="grid {{ $base_cols_class }} {{ $grid_cols_class }} gap-8 lg:gap-12">
|
||||
{{ range $item := $items }}
|
||||
<div class="text-center group">
|
||||
{{ if $item.icon }}
|
||||
{{ $icon := index $item "icon" }}
|
||||
{{ if $icon }}
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-full group-hover:scale-110 transition-transform duration-300">
|
||||
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-7 h-7 text-primary-600 dark:text-primary-400\"") }}
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-7 h-7 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $item.statistic | $page.RenderString }}
|
||||
{{ $statistic := index $item "statistic" }}
|
||||
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $statistic | replaceRE `[^0-9.]` "" }}">
|
||||
{{ $statistic | $page.RenderString }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
{{ $item.description | $page.RenderString }}
|
||||
{{ (index $item "description") | $page.RenderString }}
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -267,4 +338,4 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
@@ -5,27 +5,91 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $block.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
{{/* Enable animations */}}
|
||||
{{ if ne $block.design.animations false }}
|
||||
{{ $animations := partial "functions/coerce_bool" (dict "value" (index $design "animations") "default" true) }}
|
||||
{{ if $animations }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Style */}}
|
||||
{{ $style := $block.design.style | default "grid" }}
|
||||
{{ $show_levels := $block.design.show_levels | default false }}
|
||||
{{ $style := "grid" }}
|
||||
{{ $style_raw := index $design "style" }}
|
||||
{{ if eq (printf "%T" $style_raw) "string" }}
|
||||
{{ $style_candidate := lower (strings.TrimSpace $style_raw) }}
|
||||
{{ if in (slice "grid" "list") $style_candidate }}
|
||||
{{ $style = $style_candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $show_levels := partial "functions/coerce_bool" (dict "value" (index $design "show_levels") "default" false) }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}{{ end }}
|
||||
|
||||
{{ $subtitle := "" }}
|
||||
{{ $subtitle_raw := index $content "subtitle" }}
|
||||
{{ if $subtitle_raw }}{{ $subtitle = strings.TrimSpace (printf "%v" $subtitle_raw) }}{{ end }}
|
||||
|
||||
{{ $categories_raw := index $content "categories" }}
|
||||
{{ $categories_iter := slice }}
|
||||
{{ if reflect.IsSlice $categories_raw }}
|
||||
{{ $categories_iter = $categories_raw }}
|
||||
{{ else if and $categories_raw (reflect.IsMap $categories_raw) }}
|
||||
{{ $categories_iter = slice $categories_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $categories := slice }}
|
||||
{{ if gt (len $categories_iter) 0 }}
|
||||
{{ range $categories_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $cat_name := "" }}
|
||||
{{ with index . "name" }}{{ $cat_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $items_raw := index . "items" }}
|
||||
{{ $items_iter := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items_iter = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items_iter = slice $items_raw }}
|
||||
{{ end }}
|
||||
{{ $items := slice }}
|
||||
{{ if gt (len $items_iter) 0 }}
|
||||
{{ range $items_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $item_name := "" }}
|
||||
{{ $item_icon := "" }}
|
||||
{{ $item_level := "" }}
|
||||
{{ with index . "name" }}{{ $item_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "icon" }}{{ $item_icon = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "level" }}{{ $item_level = lower (strings.TrimSpace (printf "%v" .)) }}{{ end }}
|
||||
{{ if $item_name }}
|
||||
{{ $items = $items | append (dict "name" $item_name "icon" $item_icon "level" $item_level) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if gt (len $items) 0 }}
|
||||
{{ $categories = $categories | append (dict "name" $cat_name "items" $items) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-16">
|
||||
|
||||
{{/* Header */}}
|
||||
<div class="text-center mb-12">
|
||||
{{ with $block.content.title }}
|
||||
{{ with $title }}
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
</h2>
|
||||
{{ end }}
|
||||
{{ with $block.content.subtitle }}
|
||||
{{ with $subtitle }}
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
</p>
|
||||
@@ -33,11 +97,12 @@
|
||||
</div>
|
||||
|
||||
{{/* Categories */}}
|
||||
{{ if ne $style "list" }}
|
||||
<div class="space-y-12">
|
||||
{{ range $block.content.categories }}
|
||||
{{ range $categories }}
|
||||
<div>
|
||||
{{/* Category Title */}}
|
||||
{{ with .name }}
|
||||
{{ with index . "name" }}
|
||||
<h3 class="text-xs font-semibold uppercase tracking-widest text-primary-600 dark:text-primary-400 mb-6 flex items-center justify-center gap-4">
|
||||
<span class="h-px w-12 bg-gradient-to-r from-transparent to-primary-500/50"></span>
|
||||
<span>{{ . }}</span>
|
||||
@@ -47,8 +112,8 @@
|
||||
|
||||
{{/* Tech Items - Centered flex layout */}}
|
||||
<div class="flex flex-wrap justify-center gap-4" data-stagger="50">
|
||||
{{ range $idx, $item := .items }}
|
||||
{{ $level := $item.level | default "" }}
|
||||
{{ range $idx, $item := (index . "items") }}
|
||||
{{ $level := index $item "level" | default "" }}
|
||||
{{ $level_width := "0%" }}
|
||||
{{ $level_color := "bg-gray-500" }}
|
||||
{{ if eq $level "expert" }}
|
||||
@@ -70,7 +135,7 @@
|
||||
>
|
||||
{{/* Icon */}}
|
||||
<div class="flex flex-col items-center text-center">
|
||||
{{ with $item.icon }}
|
||||
{{ with index $item "icon" }}
|
||||
{{/* Add dark-icon class for icons that need inversion in dark mode */}}
|
||||
{{ $dark_icons := slice "express" "vercel" "github" "nextjs" }}
|
||||
{{ $icon_name := . }}
|
||||
@@ -85,7 +150,7 @@
|
||||
|
||||
{{/* Name */}}
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200 group-hover:text-gray-900 dark:group-hover:text-white transition-colors">
|
||||
{{ $item.name }}
|
||||
{{ index $item "name" }}
|
||||
</span>
|
||||
|
||||
{{/* Level Indicator */}}
|
||||
@@ -107,21 +172,22 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Simple flat list style alternative */}}
|
||||
{{ if eq $style "list" }}
|
||||
<div class="flex flex-wrap justify-center gap-3 mt-8">
|
||||
{{ range $block.content.categories }}
|
||||
{{ range $i, $tech := .items }}
|
||||
{{ range $categories }}
|
||||
{{ range $i, $tech := (index . "items") }}
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-50 dark:bg-white/[0.03] backdrop-blur-sm rounded-full border border-gray-200 dark:border-white/[0.08] hover:border-primary-300 dark:hover:border-primary-500/30 hover:bg-gray-100 dark:hover:bg-white/[0.06] transition-all duration-300 scale-hover"
|
||||
>
|
||||
{{ with $tech.icon }}
|
||||
{{ with index $tech "icon" }}
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-5 h-5\"") }}
|
||||
</span>
|
||||
{{ end }}
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">{{ $tech.name }}</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">{{ index $tech "name" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -5,8 +5,49 @@
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
{{ $title := $block.content.title | emojify | $page.RenderString }}
|
||||
{{ $text := $block.content.text | emojify | $page.RenderString }}
|
||||
{{ $content := $block.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}
|
||||
{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}
|
||||
{{ end }}
|
||||
{{ if $title }}{{ $title = $title | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $text := "" }}
|
||||
{{ $text_raw := index $content "text" }}
|
||||
{{ if $text_raw }}
|
||||
{{ $text = strings.TrimSpace (printf "%v" $text_raw) }}
|
||||
{{ end }}
|
||||
{{ if $text }}{{ $text = $text | emojify | $page.RenderString }}{{ end }}
|
||||
|
||||
{{ $items_raw := index $content "items" }}
|
||||
{{ $items_iter := slice }}
|
||||
{{ if reflect.IsSlice $items_raw }}
|
||||
{{ $items_iter = $items_raw }}
|
||||
{{ else if and $items_raw (reflect.IsMap $items_raw) }}
|
||||
{{ $items_iter = slice $items_raw }}
|
||||
{{ end }}
|
||||
|
||||
{{ $items := slice }}
|
||||
{{ if gt (len $items_iter) 0 }}
|
||||
{{ range $items_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $item_text := "" }}
|
||||
{{ $item_name := "" }}
|
||||
{{ $item_role := "" }}
|
||||
{{ $item_image := "" }}
|
||||
{{ with index . "text" }}{{ $item_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "name" }}{{ $item_name = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "role" }}{{ $item_role = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "image" }}{{ $item_image = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if or $item_text $item_name $item_role $item_image }}
|
||||
{{ $items = $items | append (dict "text" $item_text "name" $item_name "role" $item_role "image" $item_image) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<section>
|
||||
<div class="max-w-screen-xl px-4 py-8 mx-auto text-center lg:py-16 lg:px-6">
|
||||
@@ -22,16 +63,20 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ range $idx, $item := $block.content.items }}
|
||||
<div class="max-w-screen-md mx-auto {{if ne (math.Add $idx 1) (len $block.content.items) }}mb-12{{end}}">
|
||||
{{ range $idx, $item := $items }}
|
||||
<div class="max-w-screen-md mx-auto {{if ne (math.Add $idx 1) (len $items) }}mb-12{{end}}">
|
||||
<svg class="h-16 mx-auto mb-3 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M9.563 8.469l-0.813-1.25c-5.625 3.781-8.75 8.375-8.75 12.156 0 3.656 2.688 5.375 4.969 5.375 2.875 0 4.906-2.438 4.906-5 0-2.156-1.375-4-3.219-4.688-0.531-0.188-1.031-0.344-1.031-1.25 0-1.156 0.844-2.875 3.938-5.344zM21.969 8.469l-0.813-1.25c-5.563 3.781-8.75 8.375-8.75 12.156 0 3.656 2.75 5.375 5.031 5.375 2.906 0 4.969-2.438 4.969-5 0-2.156-1.406-4-3.313-4.688-0.531-0.188-1-0.344-1-1.25 0-1.156 0.875-2.875 3.875-5.344z"/>
|
||||
</svg>
|
||||
<blockquote>
|
||||
<p class="text-2xl font-medium text-gray-900 dark:text-white">"{{.text | $page.RenderString}}"</p>
|
||||
<p class="text-2xl font-medium text-gray-900 dark:text-white">"{{ (index $item "text") | $page.RenderString }}"</p>
|
||||
</blockquote>
|
||||
<div class="flex items-center justify-center mt-6 space-x-3">
|
||||
{{ $image := resources.GetMatch (path.Join "media" .image) }}
|
||||
{{ $image_name := index $item "image" }}
|
||||
{{ $image_path := "" }}
|
||||
{{ if $image_name }}{{ $image_path = path.Join "media" (strings.TrimPrefix $image_name "/") }}{{ end }}
|
||||
{{ $image := "" }}
|
||||
{{ if $image_path }}{{ $image = resources.GetMatch $image_path }}{{ end }}
|
||||
{{ with $image }}
|
||||
{{/* Serve a single 2x retina source for a 48px display */}}
|
||||
{{ $avatar_96 := $image.Process "Fill 96x96 Center webp" }}
|
||||
@@ -39,12 +84,12 @@
|
||||
src="{{$avatar_96.RelPermalink}}"
|
||||
width="48"
|
||||
height="48"
|
||||
alt="{{$item.name | plainify}}"
|
||||
alt="{{ (index $item "name") | plainify }}"
|
||||
loading="lazy">
|
||||
{{ end }}
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="pr-3 font-medium text-gray-900 dark:text-white">{{.name}}</div>
|
||||
<div class="pl-3 text-sm font-light text-gray-500 dark:text-gray-400">{{.role}}</div>
|
||||
<div class="pr-3 font-medium text-gray-900 dark:text-white">{{ index $item "name" }}</div>
|
||||
<div class="pl-3 text-sm font-light text-gray-500 dark:text-gray-400">{{ index $item "role" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,29 +2,74 @@
|
||||
{{- $wcBlock := .wcBlock -}}
|
||||
{{- $wcIdentifier := .wcIdentifier -}}
|
||||
|
||||
{{- $st := $wcBlock.design.spacing | default dict -}}
|
||||
{{- $pad := $st.padding | default (slice "4rem" "0" "4rem" "0") -}}
|
||||
{{ $content := $wcBlock.content }}
|
||||
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
|
||||
{{ $design := $wcBlock.design }}
|
||||
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
|
||||
|
||||
<section id="{{ $wcIdentifier }}" class="{{ $wcBlock.design.background.color }}" style="padding: {{ delimit $pad " " }};">
|
||||
{{ $spacing := index $design "spacing" }}
|
||||
{{ if not (reflect.IsMap $spacing) }}{{ $spacing = dict }}{{ end }}
|
||||
{{ $pad_raw := index $spacing "padding" }}
|
||||
{{ $pad := slice "4rem" "0" "4rem" "0" }}
|
||||
{{ if reflect.IsSlice $pad_raw }}
|
||||
{{ $pad_values := slice }}
|
||||
{{ range $pad_raw }}
|
||||
{{ $val := strings.TrimSpace (printf "%v" .) }}
|
||||
{{ if $val }}{{ $pad_values = $pad_values | append $val }}{{ end }}
|
||||
{{ end }}
|
||||
{{ if eq (len $pad_values) 4 }}{{ $pad = $pad_values }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $background := index $design "background" }}
|
||||
{{ if not (reflect.IsMap $background) }}{{ $background = dict }}{{ end }}
|
||||
{{ $bg_color := "" }}
|
||||
{{ with index $background "color" }}{{ $bg_color = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $title := "" }}
|
||||
{{ $title_raw := index $content "title" }}
|
||||
{{ if $title_raw }}{{ $title = strings.TrimSpace (printf "%v" $title_raw) }}{{ end }}
|
||||
|
||||
{{ $subtitle := "" }}
|
||||
{{ $subtitle_raw := index $content "subtitle" }}
|
||||
{{ if $subtitle_raw }}{{ $subtitle = strings.TrimSpace (printf "%v" $subtitle_raw) }}{{ end }}
|
||||
|
||||
{{ $view_all := index $content "view_all" }}
|
||||
{{ if not (reflect.IsMap $view_all) }}{{ $view_all = dict }}{{ end }}
|
||||
{{ $view_all_text := "" }}
|
||||
{{ $view_all_link := "" }}
|
||||
{{ with index $view_all "text" }}{{ $view_all_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $view_all "link" }}{{ $view_all_link = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
<section id="{{ $wcIdentifier }}" class="{{ $bg_color }}" style="padding: {{ delimit $pad " " }};">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
{{ with $wcBlock.content.title }}
|
||||
{{ with $title }}
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{{ . | markdownify }}
|
||||
</h2>
|
||||
{{ end }}
|
||||
{{ with $wcBlock.content.subtitle }}
|
||||
{{ with $subtitle }}
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-300">
|
||||
{{ . | markdownify }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ with $wcBlock.content.view_all }}
|
||||
<a href="{{ .link }}" class="hidden sm:flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
|
||||
{{ .text }}
|
||||
{{ if and $view_all_text $view_all_link }}
|
||||
{{ $view_link := $view_all_link }}
|
||||
{{ $view_target := "" }}
|
||||
{{ $view_scheme := (urls.Parse $view_link).Scheme }}
|
||||
{{ if not $view_scheme }}
|
||||
{{ if not (hasPrefix $view_link "#") }}
|
||||
{{ $view_link = $view_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $view_scheme }}
|
||||
{{ $view_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $view_link | safeURL }}" {{ $view_target | safeHTMLAttr }} class="hidden sm:flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
|
||||
{{ $view_all_text }}
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
@@ -36,50 +81,114 @@
|
||||
{{ $questions := slice }}
|
||||
|
||||
{{/* Method 1: Dynamic - Pull from pages with trending: true */}}
|
||||
{{ $count := $wcBlock.content.count | default 4 }}
|
||||
{{ $count := partial "functions/coerce_int" (dict "value" (index $content "count") "default" 4 "min" 0) }}
|
||||
{{ $trending_pages := where site.RegularPages "Params.trending" true }}
|
||||
{{ $trending_pages = $trending_pages.ByParam "trending_weight" | default ($trending_pages.ByDate.Reverse) }}
|
||||
{{ $trending_pages = first $count $trending_pages }}
|
||||
|
||||
{{ if $trending_pages }}
|
||||
{{ range $trending_pages }}
|
||||
{{ $question_item := dict }}
|
||||
{{ $question_item = merge $question_item (dict "question" (.Params.question | default .Title)) }}
|
||||
{{ $question_item = merge $question_item (dict "link" .RelPermalink) }}
|
||||
{{ $question_item = merge $question_item (dict "answer_preview" (.Params.answer_preview | default .Params.answer | default .Summary | plainify | truncate 150)) }}
|
||||
|
||||
{{/* Optional metadata */}}
|
||||
{{ with .Params.author }}
|
||||
{{ $question_item = merge $question_item (dict "author" .) }}
|
||||
{{ end }}
|
||||
{{ with .Params.views }}
|
||||
{{ $question_item = merge $question_item (dict "views" .) }}
|
||||
{{ end }}
|
||||
{{ with .Params.upvote_count }}
|
||||
{{ $question_item = merge $question_item (dict "upvotes" .) }}
|
||||
{{ end }}
|
||||
{{ with .Params.badge }}
|
||||
{{ $question_item = merge $question_item (dict "badge" .) }}
|
||||
{{ else }}
|
||||
{{/* Default badges based on metadata */}}
|
||||
{{ if gt (.Params.upvote_count | default 0) 50 }}
|
||||
{{ $question_item = merge $question_item (dict "badge" (dict "text" "🔥 Popular" "color" "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200")) }}
|
||||
{{ else if .Params.featured }}
|
||||
{{ $question_item = merge $question_item (dict "badge" (dict "text" "⭐ Featured" "color" "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200")) }}
|
||||
{{ else if (time .Date).After (now.AddDate 0 0 -7) }}
|
||||
{{ $question_item = merge $question_item (dict "badge" (dict "text" "✨ New" "color" "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200")) }}
|
||||
{{ else }}
|
||||
{{ $question_item = merge $question_item (dict "badge" (dict "text" "🔥 Trending" "color" "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200")) }}
|
||||
{{ $question := "" }}
|
||||
{{ $question_raw := .Params.question | default .Title }}
|
||||
{{ if $question_raw }}{{ $question = strings.TrimSpace (printf "%v" $question_raw) }}{{ end }}
|
||||
{{ $link := .RelPermalink }}
|
||||
{{ $answer_preview := "" }}
|
||||
{{ $answer_raw := .Params.answer_preview | default .Params.answer | default .Summary }}
|
||||
{{ if $answer_raw }}{{ $answer_preview = (printf "%v" $answer_raw) | plainify | truncate 150 }}{{ end }}
|
||||
|
||||
{{ $author := "" }}
|
||||
{{ with .Params.author }}{{ $author = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $views := "" }}
|
||||
{{ with .Params.views }}{{ $views = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ $upvotes := "" }}
|
||||
{{ with .Params.upvote_count }}{{ $upvotes = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $badge := dict }}
|
||||
{{ $badge_raw := .Params.badge }}
|
||||
{{ if reflect.IsMap $badge_raw }}
|
||||
{{ $badge_text := "" }}
|
||||
{{ $badge_color := "" }}
|
||||
{{ with index $badge_raw "text" }}{{ $badge_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $badge_raw "color" }}{{ $badge_color = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if $badge_text }}
|
||||
{{ $badge = dict "text" $badge_text "color" $badge_color }}
|
||||
{{ end }}
|
||||
{{ else if $badge_raw }}
|
||||
{{ $badge_text := strings.TrimSpace (printf "%v" $badge_raw) }}
|
||||
{{ if $badge_text }}
|
||||
{{ $badge = dict "text" $badge_text "color" "" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $questions = $questions | append $question_item }}
|
||||
|
||||
{{ if eq (len $badge) 0 }}
|
||||
{{ $upvote_count := partial "functions/coerce_int" (dict "value" .Params.upvote_count "default" 0 "min" 0) }}
|
||||
{{ $featured := partial "functions/coerce_bool" (dict "value" .Params.featured "default" false) }}
|
||||
{{ if gt $upvote_count 50 }}
|
||||
{{ $badge = dict "text" "🔥 Popular" "color" "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200" }}
|
||||
{{ else if $featured }}
|
||||
{{ $badge = dict "text" "⭐ Featured" "color" "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200" }}
|
||||
{{ else if (time .Date).After (now.AddDate 0 0 -7) }}
|
||||
{{ $badge = dict "text" "✨ New" "color" "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200" }}
|
||||
{{ else }}
|
||||
{{ $badge = dict "text" "🔥 Trending" "color" "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $question }}
|
||||
{{ $questions = $questions | append (dict "question" $question "link" $link "answer_preview" $answer_preview "author" $author "views" $views "upvotes" $upvotes "badge" $badge) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Method 2: Static fallback - Use config if no trending pages found */}}
|
||||
{{ if not $questions }}
|
||||
{{ $questions = $wcBlock.content.questions }}
|
||||
{{ $questions_raw := index $content "questions" }}
|
||||
{{ $questions_iter := slice }}
|
||||
{{ if reflect.IsSlice $questions_raw }}
|
||||
{{ $questions_iter = $questions_raw }}
|
||||
{{ else if and $questions_raw (reflect.IsMap $questions_raw) }}
|
||||
{{ $questions_iter = slice $questions_raw }}
|
||||
{{ end }}
|
||||
{{ if gt (len $questions_iter) 0 }}
|
||||
{{ range $questions_iter }}
|
||||
{{ if reflect.IsMap . }}
|
||||
{{ $question := "" }}
|
||||
{{ $link := "" }}
|
||||
{{ $answer_preview := "" }}
|
||||
{{ $author := "" }}
|
||||
{{ $views := "" }}
|
||||
{{ $upvotes := "" }}
|
||||
|
||||
{{ with index . "question" }}{{ $question = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "link" }}{{ $link = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "answer_preview" }}{{ $answer_preview = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "author" }}{{ $author = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "views" }}{{ $views = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index . "upvotes" }}{{ $upvotes = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
|
||||
{{ $badge := dict }}
|
||||
{{ $badge_raw := index . "badge" }}
|
||||
{{ if reflect.IsMap $badge_raw }}
|
||||
{{ $badge_text := "" }}
|
||||
{{ $badge_color := "" }}
|
||||
{{ with index $badge_raw "text" }}{{ $badge_text = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ with index $badge_raw "color" }}{{ $badge_color = strings.TrimSpace (printf "%v" .) }}{{ end }}
|
||||
{{ if $badge_text }}
|
||||
{{ $badge = dict "text" $badge_text "color" $badge_color }}
|
||||
{{ end }}
|
||||
{{ else if $badge_raw }}
|
||||
{{ $badge_text := strings.TrimSpace (printf "%v" $badge_raw) }}
|
||||
{{ if $badge_text }}
|
||||
{{ $badge = dict "text" $badge_text "color" "" }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $question }}
|
||||
{{ $questions = $questions | append (dict "question" $question "link" $link "answer_preview" $answer_preview "author" $author "views" $views "upvotes" $upvotes "badge" $badge) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Questions list or carousel */}}
|
||||
@@ -88,21 +197,38 @@
|
||||
<div class="group relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 transition-all duration-300 hover:border-primary-500 hover:shadow-lg">
|
||||
|
||||
{{/* Badge */}}
|
||||
{{ with .badge }}
|
||||
<div class="mb-3 inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {{ .color | default "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200" }}">
|
||||
{{ .text }}
|
||||
</div>
|
||||
{{ $badge := index $question "badge" }}
|
||||
{{ if reflect.IsMap $badge }}
|
||||
{{ $badge_text := index $badge "text" }}
|
||||
{{ $badge_color := index $badge "color" | default "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200" }}
|
||||
{{ if $badge_text }}
|
||||
<div class="mb-3 inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {{ $badge_color }}">
|
||||
{{ $badge_text }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Question */}}
|
||||
<h3 class="mb-3 text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
<a href="{{ .link }}" class="after:absolute after:inset-0">
|
||||
{{ .question }}
|
||||
{{ $question_link := index $question "link" }}
|
||||
{{ $link := $question_link }}
|
||||
{{ if not $link }}{{ $link = "#" }}{{ end }}
|
||||
{{ $link_target := "" }}
|
||||
{{ $scheme := (urls.Parse $link).Scheme }}
|
||||
{{ if not $scheme }}
|
||||
{{ if not (hasPrefix $link "#") }}
|
||||
{{ $link = $link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $scheme }}
|
||||
{{ $link_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<a href="{{ $link | safeURL }}" {{ $link_target | safeHTMLAttr }} class="after:absolute after:inset-0">
|
||||
{{ index $question "question" }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{{/* Answer preview */}}
|
||||
{{ with .answer_preview }}
|
||||
{{ with index $question "answer_preview" }}
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
{{ . }}
|
||||
</p>
|
||||
@@ -111,7 +237,7 @@
|
||||
{{/* Meta info */}}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-4 text-gray-500 dark:text-gray-400">
|
||||
{{ with .author }}
|
||||
{{ with index $question "author" }}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
@@ -120,7 +246,7 @@
|
||||
</span>
|
||||
{{ end }}
|
||||
|
||||
{{ with .views }}
|
||||
{{ with index $question "views" }}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
@@ -130,7 +256,7 @@
|
||||
</span>
|
||||
{{ end }}
|
||||
|
||||
{{ with .upvotes }}
|
||||
{{ with index $question "upvotes" }}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
@@ -150,10 +276,20 @@
|
||||
</div>
|
||||
|
||||
{{/* Mobile view all link */}}
|
||||
{{ with $wcBlock.content.view_all }}
|
||||
{{ if and $view_all_text $view_all_link }}
|
||||
{{ $mobile_link := $view_all_link }}
|
||||
{{ $mobile_target := "" }}
|
||||
{{ $mobile_scheme := (urls.Parse $mobile_link).Scheme }}
|
||||
{{ if not $mobile_scheme }}
|
||||
{{ if not (hasPrefix $mobile_link "#") }}
|
||||
{{ $mobile_link = $mobile_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $mobile_scheme }}
|
||||
{{ $mobile_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
<div class="mt-6 sm:hidden">
|
||||
<a href="{{ .link }}" class="flex items-center justify-center gap-2 rounded-lg bg-primary-600 px-4 py-2 text-white font-medium hover:bg-primary-700 transition-colors">
|
||||
{{ .text }}
|
||||
<a href="{{ $mobile_link | safeURL }}" {{ $mobile_target | safeHTMLAttr }} class="flex items-center justify-center gap-2 rounded-lg bg-primary-600 px-4 py-2 text-white font-medium hover:bg-primary-700 transition-colors">
|
||||
{{ $view_all_text }}
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
@@ -162,4 +298,4 @@
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
Reference in new issue
Block a user