mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-14 04:15:40 +00:00
feat: add dev portfolio template and add dev-hero, portfolio, tech-stack blocks
Also, fix card view to use theme colors rather than hard-coded colors
This commit is contained in:
1 parent
30dd554e8d
commit
3526fb1ca1
68 files changed
+6664
-152
No files matched your search
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Hugo Blox Animations
|
||||
* Scroll-triggered reveals and micro-interactions
|
||||
*/
|
||||
|
||||
/* Typewriter cursor */
|
||||
.typewriter-cursor {
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
margin-left: 2px;
|
||||
background-color: currentColor;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reveal animations base */
|
||||
[x-reveal] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Stagger container */
|
||||
[data-stagger] > [data-stagger-item] {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition:
|
||||
opacity 0.5s ease-out,
|
||||
transform 0.5s ease-out;
|
||||
}
|
||||
|
||||
[data-stagger].revealed > [data-stagger-item] {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Hover enhancements for cards */
|
||||
.card-hover-lift {
|
||||
transition:
|
||||
transform 0.3s ease-out,
|
||||
box-shadow 0.3s ease-out;
|
||||
}
|
||||
|
||||
.card-hover-lift:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px -12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Hover glow effect */
|
||||
.hover-glow {
|
||||
position: relative;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.hover-glow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(135deg, var(--color-primary-500), var(--color-secondary-500));
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
transition: opacity 0.3s ease-out;
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
.hover-glow:hover::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Icon bounce on hover */
|
||||
.icon-bounce:hover {
|
||||
animation: iconBounce 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes iconBounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scale up on hover */
|
||||
.scale-hover {
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.scale-hover:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Button pulse */
|
||||
.btn-pulse {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-pulse::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: 0 0 0 0 currentColor;
|
||||
opacity: 0.4;
|
||||
animation: pulse-ring 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 currentColor;
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 12px currentColor;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Float animation for background orbs */
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-20px) translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-30px) translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Slow rotate for decorative elements */
|
||||
@keyframes rotate-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-rotate-slow {
|
||||
animation: rotate-slow 30s linear infinite;
|
||||
}
|
||||
|
||||
/* Fade in up - for scroll reveals */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Scale in - for icons/badges */
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Slide in from left */
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms;
|
||||
animation-iteration-count: 1;
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
|
||||
.typewriter-cursor {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[x-reveal],
|
||||
[data-stagger] > [data-stagger-item] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.dark .hover-glow::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dark .hover-glow:hover::before {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Dark mode icon visibility fix */
|
||||
/* For icons that are dark/black and invisible on dark backgrounds */
|
||||
.dark .dark-mode-icon svg,
|
||||
.dark .dark-mode-icon img {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
/* Alternative: Add subtle background to icon containers */
|
||||
.dark .dark-mode-icon {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
/* Hover state restores some contrast */
|
||||
.dark .group:hover .dark-mode-icon svg,
|
||||
.dark .group:hover .dark-mode-icon img {
|
||||
filter: invert(1) brightness(1.5);
|
||||
}
|
||||
@@ -41,5 +41,8 @@
|
||||
/* Hugo Blox Search */
|
||||
@import "hb-search.css";
|
||||
|
||||
/* Hugo Blox Animations (typewriter, scroll reveals) */
|
||||
@import "animations.css";
|
||||
|
||||
/* Safelist for dynamic classes */
|
||||
@import "config/safelist.css";
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Hugo Blox Animations
|
||||
* Typewriter effect and scroll-triggered animations using Alpine.js
|
||||
*/
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
// Typewriter effect component
|
||||
Alpine.data("typewriter", (strings = [], options = {}) => ({
|
||||
strings: strings,
|
||||
currentIndex: 0,
|
||||
currentText: "",
|
||||
isDeleting: false,
|
||||
typeSpeed: options.typeSpeed || 80,
|
||||
deleteSpeed: options.deleteSpeed || 50,
|
||||
pauseTime: options.pauseTime || 2000,
|
||||
loop: options.loop !== false,
|
||||
showCursor: options.cursor !== false,
|
||||
|
||||
init() {
|
||||
if (this.strings.length === 0) return;
|
||||
this.type();
|
||||
},
|
||||
|
||||
type() {
|
||||
const current = this.strings[this.currentIndex];
|
||||
|
||||
if (this.isDeleting) {
|
||||
this.currentText = current.substring(0, this.currentText.length - 1);
|
||||
} else {
|
||||
this.currentText = current.substring(0, this.currentText.length + 1);
|
||||
}
|
||||
|
||||
let delay = this.isDeleting ? this.deleteSpeed : this.typeSpeed;
|
||||
|
||||
if (!this.isDeleting && this.currentText === current) {
|
||||
// Finished typing, pause then delete
|
||||
delay = this.pauseTime;
|
||||
this.isDeleting = true;
|
||||
} else if (this.isDeleting && this.currentText === "") {
|
||||
// Finished deleting, move to next string
|
||||
this.isDeleting = false;
|
||||
this.currentIndex = (this.currentIndex + 1) % this.strings.length;
|
||||
|
||||
if (!this.loop && this.currentIndex === 0) {
|
||||
// Don't loop, show first string permanently
|
||||
this.currentText = this.strings[0];
|
||||
return;
|
||||
}
|
||||
delay = 500; // Pause before typing next
|
||||
}
|
||||
|
||||
setTimeout(() => this.type(), delay);
|
||||
},
|
||||
}));
|
||||
|
||||
// Scroll reveal directive
|
||||
Alpine.directive("reveal", (el, {expression, modifiers}, {evaluate}) => {
|
||||
const options = expression ? evaluate(expression) : {};
|
||||
const delay = options.delay || 0;
|
||||
const duration = options.duration || 600;
|
||||
const distance = options.distance || "20px";
|
||||
const direction = modifiers.includes("left") ? "left" : modifiers.includes("right") ? "right" : modifiers.includes("down") ? "down" : "up";
|
||||
|
||||
// Set initial state
|
||||
el.style.opacity = "0";
|
||||
el.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
|
||||
el.style.transitionDelay = `${delay}ms`;
|
||||
|
||||
const transforms = {
|
||||
up: `translateY(${distance})`,
|
||||
down: `translateY(-${distance})`,
|
||||
left: `translateX(${distance})`,
|
||||
right: `translateX(-${distance})`,
|
||||
};
|
||||
el.style.transform = transforms[direction];
|
||||
|
||||
// Observe intersection
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
el.style.opacity = "1";
|
||||
el.style.transform = "translate(0, 0)";
|
||||
observer.unobserve(el);
|
||||
}
|
||||
});
|
||||
},
|
||||
{threshold: 0.1, rootMargin: "0px 0px -50px 0px"},
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
});
|
||||
});
|
||||
|
||||
// Staggered reveal for groups of elements
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Add stagger delays to elements with data-stagger attribute
|
||||
document.querySelectorAll("[data-stagger]").forEach((container) => {
|
||||
const children = container.querySelectorAll("[data-stagger-item]");
|
||||
const baseDelay = parseInt(container.dataset.stagger, 10) || 100;
|
||||
|
||||
children.forEach((child, index) => {
|
||||
child.style.transitionDelay = `${index * baseDelay}ms`;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Respect reduced motion preference
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
document.documentElement.classList.add("reduce-motion");
|
||||
}
|
||||
@@ -6,9 +6,37 @@
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{/* 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 }}
|
||||
|
||||
{{/* Check if Visit Us card has content */}}
|
||||
{{ $has_visit_content := or $block.content.address $block.content.office_hours $block.content.map_url }}
|
||||
|
||||
{{/* Check if Connect card has content - fallback to author profile if no email/social */}}
|
||||
{{ $author := $block.content.username | default "me" }}
|
||||
{{ $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 }}
|
||||
|
||||
{{/* Determine grid layout - single column centered if only one card */}}
|
||||
{{ $grid_class := "grid-cols-1 lg:grid-cols-2" }}
|
||||
{{ $card_max_width := "" }}
|
||||
{{ $is_single_card := false }}
|
||||
{{ if and $has_visit_content (not $has_connect_content) }}
|
||||
{{ $grid_class = "grid-cols-1" }}
|
||||
{{ $card_max_width = "max-w-3xl mx-auto" }}
|
||||
{{ $is_single_card = true }}
|
||||
{{ else if and $has_connect_content (not $has_visit_content) }}
|
||||
{{ $grid_class = "grid-cols-1" }}
|
||||
{{ $card_max_width = "max-w-3xl mx-auto" }}
|
||||
{{ $is_single_card = true }}
|
||||
{{ end }}
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16 lg:py-20">
|
||||
|
||||
{{/* Section Header */}}
|
||||
@@ -27,12 +55,13 @@
|
||||
</div>
|
||||
|
||||
{{/* Contact Cards Grid */}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
|
||||
<div class="grid {{ $grid_class }} gap-8 lg:gap-12">
|
||||
|
||||
{{/* Visit Us Card */}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300">
|
||||
{{/* Visit Us Card - Only show if has content */}}
|
||||
{{ if $has_visit_content }}
|
||||
<div class="{{ $card_max_width }} bg-white dark:bg-white/[0.03] backdrop-blur-xl rounded-2xl border border-gray-200 dark:border-white/[0.08] p-8 hover:bg-gray-50 dark:hover:bg-white/[0.06] hover:border-primary-300 dark:hover:border-primary-500/30 transition-all duration-300">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-500/10 rounded-full flex items-center justify-center border border-primary-200 dark:border-primary-500/20">
|
||||
{{ 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">
|
||||
@@ -50,7 +79,7 @@
|
||||
{{ end }}
|
||||
|
||||
{{ with $block.content.office_hours }}
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<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 . }}
|
||||
@@ -62,18 +91,116 @@
|
||||
|
||||
{{ with $block.content.map_url }}
|
||||
<a href="{{ . }}" target="_blank" rel="noopener"
|
||||
class="inline-flex items-center mt-4 px-4 py-2 bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/50 transition-colors">
|
||||
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" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Connect Card */}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300">
|
||||
{{/* Connect Card - Only show if has content */}}
|
||||
{{ if $has_connect_content }}
|
||||
<div class="{{ $card_max_width }} bg-white dark:bg-white/[0.03] backdrop-blur-xl rounded-2xl border border-gray-200 dark:border-white/[0.08] p-8 lg:p-10 hover:bg-gray-50 dark:hover:bg-white/[0.06] hover:border-primary-300 dark:hover:border-primary-500/30 transition-all duration-300">
|
||||
|
||||
{{/* Filter social links - remove mailto if email is shown separately */}}
|
||||
{{ $filtered_social := slice }}
|
||||
{{ range $social }}
|
||||
{{ if not (hasPrefix .url "mailto:") }}
|
||||
{{ $filtered_social = $filtered_social | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $is_single_card }}
|
||||
{{/* Single card layout - horizontal on desktop */}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12" x-data="{ copied: false }">
|
||||
|
||||
{{/* Left: Email & CTA */}}
|
||||
<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") }}
|
||||
</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." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{ with $email }}
|
||||
{{/* Email with click-to-copy */}}
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="navigator.clipboard.writeText('{{ . }}'); 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">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-300 transition-colors">Click to copy</p>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">{{ . }}</p>
|
||||
</div>
|
||||
</button>
|
||||
{{/* Toast notification */}}
|
||||
<div
|
||||
x-show="copied"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-1"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 translate-y-1"
|
||||
class="absolute left-0 -bottom-10 px-3 py-1.5 bg-emerald-500/90 text-white text-sm font-medium rounded-lg shadow-lg"
|
||||
>
|
||||
✓ Copied to clipboard!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Primary CTA Button */}}
|
||||
<a href="mailto:{{ . }}"
|
||||
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
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with $phone }}
|
||||
<a href="tel:{{ . }}" 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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Call me</p>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">{{ . }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Right: Social Links */}}
|
||||
{{ with $filtered_social }}
|
||||
<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"
|
||||
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 }}>
|
||||
<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\"") }}
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ else }}
|
||||
{{/* Standard card layout (when shown alongside Visit Us) */}}
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-500/10 rounded-full flex items-center justify-center border border-primary-200 dark:border-primary-500/20">
|
||||
{{ 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">
|
||||
@@ -84,32 +211,39 @@
|
||||
<div class="space-y-4">
|
||||
{{/* Contact Methods */}}
|
||||
<div class="space-y-3">
|
||||
{{ with $block.content.email }}
|
||||
<div class="flex items-center">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }}
|
||||
<a href="mailto:{{ . }}" class="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ . }}
|
||||
</a>
|
||||
</div>
|
||||
{{ 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">
|
||||
<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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Email</p>
|
||||
<p class="text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">{{ . }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with $block.content.phone }}
|
||||
<div class="flex items-center">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }}
|
||||
<a href="tel:{{ . }}" class="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
||||
{{ . }}
|
||||
</a>
|
||||
</div>
|
||||
{{ 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">
|
||||
<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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Phone</p>
|
||||
<p class="text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">{{ . }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{/* Social Media */}}
|
||||
{{ with $block.content.social }}
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">{{ i18n "block_contact_follow_us" | default "Follow Us" }}</h4>
|
||||
<div class="flex gap-3">
|
||||
{{ with $filtered_social }}
|
||||
<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"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-primary-100 dark:hover:bg-primary-900/50 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
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\"") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
@@ -117,28 +251,30 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Prospective Members */}}
|
||||
{{ with $block.content.prospective }}
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<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 }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg shadow hover:shadow-lg transition-all">
|
||||
{{ partial "functions/get_icon" (dict "name" "hero/user-plus" "attributes" "class=\"w-5 h-5 mr-2\"") }}
|
||||
{{ .text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{/* Prospective Members - shown in both layouts */}}
|
||||
{{ with $block.content.prospective }}
|
||||
<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 }}"
|
||||
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 }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Optional Map Embed */}}
|
||||
{{ with $block.content.map_embed }}
|
||||
<div class="mt-12 rounded-2xl overflow-hidden shadow-xl">
|
||||
<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 }}
|
||||
</div>
|
||||
@@ -148,33 +284,33 @@
|
||||
{{/* Contact Form (optional) */}}
|
||||
{{ if $block.content.show_form }}
|
||||
<div class="mt-12 max-w-2xl mx-auto">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
|
||||
<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">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ i18n "block_contact_form_name" | default "Name" }}</label>
|
||||
<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>
|
||||
<input type="text" id="name" name="name" required
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||
class="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500/50 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ i18n "block_contact_form_email" | default "Email" }}</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||
<label for="form-email" class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">{{ i18n "block_contact_form_email" | default "Email" }}</label>
|
||||
<input type="email" id="form-email" name="email" required
|
||||
class="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500/50 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ i18n "block_contact_form_subject" | default "Subject" }}</label>
|
||||
<label for="subject" class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">{{ i18n "block_contact_form_subject" | default "Subject" }}</label>
|
||||
<input type="text" id="subject" name="subject" required
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||
class="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500/50 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ i18n "block_contact_form_message" | default "Message" }}</label>
|
||||
<label for="message" class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">{{ i18n "block_contact_form_message" | default "Message" }}</label>
|
||||
<textarea id="message" name="message" rows="5" required
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"></textarea>
|
||||
class="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500/50 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-colors resize-none"></textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow hover:shadow-lg transition-all">
|
||||
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-500 text-white font-medium rounded-lg shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/30 transition-all">
|
||||
{{ i18n "block_contact_form_submit" | default "Send Message" }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
{{ $button_classes := "" }}
|
||||
{{ if eq $text_color_mode "light" }}
|
||||
{{ $text_classes = "text-white" }}
|
||||
{{ $button_classes = "text-gray-900" }}
|
||||
{{ $button_classes = "text-gray-900 dark:text-white" }}
|
||||
{{ else if eq $text_color_mode "dark" }}
|
||||
{{ $text_classes = "text-gray-900" }}
|
||||
{{ $button_classes = "text-gray-900" }}
|
||||
{{ $text_classes = "text-gray-900 dark:text-white" }}
|
||||
{{ $button_classes = "text-gray-900 dark:text-white" }}
|
||||
{{ else }}
|
||||
{{/* Auto mode - detect based on gradient colors */}}
|
||||
{{ $is_dark_bg := false }}
|
||||
@@ -67,6 +67,7 @@
|
||||
{{ $button_link := .url }}
|
||||
{{ $button_scheme := (urls.Parse $button_link).Scheme }}
|
||||
{{ $button_target := "" }}
|
||||
{{ $new_tab := .new_tab | default false }}
|
||||
{{ if not $button_scheme }}
|
||||
{{/* Check if it's a hash fragment (page anchor) */}}
|
||||
{{ if hasPrefix $button_link "#" }}
|
||||
@@ -77,6 +78,10 @@
|
||||
{{ $button_link = $button_link | relLangURL }}
|
||||
{{ end }}
|
||||
{{ else if in (slice "http" "https") $button_scheme }}
|
||||
{{ $new_tab = true }}
|
||||
{{ end }}
|
||||
{{/* Apply new tab if requested or external link */}}
|
||||
{{ if $new_tab }}
|
||||
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Dev Hero Block
|
||||
|
||||
A modern, developer-focused hero section optimized for dark mode with gradient effects, animated backgrounds, and flexible layouts.
|
||||
|
||||
## Features
|
||||
|
||||
- **Gradient Text**: Name displayed with beautiful gradient styling
|
||||
- **Animated Background**: Floating orbs with subtle grid pattern
|
||||
- **Social Links**: Automatic icon detection for common platforms
|
||||
- **CTA Buttons**: Primary and secondary button styling
|
||||
- **Two Layouts**: Centered or split (avatar on side) layouts
|
||||
- **Author Integration**: Pulls data from author profiles
|
||||
- **Emoji Status Badge**: Optional personalized emoji on avatar (💻 developer, 🎨 designer, 🚀 startup, etc.)
|
||||
- **Scroll Indicator**: Optional animated scroll prompt
|
||||
|
||||
## Usage
|
||||
|
||||
### Centered Layout (Default)
|
||||
|
||||
```yaml
|
||||
- block: dev-hero
|
||||
id: hero
|
||||
content:
|
||||
username: me
|
||||
greeting: "Hi, I'm"
|
||||
show_status: true
|
||||
show_scroll_indicator: true
|
||||
cta_buttons:
|
||||
- text: View My Work
|
||||
url: "#projects"
|
||||
icon: arrow-down
|
||||
- text: Get In Touch
|
||||
url: "#contact"
|
||||
icon: envelope
|
||||
design:
|
||||
style: centered
|
||||
avatar_shape: circle
|
||||
background:
|
||||
color:
|
||||
dark: "#0a0a0f"
|
||||
```
|
||||
|
||||
### Split Layout
|
||||
|
||||
```yaml
|
||||
- block: dev-hero
|
||||
id: hero
|
||||
content:
|
||||
username: me
|
||||
greeting: "Hello, I'm"
|
||||
cta_buttons:
|
||||
- text: View Projects
|
||||
url: "#projects"
|
||||
- text: Download Resume
|
||||
url: "/resume.pdf"
|
||||
icon: document-arrow-down
|
||||
design:
|
||||
style: split
|
||||
avatar_shape: rounded
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Content
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `username` | string | `me` | Author folder to pull data from |
|
||||
| `title` | string | - | Override author title |
|
||||
| `tagline` | string | - | Override author role |
|
||||
| `bio` | string | - | Override author bio |
|
||||
| `greeting` | string | - | Text above the name |
|
||||
| `name_prefix` | string | - | Prefix before name (styled differently) |
|
||||
| `show_status` | boolean | `false` | Show emoji status badge (uses author's `status.icon`) |
|
||||
| `show_scroll_indicator` | boolean | `false` | Show scroll prompt |
|
||||
| `social` | array | - | Override author social links |
|
||||
| `cta_buttons` | array | - | Call-to-action buttons |
|
||||
|
||||
### Design
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `style` | string | `centered` | Layout style (`centered` or `split`) |
|
||||
| `avatar_shape` | string | `circle` | Avatar shape (`circle` or `rounded`) |
|
||||
|
||||
## Social Networks
|
||||
|
||||
The block automatically detects icons for these networks:
|
||||
- `github` → GitHub logo
|
||||
- `linkedin` → LinkedIn logo
|
||||
- `twitter` → X (Twitter) logo
|
||||
- `email` → Envelope icon
|
||||
|
||||
Other networks will use a generic link icon unless you specify a custom `icon`.
|
||||
|
||||
## CTA Buttons
|
||||
|
||||
The first button is styled as primary (filled), subsequent buttons are secondary (outlined).
|
||||
|
||||
```yaml
|
||||
cta_buttons:
|
||||
- text: Primary Button
|
||||
url: "#action"
|
||||
icon: arrow-right # optional
|
||||
- text: Secondary Button
|
||||
url: "#other"
|
||||
```
|
||||
|
||||
## Author Data
|
||||
|
||||
The block automatically pulls from `/data/authors/{username}.yaml`:
|
||||
- `name.display` → Name
|
||||
- `role` → Tagline/role
|
||||
- `bio` → Short biography
|
||||
- `links` → Social links
|
||||
- `avatar` → Profile image
|
||||
- `status.icon` → Emoji badge (shown when `show_status: true`)
|
||||
|
||||
**Status Emoji Examples:**
|
||||
```yaml
|
||||
# In data/authors/me.yaml
|
||||
status:
|
||||
icon: "💻" # Developer
|
||||
# Or: 🎨 Designer, 🚀 Entrepreneur, 🔬 Researcher, 🎓 Student
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The block includes:
|
||||
- Animated gradient orbs in the background
|
||||
- Subtle grid pattern overlay
|
||||
- Gradient text effect on the name
|
||||
- Glass morphism social buttons
|
||||
- Primary/secondary CTA button styles
|
||||
@@ -0,0 +1,278 @@
|
||||
{{/* Hugo Blox: Dev Hero */}}
|
||||
{{/* Documentation: https://hugoblox.com/blocks/ */}}
|
||||
{{/* License: https://github.com/HugoBlox/kit/blob/main/LICENSE.md */}}
|
||||
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{/* Enable animations if typewriter or scroll animations are configured */}}
|
||||
{{ $typewriter := $block.content.typewriter | default dict }}
|
||||
{{ $has_animations := or $typewriter.enable $block.design.animations }}
|
||||
{{ 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" }}
|
||||
{{ $author := partial "functions/get_author_profile" $username }}
|
||||
|
||||
{{/* Get avatar */}}
|
||||
{{ $avatar := "" }}
|
||||
{{ $avatar_shape := $block.design.avatar_shape | default "circle" }}
|
||||
{{ with $author.avatar }}
|
||||
{{ $avatar = .Fill "300x300 webp Center" }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Layout style */}}
|
||||
{{ $style := $block.design.style | default "centered" }}
|
||||
|
||||
<div class="relative min-h-[80vh] flex items-center justify-center overflow-hidden">
|
||||
|
||||
{{/* Background Effects - only visible in dark mode */}}
|
||||
<div class="absolute inset-0 pointer-events-none dark:block hidden">
|
||||
{{/* Gradient orbs */}}
|
||||
<div class="absolute top-1/4 -left-20 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div class="absolute bottom-1/4 -right-20 w-80 h-80 bg-secondary-500/20 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[40rem] h-[40rem] bg-primary-600/5 rounded-full blur-3xl"></div>
|
||||
|
||||
{{/* Grid pattern */}}
|
||||
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:64px_64px]"></div>
|
||||
</div>
|
||||
|
||||
{{/* Light mode subtle background */}}
|
||||
<div class="absolute inset-0 pointer-events-none dark:hidden">
|
||||
<div class="absolute top-1/4 -left-20 w-96 h-96 bg-primary-100 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-1/4 -right-20 w-80 h-80 bg-secondary-100 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 mx-auto max-w-5xl px-4 sm:px-6 lg:px-8 py-20 {{ if eq $style "centered" }}text-center{{ end }}">
|
||||
|
||||
{{ if eq $style "split" }}
|
||||
{{/* Split layout with avatar on side */}}
|
||||
<div class="flex flex-col md:flex-row items-center gap-12">
|
||||
|
||||
{{/* Avatar */}}
|
||||
{{ with $avatar }}
|
||||
<div class="flex-shrink-0">
|
||||
<div class="relative">
|
||||
<div class="w-48 h-48 md:w-64 md:h-64 {{ if eq $avatar_shape "circle" }}rounded-full{{ else }}rounded-2xl{{ end }} overflow-hidden ring-4 ring-gray-200 dark:ring-white/10 shadow-2xl shadow-primary-500/20">
|
||||
<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 }}
|
||||
<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>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* Decorative ring */}}
|
||||
<div class="absolute -inset-2 {{ if eq $avatar_shape "circle" }}rounded-full{{ else }}rounded-3xl{{ end }} border border-dashed border-gray-300 dark:border-white/10 animate-spin" style="animation-duration: 30s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Content */}}
|
||||
<div class="text-center md:text-left">
|
||||
{{/* Greeting */}}
|
||||
{{ with $block.content.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>
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* 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 }}
|
||||
<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") }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{{/* Role/Tagline with optional typewriter */}}
|
||||
{{ $role := $author.role | default $block.content.tagline }}
|
||||
{{ if and $typewriter.enable $typewriter.strings }}
|
||||
<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 }}
|
||||
<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-text="currentText"
|
||||
class="text-primary-600 dark:text-primary-400"
|
||||
></span><span class="typewriter-cursor"></span>
|
||||
</p>
|
||||
{{ else if $role }}
|
||||
<p class="text-xl sm:text-2xl text-gray-600 dark:text-gray-400 font-light mb-6">
|
||||
{{ $role | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* Bio */}}
|
||||
{{ with $author.bio | default $block.content.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 }}
|
||||
<div class="flex items-center gap-4 mb-8 {{ if eq $style "centered" }}justify-center{{ end }}">
|
||||
{{ range . }}
|
||||
{{ $icon := .icon | default "link" }}
|
||||
{{ $url := .url }}
|
||||
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
target="{{ if hasPrefix $url "mailto:" }}_self{{ else }}_blank{{ end }}"
|
||||
rel="noopener noreferrer"
|
||||
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 }}
|
||||
>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* CTA Buttons */}}
|
||||
{{ with $block.content.cta_buttons }}
|
||||
<div class="flex flex-wrap gap-4 {{ if eq $style "centered" }}justify-center{{ end }}">
|
||||
{{ range $idx, $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{{ 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\"") }}
|
||||
{{ end }}
|
||||
{{ .text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ else }}
|
||||
{{/* Centered layout */}}
|
||||
|
||||
{{/* Avatar */}}
|
||||
{{ with $avatar }}
|
||||
<div class="mb-8">
|
||||
<div class="relative inline-block">
|
||||
<div class="w-32 h-32 md:w-40 md:h-40 {{ if eq $avatar_shape "circle" }}rounded-full{{ else }}rounded-2xl{{ end }} overflow-hidden ring-4 ring-gray-200 dark:ring-white/10 shadow-2xl shadow-primary-500/20 mx-auto">
|
||||
<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 }}
|
||||
<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>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Greeting */}}
|
||||
{{ with $block.content.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>
|
||||
{{ . }}
|
||||
<span class="inline-block w-8 h-px bg-primary-500"></span>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* 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 }}
|
||||
<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") }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{{/* Role/Tagline with optional typewriter */}}
|
||||
{{ $role := $author.role | default $block.content.tagline }}
|
||||
{{ if and $typewriter.enable $typewriter.strings }}
|
||||
<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 }}
|
||||
<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-text="currentText"
|
||||
class="text-primary-600 dark:text-primary-400"
|
||||
></span><span class="typewriter-cursor"></span>
|
||||
</p>
|
||||
{{ else if $role }}
|
||||
<p class="text-xl sm:text-2xl md:text-3xl text-gray-600 dark:text-gray-400 font-light mb-6">
|
||||
{{ $role | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* Bio */}}
|
||||
{{ with $author.bio | default $block.content.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 }}
|
||||
<div class="flex items-center justify-center gap-4 mb-10">
|
||||
{{ range . }}
|
||||
{{ $icon := .icon | default "link" }}
|
||||
{{ $url := .url }}
|
||||
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
target="{{ if hasPrefix $url "mailto:" }}_self{{ else }}_blank{{ end }}"
|
||||
rel="noopener noreferrer"
|
||||
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 }}
|
||||
>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* CTA Buttons */}}
|
||||
{{ with $block.content.cta_buttons }}
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{{ range $idx, $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\"") }}
|
||||
{{ end }}
|
||||
{{ .text }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{/* Scroll indicator - clicks to scroll to next section */}}
|
||||
{{ if $block.content.show_scroll_indicator }}
|
||||
{{ $scroll_target := $block.content.scroll_target | default "#projects" }}
|
||||
<a
|
||||
href="{{ $scroll_target }}"
|
||||
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" }}"
|
||||
>
|
||||
<div class="w-6 h-10 rounded-full border-2 border-gray-300 dark:border-white/20 group-hover:border-primary-400/50 flex items-start justify-center p-2 transition-colors">
|
||||
<div class="w-1 h-2 bg-gray-400 dark:bg-white/40 group-hover:bg-primary-400 rounded-full animate-pulse transition-colors"></div>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "dev-hero",
|
||||
"name": "Dev Hero",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"category": "hero",
|
||||
"tags": ["hero", "developer", "portfolio", "introduction", "landing"],
|
||||
"description": "A developer-focused hero section with gradient effects, social links, and CTA buttons - optimized for dark mode",
|
||||
"author": "Hugo Blox",
|
||||
"homepage": "https://hugoblox.com/blocks/",
|
||||
"repository": "https://github.com/HugoBlox/kit",
|
||||
"keywords": ["hugo", "hero", "developer", "portfolio", "landing-page"]
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://hugoblox.com/schemas/blocks/dev-hero.json",
|
||||
"title": "Dev Hero Block Schema",
|
||||
"description": "Schema for the Dev Hero block - a developer-focused hero section with gradient effects",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "../shared/schemas/base-block.json"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block": {
|
||||
"const": "dev-hero",
|
||||
"description": "Block type identifier"
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"description": "Content configuration for the dev hero block",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "Author username to pull data from (matches folder name in /authors/)",
|
||||
"default": "me",
|
||||
"examples": ["me", "admin"]
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Override name/title (falls back to author title)"
|
||||
},
|
||||
"tagline": {
|
||||
"type": "string",
|
||||
"description": "Override role/tagline (falls back to author role)"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string",
|
||||
"description": "Override bio text (falls back to author bio)"
|
||||
},
|
||||
"greeting": {
|
||||
"type": "string",
|
||||
"description": "Small greeting text above the name",
|
||||
"examples": ["Hi, I'm", "Hello, I'm", "Welcome"]
|
||||
},
|
||||
"name_prefix": {
|
||||
"type": "string",
|
||||
"description": "Prefix before the name (styled differently)",
|
||||
"examples": ["I'm", "Hey, I'm"]
|
||||
},
|
||||
"show_status": {
|
||||
"type": "boolean",
|
||||
"description": "Show status emoji badge on avatar (uses author's status.icon from profile)",
|
||||
"default": false
|
||||
},
|
||||
"show_scroll_indicator": {
|
||||
"type": "boolean",
|
||||
"description": "Show scroll down indicator at bottom",
|
||||
"default": false
|
||||
},
|
||||
"scroll_target": {
|
||||
"type": "string",
|
||||
"description": "CSS selector for scroll indicator target",
|
||||
"default": "#projects",
|
||||
"examples": ["#projects", "#about", "#contact"]
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
"description": "Override social links (falls back to author links)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Icon identifier",
|
||||
"examples": ["brands/github", "brands/linkedin", "brands/x", "at-symbol"]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Link URL"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Link label/title"
|
||||
}
|
||||
},
|
||||
"required": ["icon", "url"]
|
||||
}
|
||||
},
|
||||
"cta_buttons": {
|
||||
"type": "array",
|
||||
"description": "Call-to-action buttons",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Button text",
|
||||
"examples": ["View My Work", "Download Resume", "Contact Me"]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Button URL",
|
||||
"examples": ["#projects", "/resume.pdf", "#contact"]
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Button icon (optional)",
|
||||
"examples": ["arrow-down", "document-arrow-down", "envelope"]
|
||||
}
|
||||
},
|
||||
"required": ["text", "url"]
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
{ "text": "View My Work", "url": "#projects", "icon": "arrow-down" },
|
||||
{ "text": "Download Resume", "url": "/resume.pdf", "icon": "document-arrow-down" }
|
||||
]
|
||||
]
|
||||
},
|
||||
"typewriter": {
|
||||
"type": "object",
|
||||
"description": "Typewriter animation for cycling through roles/taglines",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"type": "boolean",
|
||||
"description": "Enable typewriter effect",
|
||||
"default": false
|
||||
},
|
||||
"strings": {
|
||||
"type": "array",
|
||||
"description": "Array of strings to cycle through",
|
||||
"items": { "type": "string" },
|
||||
"examples": [["Full-Stack Developer", "Open Source Contributor", "Tech Enthusiast"]]
|
||||
},
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"description": "Static text before the animated part",
|
||||
"examples": ["I'm a", "I build"]
|
||||
},
|
||||
"type_speed": {
|
||||
"type": "integer",
|
||||
"description": "Typing speed in milliseconds per character",
|
||||
"default": 80
|
||||
},
|
||||
"delete_speed": {
|
||||
"type": "integer",
|
||||
"description": "Delete speed in milliseconds per character",
|
||||
"default": 50
|
||||
},
|
||||
"pause_time": {
|
||||
"type": "integer",
|
||||
"description": "Pause time in milliseconds before deleting",
|
||||
"default": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"design": {
|
||||
"type": "object",
|
||||
"description": "Layout options for dev hero rendering",
|
||||
"properties": {
|
||||
"style": {
|
||||
"type": "string",
|
||||
"description": "Layout style",
|
||||
"enum": ["centered", "split"],
|
||||
"default": "centered"
|
||||
},
|
||||
"avatar_shape": {
|
||||
"type": "string",
|
||||
"description": "Avatar shape",
|
||||
"enum": ["circle", "rounded"],
|
||||
"default": "circle"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"examples": [
|
||||
{
|
||||
"block": "dev-hero",
|
||||
"id": "hero",
|
||||
"content": {
|
||||
"username": "me",
|
||||
"greeting": "Hi, I'm",
|
||||
"show_status": true,
|
||||
"show_scroll_indicator": true,
|
||||
"cta_buttons": [
|
||||
{ "text": "View My Work", "url": "#projects", "icon": "arrow-down" },
|
||||
{ "text": "Get In Touch", "url": "#contact", "icon": "envelope" }
|
||||
]
|
||||
},
|
||||
"design": {
|
||||
"style": "centered",
|
||||
"avatar_shape": "circle",
|
||||
"background": {
|
||||
"color": {
|
||||
"dark": "#0a0a0f"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
# Portfolio Block
|
||||
|
||||
A flexible, filterable portfolio block for showcasing work with Alpine.js-powered filtering and modern glass morphism styling.
|
||||
|
||||
**Versatile Use Cases:** Software projects, research papers, design work, photography, teaching portfolios, case studies, publications, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Tag Filtering**: Filter items by tags with smooth Alpine.js transitions
|
||||
- **Glass Morphism**: Modern card design with backdrop blur and gradient borders
|
||||
- **Status Badges**: Display status (Live, In Progress, Completed, Published, etc.)
|
||||
- **Flexible Keywords**: Show skills, technologies, tools, materials, or methods
|
||||
- **Custom Links**: GitHub, live sites, PDFs, videos, or any custom link type
|
||||
- **Responsive Grid**: Configurable 2, 3, or 4 column layout
|
||||
- **Smooth Animations**: Fade and scale transitions on filter
|
||||
- **Customizable Icons**: Configure fallback icon for items without images
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```yaml
|
||||
- block: portfolio
|
||||
id: portfolio
|
||||
content:
|
||||
title: "My Work"
|
||||
subtitle: "A selection of recent projects"
|
||||
count: 6
|
||||
filters:
|
||||
folders:
|
||||
- projects # Or: research, portfolio, work, teaching, etc.
|
||||
buttons:
|
||||
- name: All
|
||||
tag: '*'
|
||||
- name: Category A
|
||||
tag: CategoryA
|
||||
- name: Category B
|
||||
tag: CategoryB
|
||||
default_button_index: 0
|
||||
archive:
|
||||
# Auto-shown if more items exist than displayed
|
||||
# Optionally customize:
|
||||
# enable: false # Explicitly hide
|
||||
# link: "/work/" # Custom URL
|
||||
# text: "Browse All" # Custom text
|
||||
design:
|
||||
columns: 3
|
||||
fallback_icon: code-bracket # Or: academic-cap, paint-brush, camera, etc.
|
||||
```
|
||||
|
||||
## Item Frontmatter
|
||||
|
||||
Each portfolio item should include the following frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Item Title"
|
||||
date: 2024-01-15
|
||||
summary: "Brief description of the work"
|
||||
tags:
|
||||
- Category
|
||||
- Subcategory
|
||||
- Tag
|
||||
tech_stack: # Or think of as: tools, methods, skills, materials
|
||||
- Tool 1
|
||||
- Tool 2
|
||||
- Tool 3
|
||||
links:
|
||||
- type: custom # Or: github, live, pdf, video, etc.
|
||||
url: https://example.com
|
||||
label: View Project
|
||||
icon: globe-alt # Optional
|
||||
status: "Completed" # Or: Live, Published, In Progress, etc.
|
||||
featured: true
|
||||
---
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Content
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `title` | string | - | Section title |
|
||||
| `subtitle` | string | - | Section subtitle |
|
||||
| `count` | integer | 6 | Number of projects to display |
|
||||
| `filters.folders` | array | `["projects"]` | Content folders to query |
|
||||
| `buttons` | array | `[{name: "All", tag: "*"}]` | Filter buttons |
|
||||
| `default_button_index` | integer | 0 | Default active filter |
|
||||
| `archive.enable` | boolean | auto | Show "View All" link (auto-shown if more projects exist, set to `false` to hide) |
|
||||
| `archive.link` | string | auto | Archive URL (defaults to first folder in `filters.folders`) |
|
||||
| `archive.text` | string | i18n | Archive link text (uses i18n `portfolio_view_all`) |
|
||||
|
||||
### Design
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `columns` | integer | 3 | Grid columns (2, 3, or 4) |
|
||||
| `fallback_icon` | string | `code-bracket` | Icon shown when item has no image. Supports all Hugo Blox icon packs (hero, brands, devicon, emoji, custom). Format: `icon-name` or `pack/icon-name` |
|
||||
|
||||
## Link Types
|
||||
|
||||
Each link supports the following properties:
|
||||
- `type`: Link type (github, live, demo, or custom)
|
||||
- `url`: Link URL
|
||||
- `label`: Custom label text (optional, overrides default)
|
||||
- `icon`: Custom icon (optional, overrides default - supports all icon packs)
|
||||
|
||||
| Type | Icon | Default Label |
|
||||
|------|------|---------------|
|
||||
| `github` | `brands/github` | "Code" |
|
||||
| `live` | `globe-alt` | "Live" |
|
||||
| `demo` | `play` | "Demo" |
|
||||
| Other | `link` | "Link" |
|
||||
|
||||
**Note:** Labels are shared with the project page header, so setting them in frontmatter ensures consistency across card and page views.
|
||||
|
||||
**Icon Examples:**
|
||||
```yaml
|
||||
links:
|
||||
- type: custom
|
||||
url: "..."
|
||||
label: "Watch Video"
|
||||
icon: play-circle # Hero icon (default pack)
|
||||
- type: custom
|
||||
url: "..."
|
||||
label: "View on GitHub"
|
||||
icon: brands/github # Brand icon
|
||||
- type: pdf
|
||||
url: "paper.pdf"
|
||||
label: "Read Paper"
|
||||
icon: document-text # Hero icon
|
||||
- type: custom
|
||||
url: "..."
|
||||
label: "Python Code"
|
||||
icon: devicon/python # Devicon
|
||||
```
|
||||
|
||||
## Status Badges
|
||||
|
||||
| Status | Color |
|
||||
|--------|-------|
|
||||
| `Live` | Emerald/Green |
|
||||
| `WIP` | Amber/Yellow |
|
||||
| `Archived` | Gray |
|
||||
|
||||
**Note:** You can use any custom status text. Color coding: emerald for active/live/published, amber for in-progress, gray for archived/completed.
|
||||
|
||||
## Use Case Examples
|
||||
|
||||
### Software Developer Portfolio
|
||||
|
||||
```yaml
|
||||
- block: portfolio
|
||||
content:
|
||||
title: "Featured Projects"
|
||||
filters:
|
||||
folders: [projects]
|
||||
buttons:
|
||||
- {name: All, tag: '*'}
|
||||
- {name: Full-Stack, tag: Full-Stack}
|
||||
- {name: Frontend, tag: Frontend}
|
||||
design:
|
||||
fallback_icon: code-bracket
|
||||
```
|
||||
|
||||
**Project frontmatter:**
|
||||
```yaml
|
||||
title: "E-Commerce Platform"
|
||||
tags: [Full-Stack, React, Node.js]
|
||||
tech_stack: [React, TypeScript, PostgreSQL]
|
||||
links:
|
||||
- {type: github, url: "...", label: Code}
|
||||
- {type: live, url: "...", label: Demo}
|
||||
status: "Live"
|
||||
```
|
||||
|
||||
### Academic Research Portfolio
|
||||
|
||||
```yaml
|
||||
- block: portfolio
|
||||
content:
|
||||
title: "Research Projects"
|
||||
filters:
|
||||
folders: [research]
|
||||
buttons:
|
||||
- {name: All, tag: '*'}
|
||||
- {name: Machine Learning, tag: ML}
|
||||
- {name: Computational Biology, tag: CompBio}
|
||||
design:
|
||||
fallback_icon: academic-cap # Or: beaker, emoji/microscope, devicon/python
|
||||
columns: 2
|
||||
```
|
||||
|
||||
**Research project frontmatter:**
|
||||
```yaml
|
||||
title: "Neural Network Optimization Study"
|
||||
tags: [ML, Optimization, Deep Learning]
|
||||
tech_stack: [Python, PyTorch, CUDA, NumPy] # Or rename to "methods", "tools"
|
||||
links:
|
||||
- {type: pdf, url: "paper.pdf", label: Read Paper}
|
||||
- {type: code, url: "github.com/...", label: Code}
|
||||
- {type: dataset, url: "...", label: Dataset}
|
||||
status: "Published"
|
||||
```
|
||||
|
||||
### Design/Creative Portfolio
|
||||
|
||||
```yaml
|
||||
- block: portfolio
|
||||
content:
|
||||
title: "Design Work"
|
||||
filters:
|
||||
folders: [portfolio]
|
||||
buttons:
|
||||
- {name: All, tag: '*'}
|
||||
- {name: Brand Design, tag: Branding}
|
||||
- {name: UI/UX, tag: UI/UX}
|
||||
- {name: Illustration, tag: Illustration}
|
||||
design:
|
||||
fallback_icon: paint-brush # Or: camera, emoji/art, brands/dribbble
|
||||
columns: 3
|
||||
```
|
||||
|
||||
**Design project frontmatter:**
|
||||
```yaml
|
||||
title: "Acme Corp Brand Identity"
|
||||
tags: [Branding, Logo Design, Print]
|
||||
tech_stack: [Adobe Illustrator, Photoshop, Figma] # Tools used
|
||||
links:
|
||||
- {type: custom, url: "behance.net/...", label: View on Behance, icon: globe-alt}
|
||||
- {type: pdf, url: "case-study.pdf", label: Case Study}
|
||||
status: "Completed"
|
||||
```
|
||||
|
||||
### Teaching Portfolio
|
||||
|
||||
```yaml
|
||||
- block: portfolio
|
||||
content:
|
||||
title: "Courses & Materials"
|
||||
filters:
|
||||
folders: [teaching]
|
||||
buttons:
|
||||
- {name: All, tag: '*'}
|
||||
- {name: Undergraduate, tag: Undergrad}
|
||||
- {name: Graduate, tag: Grad}
|
||||
design:
|
||||
fallback_icon: document-text
|
||||
```
|
||||
|
||||
**Course frontmatter:**
|
||||
```yaml
|
||||
title: "Introduction to Data Science"
|
||||
tags: [Undergrad, Statistics, Programming]
|
||||
tech_stack: [Python, R, Jupyter, Pandas] # Tools taught
|
||||
links:
|
||||
- {type: custom, url: "syllabus.pdf", label: Syllabus, icon: document-text}
|
||||
- {type: custom, url: "github.com/...", label: Course Materials, icon: brands/github}
|
||||
status: "Spring 2024"
|
||||
```
|
||||
|
||||
## Customization Tips
|
||||
|
||||
1. **Rename fields conceptually** - While `tech_stack` is the parameter name, you can use it for:
|
||||
- Software: Technologies, frameworks, tools
|
||||
- Research: Methods, techniques, software
|
||||
- Design: Tools, software, materials
|
||||
- Writing: Genres, themes, topics
|
||||
|
||||
2. **Custom link types** - Beyond `github`, `live`, `demo`:
|
||||
- `pdf` for papers, reports, case studies
|
||||
- `dataset` for research data
|
||||
- `video` for presentations, demos
|
||||
- `slides` for slide decks
|
||||
- Custom types with your own labels and icons
|
||||
|
||||
3. **Flexible status badges** - Use any text:
|
||||
- Software: "Live", "Beta", "Archived"
|
||||
- Research: "Published", "Under Review", "In Progress"
|
||||
- Design: "Completed", "Ongoing", "Client Work"
|
||||
- Academic: "Spring 2024", "Fall 2023", "Ongoing"
|
||||
|
||||
4. **Icon selection** - Choose an appropriate `fallback_icon`:
|
||||
- **Hero icons** (default pack, no prefix needed):
|
||||
- `code-bracket` - Software/development
|
||||
- `academic-cap` - Academic/education
|
||||
- `beaker` - Research/science
|
||||
- `paint-brush` - Art/design
|
||||
- `camera` - Photography
|
||||
- `document-text` - Writing/documentation
|
||||
- `star` - Featured/highlights
|
||||
- **Brand icons** (use `brands/` prefix):
|
||||
- `brands/github` - GitHub projects
|
||||
- `brands/linkedin` - Professional work
|
||||
- `brands/dribbble` - Design work
|
||||
- **Devicon** (use `devicon/` prefix):
|
||||
- `devicon/python` - Python projects
|
||||
- `devicon/react` - React projects
|
||||
- `devicon/docker` - DevOps work
|
||||
- **Emoji** (use `emoji/` prefix or direct unicode):
|
||||
- `emoji/rocket` or 🚀
|
||||
- `emoji/art` or 🎨
|
||||
- `emoji/microscope` or 🔬
|
||||
- **Custom icons**: Place in `assets/media/icons/<pack>/` and use `<pack>/<name>`
|
||||
@@ -0,0 +1,267 @@
|
||||
{{/* 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 }}
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16" x-data="{ activeFilter: '{{ $default_tag }}' }">
|
||||
|
||||
{{/* Header */}}
|
||||
<div class="text-center mb-12">
|
||||
{{ with $block.content.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 }}
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Filter Buttons */}}
|
||||
{{ if gt (len $buttons) 1 }}
|
||||
<div class="flex flex-wrap justify-center gap-2 mb-10">
|
||||
{{ range $idx, $button := $buttons }}
|
||||
<button
|
||||
@click="activeFilter = '{{ $button.tag }}'"
|
||||
:class="activeFilter === '{{ $button.tag }}'
|
||||
? '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>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Project Grid with staggered reveal animation */}}
|
||||
<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 . "," }}
|
||||
{{ 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 }}
|
||||
|
||||
<article
|
||||
x-show="activeFilter === '*' || '{{ $item_tags }}'.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"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="group relative flex flex-col bg-white dark:bg-white/[0.03] backdrop-blur-xl rounded-2xl border border-gray-200 dark:border-white/[0.08] overflow-hidden hover:border-primary-500/50 hover:bg-gray-50 dark:hover:bg-white/[0.06] transition-all duration-300 hover:shadow-xl hover:shadow-primary-500/10 card-hover-lift"
|
||||
>
|
||||
{{/* Card Header / Image */}}
|
||||
<div class="relative h-48 overflow-hidden">
|
||||
{{ $image := $item.Resources.GetMatch "{featured,cover,thumbnail}*" }}
|
||||
{{ if $image }}
|
||||
{{ if strings.Contains $image.MediaType.SubType "svg" }}
|
||||
<img
|
||||
src="{{ $image.RelPermalink }}"
|
||||
alt="{{ $item.Title }}"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
>
|
||||
{{ else }}
|
||||
{{ $img := $image.Fill "600x400 webp" }}
|
||||
<img
|
||||
src="{{ $img.RelPermalink }}"
|
||||
alt="{{ $item.Title }}"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
>
|
||||
{{ 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>
|
||||
{{ end }}
|
||||
|
||||
{{/* Status Badge */}}
|
||||
{{ with $status }}
|
||||
<div class="absolute top-3 right-3">
|
||||
<span class="px-2.5 py-1 text-xs font-semibold rounded-full border backdrop-blur-sm {{ $status_class }}">
|
||||
{{ . }}
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Gradient Overlay */}}
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-gray-900/90 via-gray-900/20 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{{/* Card Content - use flex-col to push links to bottom */}}
|
||||
<div class="p-5 flex flex-col flex-grow">
|
||||
{{/* Title */}}
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
<a href="{{ $item.RelPermalink }}" class="stretched-link">
|
||||
{{ $item.Title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{{/* Summary */}}
|
||||
{{ with $item.Params.summary }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ else }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
|
||||
{{ $item.Summary | truncate 100 }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{/* Tech Stack Tags - fixed 2-line height for consistency */}}
|
||||
{{ with $item.Params.tech_stack }}
|
||||
<div class="flex flex-wrap gap-1.5 mb-4 min-h-[3.5rem]">
|
||||
{{ range first 4 . }}
|
||||
<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 }}
|
||||
<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 }}
|
||||
</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" }}
|
||||
{{ 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 }}
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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();"
|
||||
>
|
||||
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-4 h-4\"") }}
|
||||
<span>{{ $label }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* 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") }}
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a
|
||||
href="{{ $archive_link }}"
|
||||
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 }}
|
||||
{{ partial "functions/get_icon" (dict "name" "arrow-right" "attributes" "class=\"w-4 h-4\"") }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "portfolio",
|
||||
"name": "Portfolio",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"category": "content",
|
||||
"tags": ["portfolio", "projects", "filter", "alpine", "developer", "showcase"],
|
||||
"description": "Display filterable project cards with Alpine.js powered tag filtering, glass morphism styling, and smooth animations",
|
||||
"author": "Hugo Blox",
|
||||
"homepage": "https://hugoblox.com/blocks/",
|
||||
"repository": "https://github.com/HugoBlox/kit",
|
||||
"keywords": ["hugo", "portfolio", "projects", "filtering", "alpine.js", "developer"]
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://hugoblox.com/schemas/blocks/portfolio.json",
|
||||
"title": "Portfolio Block Schema",
|
||||
"description": "Schema for the Portfolio block - displays filterable project cards with Alpine.js",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "../shared/schemas/base-block.json"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block": {
|
||||
"const": "portfolio",
|
||||
"description": "Block type identifier"
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"description": "Content configuration for the portfolio block",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Section title (supports Markdown)",
|
||||
"examples": ["Featured Projects"]
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"description": "Section subtitle (supports Markdown)",
|
||||
"examples": ["Things I've built"]
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of projects to display (0 means unlimited)",
|
||||
"minimum": 0,
|
||||
"default": 6
|
||||
},
|
||||
"sort_by": {
|
||||
"type": "string",
|
||||
"description": "Field to sort by (e.g., Date, title, weight)",
|
||||
"default": "Date"
|
||||
},
|
||||
"sort_ascending": {
|
||||
"type": "boolean",
|
||||
"description": "Sort in ascending order",
|
||||
"default": false
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"description": "Content filtering options",
|
||||
"properties": {
|
||||
"folders": {
|
||||
"type": "array",
|
||||
"description": "Content folders to query projects from",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["projects"],
|
||||
"examples": [["projects", "software"]]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"buttons": {
|
||||
"type": "array",
|
||||
"description": "Filter buttons configuration",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Button display text",
|
||||
"examples": ["All", "Frontend", "Backend"]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Tag to filter by (* for all)",
|
||||
"examples": ["*", "Frontend", "Full-Stack"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "tag"]
|
||||
},
|
||||
"default": [{ "name": "All", "tag": "*" }],
|
||||
"examples": [
|
||||
[
|
||||
{ "name": "All", "tag": "*" },
|
||||
{ "name": "Full-Stack", "tag": "Full-Stack" },
|
||||
{ "name": "Frontend", "tag": "Frontend" },
|
||||
{ "name": "Backend", "tag": "Backend" }
|
||||
]
|
||||
]
|
||||
},
|
||||
"default_button_index": {
|
||||
"type": "integer",
|
||||
"description": "Index of the default active filter button",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"archive": {
|
||||
"type": "object",
|
||||
"description": "Archive link configuration",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"type": "boolean",
|
||||
"description": "Show 'View All' link (defaults to true if more projects exist than displayed, set to false to explicitly hide)"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"description": "Override archive URL (defaults to the first folder in filters.folders)",
|
||||
"examples": ["/projects/", "/work/"]
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Override archive link text (uses i18n 'portfolio_view_all' by default)",
|
||||
"examples": ["View All Projects", "See My Work", "Browse Portfolio"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"design": {
|
||||
"type": "object",
|
||||
"description": "Layout options for portfolio rendering",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "integer",
|
||||
"description": "Number of columns (2, 3, or 4)",
|
||||
"default": 3,
|
||||
"enum": [2, 3, 4],
|
||||
"examples": [2, 3, 4]
|
||||
},
|
||||
"fallback_icon": {
|
||||
"type": "string",
|
||||
"description": "Icon to display when no image is provided. Supports all Hugo Blox icon packs: hero (default), brands, devicon, emoji, or custom packs. Format: 'icon-name' or 'pack/icon-name'",
|
||||
"default": "code-bracket",
|
||||
"examples": ["code-bracket", "academic-cap", "beaker", "paint-brush", "brands/github", "devicon/python", "emoji/rocket"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": true,
|
||||
"examples": [
|
||||
{
|
||||
"block": "portfolio",
|
||||
"id": "projects",
|
||||
"content": {
|
||||
"title": "Featured Projects",
|
||||
"subtitle": "A selection of my recent work",
|
||||
"count": 6,
|
||||
"filters": {
|
||||
"folders": ["projects"]
|
||||
},
|
||||
"buttons": [
|
||||
{ "name": "All", "tag": "*" },
|
||||
{ "name": "Full-Stack", "tag": "Full-Stack" },
|
||||
{ "name": "Frontend", "tag": "Frontend" },
|
||||
{ "name": "Backend", "tag": "Backend" }
|
||||
],
|
||||
"default_button_index": 0,
|
||||
"archive": {
|
||||
"enable": true,
|
||||
"text": "View All Projects"
|
||||
}
|
||||
},
|
||||
"design": {
|
||||
"columns": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{/* Hugo Blox: Experience */}}
|
||||
{{/* Hugo Blox: Experience - 2026 Redesign */}}
|
||||
{{/* Documentation: https://hugoblox.com/blocks/ */}}
|
||||
{{/* License: https://github.com/HugoBlox/kit/blob/main/LICENSE.md */}}
|
||||
|
||||
@@ -11,106 +11,209 @@
|
||||
{{ $profile := partial "functions/get_author_profile" $author }}
|
||||
{{ $is_education_first := $block.design.is_education_first | default false }}
|
||||
|
||||
<div class="flex flex-col items-center mx-auto w-full max-w-prose justify-center">
|
||||
<!-- <article class="prose prose-slate lg:prose-xl dark:prose-invert">-->
|
||||
<!-- {{ with $block.content.title }}-->
|
||||
<!-- <h2>{{ . }}</h2>-->
|
||||
<!-- {{end}}-->
|
||||
<!-- </article>-->
|
||||
<div class="mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- Need Gap >= 3 when flex-row due to negative margin on cols (-start-3) -->
|
||||
<div class="flex flex-col lg:gap-x-6 w-full px-6 sm:px-0 {{ if $is_education_first }}flex-col-reverse{{end}}">
|
||||
<div class="flex flex-col gap-16 {{ if $is_education_first }}flex-col-reverse{{end}}">
|
||||
|
||||
{{ $experience := or $profile.experience $profile.work }}
|
||||
{{ if $experience }}
|
||||
<div class="w-full">
|
||||
<h3 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "experience" | default "Experience" }}</h3>
|
||||
<ol class="relative w-full border-s border-gray-200 dark:border-gray-700">
|
||||
{{ range $idx, $key := $experience }}
|
||||
<li class="mb-10 ms-6">
|
||||
<span class="absolute flex items-center justify-center w-6 h-6 bg-primary-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900">
|
||||
{{ if .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class='w-5 h-5'") }}
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-5 h-5 text-primary-800 dark:text-primary-300"><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}}
|
||||
</span>
|
||||
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{ .role | default .position }}
|
||||
<!-- <span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 ms-3">Latest</span>-->
|
||||
</h3>
|
||||
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{ .org | default .company_name }}</span>
|
||||
<time class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
|
||||
{{ $start := or .start .date_start }}
|
||||
{{ $end := or .end .date_end }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "January 2006") }} –
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "January 2006") }}
|
||||
{{ else }}
|
||||
{{ i18n "present" | default "Present" }}
|
||||
{{end}}
|
||||
</time>
|
||||
<div class="mb-4 text-base font-normal text-gray-500 dark:text-gray-300 prose max-w-none prose-slate dark:prose-invert">{{.summary | $page.RenderString}}</div>
|
||||
<h2 class="mb-10 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "experience" | default "Experience" }}</h2>
|
||||
|
||||
{{/* Timeline container */}}
|
||||
<div class="relative">
|
||||
{{/* Gradient timeline line */}}
|
||||
<div class="absolute left-4 sm:left-6 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary-500 via-primary-400 to-primary-600/50"></div>
|
||||
|
||||
{{/* Experience items */}}
|
||||
<div class="space-y-8">
|
||||
{{ range $idx, $item := $experience }}
|
||||
{{/* Card wrapper */}}
|
||||
<div class="group relative pl-12 sm:pl-16">
|
||||
|
||||
{{/* Timeline marker */}}
|
||||
<div class="absolute left-0 sm:left-2 top-6 flex items-center justify-center">
|
||||
{{/* Glowing dot */}}
|
||||
<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'") }}
|
||||
{{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}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Content card */}}
|
||||
<div class="relative bg-white dark:bg-white/[0.02] backdrop-blur-sm rounded-xl border border-gray-200 dark:border-white/[0.06] p-6 hover:bg-gray-50 dark:hover:bg-white/[0.04] hover:border-primary-300 dark:hover:border-primary-500/30 transition-all duration-300 group-hover:translate-x-1 shadow-sm dark:shadow-none">
|
||||
|
||||
{{/* Header row */}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 mb-3">
|
||||
<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 }}
|
||||
</h3>
|
||||
{{/* Company */}}
|
||||
<p class="text-primary-600 dark:text-primary-400 font-medium mt-1">
|
||||
{{ $item.org | default $item.company_name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{/* Date badge */}}
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 shrink-0">
|
||||
<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="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 }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "Jan 2006") }} –
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "Jan 2006") }}
|
||||
{{ else }}
|
||||
<span class="text-primary-600 dark:text-primary-400 font-medium">{{ i18n "present" | default "Present" }}</span>
|
||||
{{end}}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Description */}}
|
||||
{{ with $item.summary }}
|
||||
<div class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Tags/Skills */}}
|
||||
{{ with $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">
|
||||
{{ . }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .button }}
|
||||
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 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-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700">
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{ end }}
|
||||
{{ .text | default "Learn more" }}
|
||||
</a>
|
||||
{{/* 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'") }}
|
||||
{{ 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>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $profile.education }}
|
||||
<div class="w-full">
|
||||
<h3 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "education" }}</h3>
|
||||
<ol class="relative w-full border-s border-gray-200 dark:border-gray-700">
|
||||
{{ range $idx, $key := $profile.education }}
|
||||
<li class="mb-10 ms-6">
|
||||
<span class="absolute flex items-center justify-center w-6 h-6 bg-primary-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900">
|
||||
{{ if .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class='w-5 h-5 text-primary-800 dark:text-primary-300'") }}
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-5 h-5 text-primary-800 dark:text-primary-300"><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}}
|
||||
</span>
|
||||
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{ .degree | default .area }}
|
||||
<!-- <span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 ms-3">Latest</span>-->
|
||||
</h3>
|
||||
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{ .institution }}</span>
|
||||
<time class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
|
||||
{{ $start := or .start .date_start }}
|
||||
{{ $end := or .end .date_end }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "January 2006") }} –
|
||||
<h2 class="mb-10 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "education" }}</h2>
|
||||
|
||||
{{/* Timeline container */}}
|
||||
<div class="relative">
|
||||
{{/* Gradient timeline line */}}
|
||||
<div class="absolute left-4 sm:left-6 top-0 bottom-0 w-0.5 bg-gradient-to-b from-secondary-500 via-secondary-400 to-secondary-600/50"></div>
|
||||
|
||||
{{/* Education items */}}
|
||||
<div class="space-y-8">
|
||||
{{ range $idx, $item := $profile.education }}
|
||||
{{/* Card wrapper */}}
|
||||
<div class="group relative pl-12 sm:pl-16">
|
||||
|
||||
{{/* Timeline marker */}}
|
||||
<div class="absolute left-0 sm:left-2 top-6 flex items-center justify-center">
|
||||
{{/* Glowing dot */}}
|
||||
<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'") }}
|
||||
{{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}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Content card */}}
|
||||
<div class="relative bg-white dark:bg-white/[0.02] backdrop-blur-sm rounded-xl border border-gray-200 dark:border-white/[0.06] p-6 hover:bg-gray-50 dark:hover:bg-white/[0.04] hover:border-secondary-300 dark:hover:border-secondary-500/30 transition-all duration-300 group-hover:translate-x-1 shadow-sm dark:shadow-none">
|
||||
|
||||
{{/* Header row */}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 mb-3">
|
||||
<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 }}
|
||||
</h3>
|
||||
{{/* Institution */}}
|
||||
<p class="text-secondary-600 dark:text-secondary-400 font-medium mt-1">
|
||||
{{ $item.institution }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{/* Date badge */}}
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 shrink-0">
|
||||
<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="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 }}
|
||||
{{ if $start }}
|
||||
{{ (time $start) | time.Format ($block.design.date_format | default "Jan 2006") }} –
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "Jan 2006") }}
|
||||
{{ else }}
|
||||
<span class="text-primary-600 dark:text-primary-400 font-medium">{{ i18n "present" | default "Present" }}</span>
|
||||
{{end}}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Description */}}
|
||||
{{ with $item.summary }}
|
||||
<div class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{{ . | $page.RenderString }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if $end }}
|
||||
{{ (time $end) | time.Format ($block.design.date_format | default "January 2006") }}
|
||||
{{ else }}
|
||||
{{ i18n "present" | default "Present" }}
|
||||
{{end}}
|
||||
</time>
|
||||
<div class="mb-4 text-base font-normal text-gray-500 dark:text-gray-300 prose max-w-none prose-slate dark:prose-invert">{{.summary | $page.RenderString}}</div>
|
||||
|
||||
{{ with .button }}
|
||||
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 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-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700">
|
||||
{{ with .icon }}
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
|
||||
{{/* 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'") }}
|
||||
{{ 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>
|
||||
{{ end }}
|
||||
{{ .text | default "Learn more" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Tech Stack Block
|
||||
|
||||
A visual grid display of technologies and skills organized by category, with icon support and optional proficiency indicators.
|
||||
|
||||
## Features
|
||||
|
||||
- **Category Organization**: Group technologies by type (Languages, Frontend, Backend, etc.)
|
||||
- **Icon Support**: DevIcon, brand icons, or Heroicons integration
|
||||
- **Proficiency Levels**: Optional skill level indicators (expert, advanced, intermediate, beginner)
|
||||
- **Glass Morphism**: Modern card design with hover effects
|
||||
- **Responsive Grid**: Adapts from 2 to 6 columns based on screen size
|
||||
- **Multiple Styles**: Grid or flat list layout options
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
- block: tech-stack
|
||||
id: skills
|
||||
content:
|
||||
title: "Tech Stack"
|
||||
subtitle: "Technologies I use to build things"
|
||||
categories:
|
||||
- name: Languages
|
||||
items:
|
||||
- name: TypeScript
|
||||
icon: devicon/typescript
|
||||
level: expert
|
||||
- name: Python
|
||||
icon: devicon/python
|
||||
level: advanced
|
||||
- name: Go
|
||||
icon: devicon/go
|
||||
level: intermediate
|
||||
- name: Frontend
|
||||
items:
|
||||
- name: React
|
||||
icon: devicon/react
|
||||
- name: Next.js
|
||||
icon: devicon/nextjs
|
||||
- name: Tailwind CSS
|
||||
icon: devicon/tailwindcss
|
||||
- name: Backend
|
||||
items:
|
||||
- name: Node.js
|
||||
icon: devicon/nodejs
|
||||
- name: PostgreSQL
|
||||
icon: devicon/postgresql
|
||||
- name: Redis
|
||||
icon: devicon/redis
|
||||
- name: DevOps
|
||||
items:
|
||||
- name: Docker
|
||||
icon: devicon/docker
|
||||
- name: AWS
|
||||
icon: devicon/amazonwebservices
|
||||
- name: GitHub Actions
|
||||
icon: brands/github
|
||||
design:
|
||||
style: grid
|
||||
show_levels: true
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Content
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `title` | string | - | Section title |
|
||||
| `subtitle` | string | - | Section subtitle |
|
||||
| `categories` | array | required | Array of category objects |
|
||||
| `categories[].name` | string | - | Category heading |
|
||||
| `categories[].items` | array | required | Technologies in category |
|
||||
| `categories[].items[].name` | string | required | Technology name |
|
||||
| `categories[].items[].icon` | string | - | Icon identifier |
|
||||
| `categories[].items[].level` | string | - | Proficiency level |
|
||||
|
||||
### Design
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `style` | string | `grid` | Display style (`grid` or `list`) |
|
||||
| `show_levels` | boolean | `false` | Show proficiency indicators |
|
||||
|
||||
## Icon Support
|
||||
|
||||
The block supports multiple icon sources:
|
||||
|
||||
- **DevIcon**: `devicon/react`, `devicon/typescript`, `devicon/nodejs`
|
||||
- **Brand Icons**: `brands/github`, `brands/linkedin`
|
||||
- **Heroicons**: `code-bracket`, `server`, `cloud`
|
||||
|
||||
## Proficiency Levels
|
||||
|
||||
| Level | Color | Bar Width |
|
||||
|-------|-------|-----------|
|
||||
| `expert` | Emerald | 100% |
|
||||
| `advanced` | Primary | 75% |
|
||||
| `intermediate` | Amber | 50% |
|
||||
| `beginner` | Gray | 25% |
|
||||
|
||||
## Styling
|
||||
|
||||
The block uses glass morphism styling with:
|
||||
- Semi-transparent backgrounds
|
||||
- Backdrop blur effects
|
||||
- Subtle borders that glow on hover
|
||||
- Gradient hover effects
|
||||
@@ -0,0 +1,130 @@
|
||||
{{/* Hugo Blox: Tech Stack */}}
|
||||
{{/* Documentation: https://hugoblox.com/blocks/ */}}
|
||||
{{/* License: https://github.com/HugoBlox/kit/blob/main/LICENSE.md */}}
|
||||
|
||||
{{/* Initialise */}}
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{/* Enable animations */}}
|
||||
{{ if ne $block.design.animations false }}
|
||||
{{ $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 }}
|
||||
|
||||
<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 }}
|
||||
<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 }}
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ . | emojify | $page.RenderString }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Categories */}}
|
||||
<div class="space-y-12">
|
||||
{{ range $block.content.categories }}
|
||||
<div>
|
||||
{{/* Category Title */}}
|
||||
{{ with .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>
|
||||
<span class="h-px w-12 bg-gradient-to-l from-transparent to-primary-500/50"></span>
|
||||
</h3>
|
||||
{{ end }}
|
||||
|
||||
{{/* 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 "" }}
|
||||
{{ $level_width := "0%" }}
|
||||
{{ $level_color := "bg-gray-500" }}
|
||||
{{ if eq $level "expert" }}
|
||||
{{ $level_width = "100%" }}
|
||||
{{ $level_color = "bg-emerald-500" }}
|
||||
{{ else if eq $level "advanced" }}
|
||||
{{ $level_width = "75%" }}
|
||||
{{ $level_color = "bg-primary-500" }}
|
||||
{{ else if eq $level "intermediate" }}
|
||||
{{ $level_width = "50%" }}
|
||||
{{ $level_color = "bg-amber-500" }}
|
||||
{{ else if eq $level "beginner" }}
|
||||
{{ $level_width = "25%" }}
|
||||
{{ $level_color = "bg-gray-400" }}
|
||||
{{ end }}
|
||||
|
||||
<div
|
||||
class="group relative w-[calc(50%-0.5rem)] sm:w-[calc(33.333%-0.75rem)] md:w-[calc(25%-0.75rem)] lg:w-[140px] bg-gray-50 dark:bg-white/[0.03] backdrop-blur-sm rounded-xl border border-gray-200 dark:border-white/[0.06] p-4 hover:bg-gray-100 dark:hover:bg-white/[0.06] hover:border-primary-300 dark:hover:border-primary-500/30 transition-all duration-300 hover:shadow-lg hover:shadow-primary-500/5 scale-hover"
|
||||
>
|
||||
{{/* Icon */}}
|
||||
<div class="flex flex-col items-center text-center">
|
||||
{{ with $item.icon }}
|
||||
{{/* Add dark-icon class for icons that need inversion in dark mode */}}
|
||||
{{ $dark_icons := slice "express" "vercel" "github" "nextjs" }}
|
||||
{{ $icon_name := . }}
|
||||
{{ $needs_invert := false }}
|
||||
{{ range $dark_icons }}
|
||||
{{ if in $icon_name . }}{{ $needs_invert = true }}{{ end }}
|
||||
{{ end }}
|
||||
<div class="w-12 h-12 mb-3 flex items-center justify-center transition-all duration-300 icon-bounce {{ if $needs_invert }}dark-mode-icon{{ end }}">
|
||||
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-10 h-10\"") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* 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 }}
|
||||
</span>
|
||||
|
||||
{{/* Level Indicator */}}
|
||||
{{ if and $show_levels $level }}
|
||||
<div class="mt-2 w-full">
|
||||
<div class="h-1 bg-gray-200 dark:bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full {{ $level_color }} rounded-full transition-all duration-500" style="width: {{ $level_width }}"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 mt-1 capitalize">{{ $level }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Hover glow effect */}}
|
||||
<div class="absolute inset-0 rounded-xl bg-gradient-to-br from-primary-500/0 via-primary-500/0 to-secondary-500/0 group-hover:from-primary-500/5 group-hover:to-secondary-500/5 transition-all duration-500 pointer-events-none"></div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* 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 }}
|
||||
<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 }}
|
||||
<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>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "tech-stack",
|
||||
"name": "Tech Stack",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"category": "content",
|
||||
"tags": ["tech-stack", "skills", "technologies", "icons", "developer", "showcase"],
|
||||
"description": "Display technologies and skills with icons organized by category, featuring hover effects and optional proficiency indicators",
|
||||
"author": "Hugo Blox",
|
||||
"homepage": "https://hugoblox.com/blocks/",
|
||||
"repository": "https://github.com/HugoBlox/kit",
|
||||
"keywords": ["hugo", "tech-stack", "skills", "technologies", "developer", "portfolio"]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://hugoblox.com/schemas/blocks/tech-stack.json",
|
||||
"title": "Tech Stack Block Schema",
|
||||
"description": "Schema for the Tech Stack block - displays technologies with icons organized by category",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "../shared/schemas/base-block.json"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block": {
|
||||
"const": "tech-stack",
|
||||
"description": "Block type identifier"
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"description": "Content configuration for the tech stack block",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Section title (supports Markdown)",
|
||||
"examples": ["Tech Stack", "Skills & Technologies"]
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"description": "Section subtitle (supports Markdown)",
|
||||
"examples": ["Technologies I work with daily"]
|
||||
},
|
||||
"categories": {
|
||||
"type": "array",
|
||||
"description": "Categories of technologies",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Category name",
|
||||
"examples": ["Languages", "Frontend", "Backend", "DevOps"]
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Technologies in this category",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Technology name",
|
||||
"examples": ["TypeScript", "React", "Node.js"]
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Icon identifier (supports devicon/, brands/, or hero icons)",
|
||||
"examples": ["devicon/typescript", "devicon/react", "brands/github"]
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "Proficiency level (optional)",
|
||||
"enum": ["expert", "advanced", "intermediate", "beginner"],
|
||||
"examples": ["expert", "advanced"]
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["items"]
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
{
|
||||
"name": "Languages",
|
||||
"items": [
|
||||
{ "name": "TypeScript", "icon": "devicon/typescript", "level": "expert" },
|
||||
{ "name": "Python", "icon": "devicon/python", "level": "advanced" }
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["categories"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"design": {
|
||||
"type": "object",
|
||||
"description": "Layout options for tech stack rendering",
|
||||
"properties": {
|
||||
"style": {
|
||||
"type": "string",
|
||||
"description": "Display style",
|
||||
"enum": ["grid", "list"],
|
||||
"default": "grid"
|
||||
},
|
||||
"show_levels": {
|
||||
"type": "boolean",
|
||||
"description": "Show proficiency level indicators",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": true,
|
||||
"examples": [
|
||||
{
|
||||
"block": "tech-stack",
|
||||
"id": "skills",
|
||||
"content": {
|
||||
"title": "Tech Stack",
|
||||
"subtitle": "Technologies I use to build things",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Languages",
|
||||
"items": [
|
||||
{ "name": "TypeScript", "icon": "devicon/typescript", "level": "expert" },
|
||||
{ "name": "Python", "icon": "devicon/python", "level": "advanced" },
|
||||
{ "name": "Go", "icon": "devicon/go", "level": "intermediate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Frontend",
|
||||
"items": [
|
||||
{ "name": "React", "icon": "devicon/react" },
|
||||
{ "name": "Next.js", "icon": "devicon/nextjs" },
|
||||
{ "name": "Tailwind CSS", "icon": "devicon/tailwindcss" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"design": {
|
||||
"style": "grid",
|
||||
"show_levels": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -263,3 +263,33 @@
|
||||
|
||||
- id: published_with
|
||||
translation: '{hugoblox}-এর সাথে প্রকাশিত – বিনামূল্যে, {repo_link}ওপেন সোর্স{/repo_link} ওয়েবসাইট নির্মাতা যা সৃজনকারীদের ক্ষমতায়ন করে।'
|
||||
|
||||
# Contact info block
|
||||
|
||||
- id: block_contact_follow_me
|
||||
translation: আমাকে খুঁজুন
|
||||
|
||||
# Dev Hero block
|
||||
|
||||
- id: scroll_to_content
|
||||
translation: কনটেন্টে স্ক্রোল করুন
|
||||
|
||||
- id: developer
|
||||
translation: ডেভেলপার
|
||||
|
||||
# Portfolio block
|
||||
|
||||
- id: portfolio_link_code
|
||||
translation: কোড
|
||||
|
||||
- id: portfolio_link_live
|
||||
translation: লাইভ
|
||||
|
||||
- id: portfolio_link_demo
|
||||
translation: ডেমো
|
||||
|
||||
- id: portfolio_link_default
|
||||
translation: লিংক
|
||||
|
||||
- id: portfolio_view_all
|
||||
translation: সমস্ত প্রকল্প দেখুন
|
||||
@@ -344,6 +344,36 @@
|
||||
- id: poweredby_button
|
||||
translation: 'Erstelle deine →'
|
||||
|
||||
# Contact info block
|
||||
|
||||
- id: block_contact_follow_me
|
||||
translation: Finde mich auf
|
||||
|
||||
# Dev Hero block
|
||||
|
||||
- id: scroll_to_content
|
||||
translation: Zum Inhalt scrollen
|
||||
|
||||
- id: developer
|
||||
translation: Entwickler
|
||||
|
||||
# Portfolio block
|
||||
|
||||
- id: portfolio_link_code
|
||||
translation: Code
|
||||
|
||||
- id: portfolio_link_live
|
||||
translation: Live
|
||||
|
||||
- id: portfolio_link_demo
|
||||
translation: Demo
|
||||
|
||||
- id: portfolio_link_default
|
||||
translation: Link
|
||||
|
||||
- id: portfolio_view_all
|
||||
translation: Alle Projekte anzeigen
|
||||
|
||||
# Feedback widget
|
||||
|
||||
- id: feedback_widget_title
|
||||
|
||||
@@ -283,6 +283,9 @@
|
||||
- id: block_contact_follow_us
|
||||
translation: Follow Us
|
||||
|
||||
- id: block_contact_follow_me
|
||||
translation: Find me on
|
||||
|
||||
- id: block_contact_prospective_title
|
||||
translation: Prospective Members
|
||||
|
||||
@@ -462,6 +465,31 @@
|
||||
- id: poweredby_button
|
||||
translation: 'Build yours →'
|
||||
|
||||
# Dev Hero block
|
||||
|
||||
- id: scroll_to_content
|
||||
translation: Scroll to content
|
||||
|
||||
- id: developer
|
||||
translation: Developer
|
||||
|
||||
# Portfolio block
|
||||
|
||||
- id: portfolio_link_code
|
||||
translation: Code
|
||||
|
||||
- id: portfolio_link_live
|
||||
translation: Live
|
||||
|
||||
- id: portfolio_link_demo
|
||||
translation: Demo
|
||||
|
||||
- id: portfolio_link_default
|
||||
translation: Link
|
||||
|
||||
- id: portfolio_view_all
|
||||
translation: View All Projects
|
||||
|
||||
# Feedback widget
|
||||
|
||||
- id: feedback_widget_title
|
||||
|
||||
@@ -307,3 +307,33 @@
|
||||
|
||||
- id: poweredby_button
|
||||
translation: 'Bouw de jouwe →'
|
||||
|
||||
# Contact info block
|
||||
|
||||
- id: block_contact_follow_me
|
||||
translation: Vind mij op
|
||||
|
||||
# Dev Hero block
|
||||
|
||||
- id: scroll_to_content
|
||||
translation: Scroll naar inhoud
|
||||
|
||||
- id: developer
|
||||
translation: Ontwikkelaar
|
||||
|
||||
# Portfolio block
|
||||
|
||||
- id: portfolio_link_code
|
||||
translation: Code
|
||||
|
||||
- id: portfolio_link_live
|
||||
translation: Live
|
||||
|
||||
- id: portfolio_link_demo
|
||||
translation: Demo
|
||||
|
||||
- id: portfolio_link_default
|
||||
translation: Link
|
||||
|
||||
- id: portfolio_view_all
|
||||
translation: Bekijk alle projecten
|
||||
@@ -329,3 +329,33 @@
|
||||
|
||||
- id: poweredby_button
|
||||
translation: '构建您的 →'
|
||||
|
||||
# Contact info block
|
||||
|
||||
- id: block_contact_follow_me
|
||||
translation: 在这里找到我
|
||||
|
||||
# Dev Hero block
|
||||
|
||||
- id: scroll_to_content
|
||||
translation: 滚动到内容
|
||||
|
||||
- id: developer
|
||||
translation: 开发者
|
||||
|
||||
# Portfolio block
|
||||
|
||||
- id: portfolio_link_code
|
||||
translation: 代码
|
||||
|
||||
- id: portfolio_link_live
|
||||
translation: 在线
|
||||
|
||||
- id: portfolio_link_demo
|
||||
translation: 演示
|
||||
|
||||
- id: portfolio_link_default
|
||||
translation: 链接
|
||||
|
||||
- id: portfolio_view_all
|
||||
translation: 查看所有项目
|
||||
@@ -0,0 +1,7 @@
|
||||
{{/* Hugo Blox: Contact Info - config */}}
|
||||
{{/* Purpose: pre-declare dependencies before <head> libraries load */}}
|
||||
|
||||
{{ $page := .wcPage }}
|
||||
|
||||
{{/* Contact block uses Alpine for copy-to-clipboard + small interactions */}}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{/* Hugo Blox: Dev Hero - config */}}
|
||||
{{/* Purpose: pre-declare dependencies before <head> libraries load */}}
|
||||
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{ $typewriter := $block.content.typewriter | default dict }}
|
||||
{{ $has_animations := or ($block.design.animations | default false) ($typewriter.enable | default false) }}
|
||||
|
||||
{{ if $has_animations }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{/* Hugo Blox: Portfolio - config */}}
|
||||
{{/* Purpose: pre-declare dependencies before <head> libraries load */}}
|
||||
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{/* Portfolio always needs Alpine for filtering */}}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
|
||||
{{/* Enable scroll/transition helpers if animations not explicitly disabled */}}
|
||||
{{ if ne $block.design.animations false }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{/* Hugo Blox: Tech Stack - config */}}
|
||||
{{/* Purpose: pre-declare dependencies before <head> libraries load */}}
|
||||
|
||||
{{ $page := .wcPage }}
|
||||
{{ $block := .wcBlock }}
|
||||
|
||||
{{ if ne $block.design.animations false }}
|
||||
{{ $page.Store.Set "has_animations" true }}
|
||||
{{ $page.Store.Set "has_alpine" true }}
|
||||
{{ end }}
|
||||
@@ -32,9 +32,19 @@
|
||||
{{/* Show site search? */}}
|
||||
{{ $show_search := site.Params.hugoblox.header.search | default false }}
|
||||
|
||||
{{/* Hugo Blox Animations - load synchronously BEFORE Alpine.js to register components */}}
|
||||
{{ if .Page.Store.Get "has_animations" }}
|
||||
{{ $animations_js := resources.Get "js/hb-animations.js" | resources.Minify }}
|
||||
{{- if hugo.IsProduction -}}
|
||||
{{ $animations_js = $animations_js | fingerprint "sha256" }}
|
||||
{{- end -}}
|
||||
<script src="{{ $animations_js.RelPermalink }}"{{ if hugo.IsProduction }} integrity="{{ $animations_js.Data.Integrity }}"{{ end }}></script>
|
||||
{{ end }}
|
||||
|
||||
{{/* Load Alpine JS extension? */}}
|
||||
{{/* Alpine is needed for search modal and other interactive components */}}
|
||||
{{ if or (.Page.Store.Get "has_alpine") $show_search }}
|
||||
{{ $needs_alpine := or (.Page.Store.Get "has_alpine") $show_search (.Page.Store.Get "has_animations") }}
|
||||
{{ if $needs_alpine }}
|
||||
<script>console.log('✓ Alpine.js loading on demand');</script>
|
||||
{{ $alpine_js := resources.Get "dist/lib/alpinejs/cdn.min.js" }}
|
||||
{{ $alpine_js = $alpine_js | resources.Fingerprint "sha256" }}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{{ $event_dates = partial "functions/get_event_dates" $item }}
|
||||
{{ end }}
|
||||
|
||||
<div class="group bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm rounded-2xl ring-1 ring-zinc-900/5 dark:ring-white/10 shadow-lg overflow-hidden transition-all duration-300 ease-out hover:shadow-xl hover:shadow-blue-500/10 hover:-translate-y-2 focus-within:ring-2 focus-within:ring-blue-500/50" role="article" aria-labelledby="card-title-{{ $item.File.UniqueID }}">
|
||||
<div class="group bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm rounded-2xl ring-1 ring-zinc-900/5 dark:ring-white/10 shadow-lg overflow-hidden transition-all duration-300 ease-out hover:shadow-xl hover:shadow-primary-500/10 hover:-translate-y-2 focus-within:ring-2 focus-within:ring-primary-500/50" role="article" aria-labelledby="card-title-{{ $item.File.UniqueID }}">
|
||||
<!-- Image Section -->
|
||||
<div class="relative overflow-hidden aspect-[16/9] bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-zinc-800 dark:to-zinc-900">
|
||||
{{ with $item.Params.content_meta }}
|
||||
@@ -36,7 +36,17 @@
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with $resource }}
|
||||
{{ if ne .MediaType.SubType "gif" }}
|
||||
{{ if strings.Contains .MediaType.SubType "svg" }}
|
||||
<a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="block">
|
||||
<img class="w-full h-full transition-transform duration-500 ease-out group-hover:scale-105 {{ if $fill_image }}object-fill{{ else }}object-contain{{ end }}"
|
||||
src="{{ .RelPermalink }}"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
{{ if eq $index 0 }}fetchpriority="high"{{ end }}
|
||||
style="position: absolute; height: 100%; width: 100%; inset: 0px; color: transparent;"
|
||||
alt="{{ $item.Title | plainify }} featured image">
|
||||
</a>
|
||||
{{ else if ne .MediaType.SubType "gif" }}
|
||||
{{ $original_image := "" }}
|
||||
{{ if $fill_image }}
|
||||
{{ $original_image = .Fill (printf "800x450 %s" $anchor) }}
|
||||
@@ -75,11 +85,11 @@
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<!-- Fallback gradient with subtle pattern - make clickable for consistency -->
|
||||
<!-- Fallback gradient using theme colors - make clickable for consistency -->
|
||||
<a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="block w-full h-full">
|
||||
<div class="w-full h-full bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center hover:from-blue-500/30 hover:to-purple-500/30 transition-colors duration-300">
|
||||
<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 transition-all duration-300 group-hover:from-primary-500/50 group-hover:via-secondary-500/40 group-hover:to-primary-700/60">
|
||||
<div class="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
||||
<svg class="w-8 h-8 text-white/60" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg class="w-8 h-8 text-white/40" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -97,7 +107,7 @@
|
||||
{{ if or .content_type .difficulty }}
|
||||
<div class="flex items-center gap-x-4">
|
||||
{{ with .content_type }}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300">
|
||||
{{ . }}
|
||||
</span>
|
||||
{{ end }}
|
||||
@@ -120,7 +130,7 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<h3 id="card-title-{{ $item.File.UniqueID }}" class="text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 leading-tight">
|
||||
<h3 id="card-title-{{ $item.File.UniqueID }}" class="text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-200 leading-tight">
|
||||
<a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="hover:underline">
|
||||
{{ $item.Title }}
|
||||
{{ if $item.Params.external_link }}
|
||||
@@ -207,7 +217,7 @@
|
||||
|
||||
<!-- Optional AI metadata -->
|
||||
{{ if $item.Params.ai_insights }}
|
||||
<div class="flex items-center gap-2 pt-2 text-xs text-blue-500 bg-blue-50 dark:bg-blue-950/30 rounded-lg px-3 py-2">
|
||||
<div class="flex items-center gap-2 pt-2 text-xs text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-950/30 rounded-lg px-3 py-2">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
|
||||
@@ -282,7 +282,20 @@
|
||||
{{end}}
|
||||
|
||||
{{/* Process featured image using utility with placement-specific constraints */}}
|
||||
{{ if ne $featured.MediaType.SubType "gif" }}
|
||||
{{ if strings.Contains $featured.MediaType.SubType "svg" }}
|
||||
{{/* SVGs cannot be processed by Hugo (Fit/Fill). Render directly. */}}
|
||||
<div class="article-header {{$image_container}} featured-image-wrapper mt-4 mb-16">
|
||||
<div style="position: relative">
|
||||
<img src="{{ $featured.RelPermalink }}"
|
||||
width="100%"
|
||||
data-pagefind-meta="image[src]"
|
||||
alt="{{ with $image_params.alt_text }}{{.}}{{ end }}"
|
||||
class="featured-image"
|
||||
fetchpriority="high">
|
||||
{{ with $image_params.caption }}<span class="article-header-caption">{{ . | markdownify | emojify }}</span>{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else if ne $featured.MediaType.SubType "gif" }}
|
||||
{{/* Pre-fit image to placement constraints, then apply responsive processing */}}
|
||||
{{ $fitted_image := $featured }}
|
||||
{{ if eq $placement 2}}
|
||||
|
||||
@@ -179,7 +179,19 @@
|
||||
{{end}}
|
||||
|
||||
{{/* Process featured image using utility with placement-specific constraints */}}
|
||||
{{ if ne $featured.MediaType.SubType "gif" }}
|
||||
{{ if strings.Contains $featured.MediaType.SubType "svg" }}
|
||||
{{/* SVGs cannot be processed by Hugo (Fit/Fill). Render directly. */}}
|
||||
<div class="article-header {{$image_container}} featured-image-wrapper mt-4 mb-16">
|
||||
<div style="position: relative">
|
||||
<img src="{{ $featured.RelPermalink }}"
|
||||
data-pagefind-meta="image[src]"
|
||||
alt="{{ with $image_params.alt_text }}{{.}}{{ end }}"
|
||||
class="featured-image"
|
||||
fetchpriority="high">
|
||||
{{ with $image_params.caption }}<span class="article-header-caption">{{ . | markdownify | emojify }}</span>{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else if ne $featured.MediaType.SubType "gif" }}
|
||||
{{/* Pre-fit image to placement constraints, then apply responsive processing */}}
|
||||
{{ $fitted_image := $featured }}
|
||||
{{ if eq $placement 2}}
|
||||
|
||||
Reference in new issue
Block a user