{{/* Hugo Blox: Portfolio */}} {{/* Documentation: https://hugoblox.com/blocks/ */}} {{/* License: https://github.com/HugoBlox/kit/blob/main/LICENSE.md */}} {{/* Initialise */}} {{ $page := .wcPage }} {{ $block := .wcBlock }} {{/* Enable Alpine.js and animations for this block */}} {{ $page.Store.Set "has_alpine" true }} {{ if ne $block.design.animations false }} {{ $page.Store.Set "has_animations" true }} {{ end }} {{/* Query projects */}} {{ $query := site.Pages }} {{ $folders := $block.content.filters.folders | default (slice "projects") }} {{ $query = where $query "Section" "in" $folders }} {{ $query = where $query "Kind" "eq" "page" }} {{/* Get archive page for "View All" link */}} {{ $archive_page := "" }} {{ $main_folder := index $folders 0 }} {{ $archive_page = site.GetPage "Section" $main_folder }} {{/* Sort */}} {{ $sort_by := $block.content.sort_by | default "Date" }} {{ $sort_by = partial "functions/get_sort_by_parameter" $sort_by }} {{ $sort_ascending := $block.content.sort_ascending | default false }} {{ $sort_order := cond $sort_ascending "asc" "desc" }} {{ $query = sort $query $sort_by $sort_order }} {{/* Store total count before limiting */}} {{ $total_count := len $query }} {{/* Limit */}} {{ $count := $block.content.count | default 6 }} {{ 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 }} {{ $default_tag := "*" }} {{ with index $buttons $default_idx }} {{ $default_tag = .tag }} {{ end }} {{/* Columns */}} {{ $columns := $block.design.columns | default 3 }} {{ $grid_class := "md:grid-cols-2 lg:grid-cols-3" }} {{ if eq $columns 2 }} {{ $grid_class = "md:grid-cols-2" }} {{ else if eq $columns 4 }} {{ $grid_class = "md:grid-cols-2 lg:grid-cols-4" }} {{ end }}
{{/* Header */}}
{{ with $block.content.title }}

{{ . | emojify | $page.RenderString }}

{{ end }} {{ with $block.content.subtitle }}

{{ . | emojify | $page.RenderString }}

{{ end }}
{{/* Filter Buttons */}} {{ if gt (len $buttons) 1 }}
{{ range $idx, $button := $buttons }} {{ end }}
{{ end }} {{/* Project Grid with staggered reveal animation */}}
{{ range $index, $item := $query }} {{/* Get tags as comma-separated string */}} {{ $item_tags := "" }} {{ with $item.Params.tags }} {{ $item_tags = delimit . "," }} {{ end }} {{/* Get status */}} {{ $status := $item.Params.status | default "" }} {{ $status_class := "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border-emerald-500/30" }} {{ if eq $status "WIP" }} {{ $status_class = "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30" }} {{ else if eq $status "Archived" }} {{ $status_class = "bg-gray-500/20 text-gray-600 dark:text-gray-400 border-gray-500/30" }} {{ end }}
{{/* Card Header / Image */}}
{{ $image := $item.Resources.GetMatch "{featured,cover,thumbnail}*" }} {{ if $image }} {{ if strings.Contains $image.MediaType.SubType "svg" }} {{ $item.Title }} {{ else }} {{ $img := $image.Fill "600x400 webp" }} {{ $item.Title }} {{ end }} {{ else }} {{/* Gradient placeholder with configurable icon */}} {{ $fallback_icon := $block.design.fallback_icon | default "code-bracket" }}
{{ partial "functions/get_icon" (dict "name" $fallback_icon "attributes" "class=\"w-16 h-16 text-white/30\"") }}
{{ end }} {{/* Status Badge */}} {{ with $status }}
{{ . }}
{{ end }} {{/* Gradient Overlay */}}
{{/* Card Content - use flex-col to push links to bottom */}}
{{/* Title */}}

{{ $item.Title }}

{{/* Summary */}} {{ with $item.Params.summary }}

{{ . }}

{{ else }}

{{ $item.Summary | truncate 100 }}

{{ end }} {{/* Tech Stack Tags - fixed 2-line height for consistency */}} {{ with $item.Params.tech_stack }}
{{ range first 4 . }} {{ . }} {{ end }} {{ if gt (len .) 4 }} +{{ sub (len .) 4 }} {{ end }}
{{ end }} {{/* Action Links - pushed to bottom with mt-auto */}} {{ with $item.Params.links }}
{{ 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" }} {{ 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" }} {{ end }} {{ end }} {{ $icon = .icon | default $icon }} {{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-4 h-4\"") }} {{ $label }} {{ end }}
{{ end }}
{{ end }}
{{/* 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_link := "" }} {{ if $block.content.archive.link }} {{ $archive_link = $block.content.archive.link | relLangURL }} {{ else }} {{ $archive_link = $archive_page.RelPermalink }} {{ end }} {{ $archive_text := $block.content.archive.text | default (i18n "portfolio_view_all" | default "View All Projects") }}
{{ $archive_text | emojify }} {{ partial "functions/get_icon" (dict "name" "arrow-right" "attributes" "class=\"w-4 h-4\"") }}
{{ end }}