diff --git a/modules/blox/assets/css/animations.css b/modules/blox/assets/css/animations.css new file mode 100644 index 00000000..a8e20e55 --- /dev/null +++ b/modules/blox/assets/css/animations.css @@ -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); +} diff --git a/modules/blox/assets/css/main.css b/modules/blox/assets/css/main.css index 24cac2a9..b7157579 100644 --- a/modules/blox/assets/css/main.css +++ b/modules/blox/assets/css/main.css @@ -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"; diff --git a/modules/blox/assets/js/hb-animations.js b/modules/blox/assets/js/hb-animations.js new file mode 100644 index 00000000..c876ad27 --- /dev/null +++ b/modules/blox/assets/js/hb-animations.js @@ -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"); +} diff --git a/modules/blox/blox/contact-info/block.html b/modules/blox/blox/contact-info/block.html index 018c32b0..f80b049c 100644 --- a/modules/blox/blox/contact-info/block.html +++ b/modules/blox/blox/contact-info/block.html @@ -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 }} +
{{/* Section Header */}} @@ -27,12 +55,13 @@
{{/* Contact Cards Grid */}} -
+
- {{/* Visit Us Card */}} -
+ {{/* Visit Us Card - Only show if has content */}} + {{ if $has_visit_content }} +
-
+
{{ partial "functions/get_icon" (dict "name" "hero/map-pin" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}

@@ -50,7 +79,7 @@ {{ end }} {{ with $block.content.office_hours }} -
+ + {{ end }} - {{/* Connect Card */}} -
+ {{/* Connect Card - Only show if has content */}} + {{ if $has_connect_content }} +
+ + {{/* 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 */}} +
+ + {{/* Left: Email & CTA */}} +
+
+

+ {{ $block.content.connect_title | default (i18n "block_contact_connect_title" | default "Let's Connect") }} +

+

+ {{ $block.content.text | default "I'm always open to discussing new projects, opportunities, or just having a chat." }} +

+
+ + {{ with $email }} + {{/* Email with click-to-copy */}} +
+ + {{/* Toast notification */}} +
+ ✓ Copied to clipboard! +
+
+ + {{/* Primary CTA Button */}} + + {{ partial "functions/get_icon" (dict "name" "hero/paper-airplane" "attributes" "class=\"w-5 h-5 mr-2\"") }} + Send a message + + {{ end }} + + {{ with $phone }} + +
+ {{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }} +
+
+

Call me

+

{{ . }}

+
+
+ {{ end }} +
+ + {{/* Right: Social Links */}} + {{ with $filtered_social }} +
+

{{ i18n "block_contact_follow_me" | default "Find me on" }}

+ +
+ {{ end }} +
+ + {{ else }} + {{/* Standard card layout (when shown alongside Visit Us) */}}
-
+
{{ partial "functions/get_icon" (dict "name" "hero/chat-bubble-left-right" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}

@@ -84,32 +211,39 @@
{{/* Contact Methods */}}
- {{ with $block.content.email }} -
- {{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }} - - {{ . }} - -
+ {{ with $email }} + +
+ {{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }} +
+
+

Email

+

{{ . }}

+
+
{{ end }} - {{ with $block.content.phone }} -
- {{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }} - - {{ . }} - -
+ {{ with $phone }} + +
+ {{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }} +
+
+

Phone

+

{{ . }}

+
+
{{ end }} {{/* Social Media */}} - {{ with $block.content.social }} -
-

{{ i18n "block_contact_follow_us" | default "Follow Us" }}

- + {{ end }} - {{/* Prospective Members */}} - {{ with $block.content.prospective }} -
-

{{ .title | default (i18n "block_contact_prospective_title" | default "Prospective Members") }}

-

{{ .text }}

- {{ with .button }} - - {{ partial "functions/get_icon" (dict "name" "hero/user-plus" "attributes" "class=\"w-5 h-5 mr-2\"") }} - {{ .text }} - - {{ end }} -
+ {{/* Prospective Members - shown in both layouts */}} + {{ with $block.content.prospective }} +
+

{{ .title | default (i18n "block_contact_prospective_title" | default "Prospective Members") }}

+

{{ .text }}

+ {{ with .button }} + + {{ partial "functions/get_icon" (dict "name" "hero/user-plus" "attributes" "class=\"w-5 h-5 mr-2\"") }} + {{ .text }} + {{ end }}
+ {{ end }}
+ {{ end }}
{{/* Optional Map Embed */}} {{ with $block.content.map_embed }} -
+
{{ . | safeHTML }}
@@ -148,33 +284,33 @@ {{/* Contact Form (optional) */}} {{ if $block.content.show_form }}
-
+

{{ i18n "block_contact_form_title" | default "Send us a Message" }}

- + + 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">
- - + +
- + + 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">
- + + 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">
diff --git a/modules/blox/blox/cta-card/block.html b/modules/blox/blox/cta-card/block.html index 3d67f3f0..60f8fcd0 100644 --- a/modules/blox/blox/cta-card/block.html +++ b/modules/blox/blox/cta-card/block.html @@ -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 }} diff --git a/modules/blox/blox/dev-hero/README.md b/modules/blox/blox/dev-hero/README.md new file mode 100644 index 00000000..c2a44f35 --- /dev/null +++ b/modules/blox/blox/dev-hero/README.md @@ -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 diff --git a/modules/blox/blox/dev-hero/block.html b/modules/blox/blox/dev-hero/block.html new file mode 100644 index 00000000..1d4a83b3 --- /dev/null +++ b/modules/blox/blox/dev-hero/block.html @@ -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" }} + +
+ + {{/* Background Effects - only visible in dark mode */}} + + + {{/* Light mode subtle background */}} +
+
+
+
+ +
+ + {{ if eq $style "split" }} + {{/* Split layout with avatar on side */}} +
+ + {{/* Avatar */}} + {{ with $avatar }} +
+
+
+ {{ $author.title }} +
+ {{/* Status emoji badge */}} + {{ if $block.content.show_status }} + {{ with $author.status.icon }} + + {{ . | emojify }} + + {{ end }} + {{ end }} + {{/* Decorative ring */}} +
+
+
+ {{ end }} + + {{/* Content */}} +
+ {{/* Greeting */}} + {{ with $block.content.greeting }} +

+ + {{ . }} +

+ {{ end }} + + {{/* Name */}} +

+ {{ with $block.content.name_prefix }}{{ . }} {{ end }} + + {{ $author.title | default $block.content.title | default (i18n "developer" | default "Developer") }} + +

+ + {{/* Role/Tagline with optional typewriter */}} + {{ $role := $author.role | default $block.content.tagline }} + {{ if and $typewriter.enable $typewriter.strings }} +

+ {{ with $typewriter.prefix }}{{ . }} {{ end }} + +

+ {{ else if $role }} +

+ {{ $role | $page.RenderString }} +

+ {{ end }} + + {{/* Bio */}} + {{ with $author.bio | default $block.content.bio }} +

+ {{ . | $page.RenderString }} +

+ {{ end }} + + {{/* Social Links */}} + {{ $links := $author.links | default $block.content.links }} + {{ with $links }} +
+ {{ range . }} + {{ $icon := .icon | default "link" }} + {{ $url := .url }} + + + {{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }} + + {{ end }} +
+ {{ end }} + + {{/* CTA Buttons */}} + {{ with $block.content.cta_buttons }} + + {{ end }} +
+
+ + {{ else }} + {{/* Centered layout */}} + + {{/* Avatar */}} + {{ with $avatar }} +
+
+
+ {{ $author.title }} +
+ {{/* Status emoji badge */}} + {{ if $block.content.show_status }} + {{ with $author.status.icon }} + + {{ . | emojify }} + + {{ end }} + {{ end }} +
+
+ {{ end }} + + {{/* Greeting */}} + {{ with $block.content.greeting }} +

+ + {{ . }} + +

+ {{ end }} + + {{/* Name */}} +

+ {{ with $block.content.name_prefix }}{{ . }} {{ end }} + + {{ $author.title | default $block.content.title | default (i18n "developer" | default "Developer") }} + +

+ + {{/* Role/Tagline with optional typewriter */}} + {{ $role := $author.role | default $block.content.tagline }} + {{ if and $typewriter.enable $typewriter.strings }} +

+ {{ with $typewriter.prefix }}{{ . }} {{ end }} + +

+ {{ else if $role }} +

+ {{ $role | $page.RenderString }} +

+ {{ end }} + + {{/* Bio */}} + {{ with $author.bio | default $block.content.bio }} +

+ {{ . | $page.RenderString }} +

+ {{ end }} + + {{/* Social Links */}} + {{ $links := $author.links | default $block.content.links }} + {{ with $links }} +
+ {{ range . }} + {{ $icon := .icon | default "link" }} + {{ $url := .url }} + + + {{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-5 h-5\"") }} + + {{ end }} +
+ {{ end }} + + {{/* CTA Buttons */}} + {{ with $block.content.cta_buttons }} + + {{ end }} + + {{ end }} + + {{/* Scroll indicator - clicks to scroll to next section */}} + {{ if $block.content.show_scroll_indicator }} + {{ $scroll_target := $block.content.scroll_target | default "#projects" }} + +
+
+
+
+ {{ end }} +
+
diff --git a/modules/blox/blox/dev-hero/manifest.json b/modules/blox/blox/dev-hero/manifest.json new file mode 100644 index 00000000..99d0b926 --- /dev/null +++ b/modules/blox/blox/dev-hero/manifest.json @@ -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"] +} diff --git a/modules/blox/blox/dev-hero/schema.json b/modules/blox/blox/dev-hero/schema.json new file mode 100644 index 00000000..f0ed7a7f --- /dev/null +++ b/modules/blox/blox/dev-hero/schema.json @@ -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" + } + } + } + } + ] + } + ] +} diff --git a/modules/blox/blox/portfolio/README.md b/modules/blox/blox/portfolio/README.md new file mode 100644 index 00000000..05d98417 --- /dev/null +++ b/modules/blox/blox/portfolio/README.md @@ -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//` and use `/` diff --git a/modules/blox/blox/portfolio/block.html b/modules/blox/blox/portfolio/block.html new file mode 100644 index 00000000..5469a0c6 --- /dev/null +++ b/modules/blox/blox/portfolio/block.html @@ -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 }} + +
+ + {{/* Header */}} +
+ {{ with $block.content.title }} +

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

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

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

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

+ + {{ $item.Title }} + +

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

+ {{ . }} +

+ {{ else }} +

+ {{ $item.Summary | truncate 100 }} +

+ {{ end }} + + {{/* Tech Stack Tags - fixed 2-line height for consistency */}} + {{ with $item.Params.tech_stack }} +
+ {{ range first 4 . }} + + {{ . }} + + {{ end }} + {{ if gt (len .) 4 }} + + +{{ sub (len .) 4 }} + + {{ end }} +
+ {{ end }} + + {{/* Action Links - pushed to bottom with mt-auto */}} + {{ with $item.Params.links }} +
+ {{ range . }} + {{ $icon := "link" }} + {{ $label := .label | default .name }} + {{/* Fallback to i18n if no custom label provided */}} + {{ if not $label }} + {{ if eq .type "github" }} + {{ $icon = "brands/github" }} + {{ $label = i18n "portfolio_link_code" | default "Code" }} + {{ else if eq .type "live" }} + {{ $icon = "globe-alt" }} + {{ $label = i18n "portfolio_link_live" | default "Live" }} + {{ else if eq .type "demo" }} + {{ $icon = "play" }} + {{ $label = i18n "portfolio_link_demo" | default "Demo" }} + {{ else }} + {{ $label = i18n "portfolio_link_default" | default "Link" }} + {{ end }} + {{ else }} + {{/* Custom label provided, still infer icon from type if not specified */}} + {{ if eq .type "github" }} + {{ $icon = "brands/github" }} + {{ else if eq .type "live" }} + {{ $icon = "globe-alt" }} + {{ else if eq .type "demo" }} + {{ $icon = "play" }} + {{ end }} + {{ end }} + {{ $icon = .icon | default $icon }} + + {{ partial "functions/get_icon" (dict "name" $icon "attributes" "class=\"w-4 h-4\"") }} + {{ $label }} + + {{ end }} +
+ {{ end }} +
+
+ {{ end }} +
+ + {{/* View All Link */}} + {{/* Smart default: show link if there are more items than displayed, unless explicitly disabled */}} + {{ $show_archive_link := $block.content.archive.enable | default (gt $total_count $count) }} + {{ if $show_archive_link | and $archive_page }} + {{ $archive_link := "" }} + {{ if $block.content.archive.link }} + {{ $archive_link = $block.content.archive.link | relLangURL }} + {{ else }} + {{ $archive_link = $archive_page.RelPermalink }} + {{ end }} + + {{ $archive_text := $block.content.archive.text | default (i18n "portfolio_view_all" | default "View All Projects") }} + + + {{ end }} +
diff --git a/modules/blox/blox/portfolio/manifest.json b/modules/blox/blox/portfolio/manifest.json new file mode 100644 index 00000000..c3fbc7f8 --- /dev/null +++ b/modules/blox/blox/portfolio/manifest.json @@ -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"] +} diff --git a/modules/blox/blox/portfolio/schema.json b/modules/blox/blox/portfolio/schema.json new file mode 100644 index 00000000..73768449 --- /dev/null +++ b/modules/blox/blox/portfolio/schema.json @@ -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 + } + } + ] + } + ] +} diff --git a/modules/blox/blox/resume-experience/block.html b/modules/blox/blox/resume-experience/block.html index dde87179..cd526e33 100644 --- a/modules/blox/blox/resume-experience/block.html +++ b/modules/blox/blox/resume-experience/block.html @@ -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 }} -
- - - - - +
- -
+
{{ $experience := or $profile.experience $profile.work }} {{ if $experience }}
-

{{ i18n "experience" | default "Experience" }}

-
    - {{ range $idx, $key := $experience }} -
  1. - - {{ if .icon }} - {{ partial "functions/get_icon" (dict "name" .icon "attributes" "class='w-5 h-5'") }} - {{else}} - - {{end}} - -

    {{ .role | default .position }} - -

    - {{ .org | default .company_name }} - -
    {{.summary | $page.RenderString}}
    +

    {{ i18n "experience" | default "Experience" }}

    + + {{/* Timeline container */}} +
    + {{/* Gradient timeline line */}} +
    + + {{/* Experience items */}} +
    + {{ range $idx, $item := $experience }} + {{/* Card wrapper */}} +
    + + {{/* Timeline marker */}} +
    + {{/* Glowing dot */}} +
    +
    +
    + {{ 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}} + + {{end}} +
    +
    +
    + + {{/* Content card */}} +
    + + {{/* Header row */}} +
    +
    + {{/* Role/Position */}} +

    + {{ $item.role | default $item.position }} +

    + {{/* Company */}} +

    + {{ $item.org | default $item.company_name }} +

    +
    + + {{/* Date badge */}} +
    + + + + +
    +
    + + {{/* Description */}} + {{ with $item.summary }} +
    + {{ . | $page.RenderString }} +
    + {{ end }} + + {{/* Tags/Skills */}} + {{ with $item.tags }} +
    + {{ range . }} + + {{ . }} + + {{ end }} +
    + {{ end }} - {{ with .button }} - - {{ with .icon }} - {{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }} - {{ end }} - {{ .text | default "Learn more" }} - + {{/* Action button */}} + {{ with $item.button }} + + {{ end }} +
    +
    {{ end }} - -
  2. - {{ end }} -
+
+
{{ end }} {{ if $profile.education }}
-

{{ i18n "education" }}

-
    - {{ range $idx, $key := $profile.education }} -
  1. - - {{ if .icon }} - {{ partial "functions/get_icon" (dict "name" .icon "attributes" "class='w-5 h-5 text-primary-800 dark:text-primary-300'") }} - {{else}} - - {{end}} - -

    {{ .degree | default .area }} - -

    - {{ .institution }} -
  2. +
+
{{ end }} - +
+
{{ end }} diff --git a/modules/blox/blox/tech-stack/README.md b/modules/blox/blox/tech-stack/README.md new file mode 100644 index 00000000..40b97921 --- /dev/null +++ b/modules/blox/blox/tech-stack/README.md @@ -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 diff --git a/modules/blox/blox/tech-stack/block.html b/modules/blox/blox/tech-stack/block.html new file mode 100644 index 00000000..ad51ed0a --- /dev/null +++ b/modules/blox/blox/tech-stack/block.html @@ -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 }} + +
+ + {{/* Header */}} +
+ {{ with $block.content.title }} +

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

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

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

+ {{ end }} +
+ + {{/* Categories */}} +
+ {{ range $block.content.categories }} +
+ {{/* Category Title */}} + {{ with .name }} +

+ + {{ . }} + +

+ {{ end }} + + {{/* Tech Items - Centered flex layout */}} +
+ {{ 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 }} + +
+ {{/* Icon */}} +
+ {{ 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 }} +
+ {{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-10 h-10\"") }} +
+ {{ end }} + + {{/* Name */}} + + {{ $item.name }} + + + {{/* Level Indicator */}} + {{ if and $show_levels $level }} +
+
+
+
+ {{ $level }} +
+ {{ end }} +
+ + {{/* Hover glow effect */}} +
+
+ {{ end }} +
+
+ {{ end }} +
+ + {{/* Simple flat list style alternative */}} + {{ if eq $style "list" }} +
+ {{ range $block.content.categories }} + {{ range $i, $tech := .items }} +
+ {{ with $tech.icon }} + + {{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"w-5 h-5\"") }} + + {{ end }} + {{ $tech.name }} +
+ {{ end }} + {{ end }} +
+ {{ end }} +
diff --git a/modules/blox/blox/tech-stack/manifest.json b/modules/blox/blox/tech-stack/manifest.json new file mode 100644 index 00000000..2a0ee935 --- /dev/null +++ b/modules/blox/blox/tech-stack/manifest.json @@ -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"] +} diff --git a/modules/blox/blox/tech-stack/schema.json b/modules/blox/blox/tech-stack/schema.json new file mode 100644 index 00000000..6d0f0f66 --- /dev/null +++ b/modules/blox/blox/tech-stack/schema.json @@ -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 + } + } + ] + } + ] +} diff --git a/modules/blox/i18n/bn.yaml b/modules/blox/i18n/bn.yaml index 73a26c93..a8d96566 100644 --- a/modules/blox/i18n/bn.yaml +++ b/modules/blox/i18n/bn.yaml @@ -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: সমস্ত প্রকল্প দেখুন diff --git a/modules/blox/i18n/de.yaml b/modules/blox/i18n/de.yaml index 337b0baa..82f7fee1 100644 --- a/modules/blox/i18n/de.yaml +++ b/modules/blox/i18n/de.yaml @@ -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 diff --git a/modules/blox/i18n/en.yaml b/modules/blox/i18n/en.yaml index dca4e4ef..dc111094 100644 --- a/modules/blox/i18n/en.yaml +++ b/modules/blox/i18n/en.yaml @@ -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 diff --git a/modules/blox/i18n/nl.yaml b/modules/blox/i18n/nl.yaml index a8450d33..401d83b5 100644 --- a/modules/blox/i18n/nl.yaml +++ b/modules/blox/i18n/nl.yaml @@ -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 diff --git a/modules/blox/i18n/zh.yaml b/modules/blox/i18n/zh.yaml index 44c27111..039a0cce 100644 --- a/modules/blox/i18n/zh.yaml +++ b/modules/blox/i18n/zh.yaml @@ -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: 查看所有项目 diff --git a/modules/blox/layouts/_partials/blox/contact-info/config.html b/modules/blox/layouts/_partials/blox/contact-info/config.html new file mode 100644 index 00000000..f428619a --- /dev/null +++ b/modules/blox/layouts/_partials/blox/contact-info/config.html @@ -0,0 +1,7 @@ +{{/* Hugo Blox: Contact Info - config */}} +{{/* Purpose: pre-declare dependencies before libraries load */}} + +{{ $page := .wcPage }} + +{{/* Contact block uses Alpine for copy-to-clipboard + small interactions */}} +{{ $page.Store.Set "has_alpine" true }} diff --git a/modules/blox/layouts/_partials/blox/dev-hero/config.html b/modules/blox/layouts/_partials/blox/dev-hero/config.html new file mode 100644 index 00000000..6a0b968a --- /dev/null +++ b/modules/blox/layouts/_partials/blox/dev-hero/config.html @@ -0,0 +1,13 @@ +{{/* Hugo Blox: Dev Hero - config */}} +{{/* Purpose: pre-declare dependencies before 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 }} diff --git a/modules/blox/layouts/_partials/blox/portfolio/config.html b/modules/blox/layouts/_partials/blox/portfolio/config.html new file mode 100644 index 00000000..1f6eab8a --- /dev/null +++ b/modules/blox/layouts/_partials/blox/portfolio/config.html @@ -0,0 +1,13 @@ +{{/* Hugo Blox: Portfolio - config */}} +{{/* Purpose: pre-declare dependencies before 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 }} diff --git a/modules/blox/layouts/_partials/blox/tech-stack/config.html b/modules/blox/layouts/_partials/blox/tech-stack/config.html new file mode 100644 index 00000000..eb936bfd --- /dev/null +++ b/modules/blox/layouts/_partials/blox/tech-stack/config.html @@ -0,0 +1,10 @@ +{{/* Hugo Blox: Tech Stack - config */}} +{{/* Purpose: pre-declare dependencies before 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 }} diff --git a/modules/blox/layouts/_partials/libraries.html b/modules/blox/layouts/_partials/libraries.html index 92eb3fa2..bb706bef 100644 --- a/modules/blox/layouts/_partials/libraries.html +++ b/modules/blox/layouts/_partials/libraries.html @@ -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 -}} + +{{ 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 }} {{ $alpine_js := resources.Get "dist/lib/alpinejs/cdn.min.js" }} {{ $alpine_js = $alpine_js | resources.Fingerprint "sha256" }} diff --git a/modules/blox/layouts/_partials/views/card.html b/modules/blox/layouts/_partials/views/card.html index f7e28909..51169347 100644 --- a/modules/blox/layouts/_partials/views/card.html +++ b/modules/blox/layouts/_partials/views/card.html @@ -24,7 +24,7 @@ {{ $event_dates = partial "functions/get_event_dates" $item }} {{ end }} -
+
{{ with $item.Params.content_meta }} @@ -36,7 +36,17 @@ {{ end }} {{ end }} {{ with $resource }} - {{ if ne .MediaType.SubType "gif" }} + {{ if strings.Contains .MediaType.SubType "svg" }} + + {{ $item.Title | plainify }} featured image + + {{ else if ne .MediaType.SubType "gif" }} {{ $original_image := "" }} {{ if $fill_image }} {{ $original_image = .Fill (printf "800x450 %s" $anchor) }} @@ -75,11 +85,11 @@ {{ end }} {{ else }} - + -
+
- +
@@ -97,7 +107,7 @@ {{ if or .content_type .difficulty }}
{{ with .content_type }} - + {{ . }} {{ end }} @@ -120,7 +130,7 @@
{{ end }} -

+

{{ $item.Title }} {{ if $item.Params.external_link }} @@ -207,7 +217,7 @@ {{ if $item.Params.ai_insights }} -
+
diff --git a/modules/blox/layouts/events/page.html b/modules/blox/layouts/events/page.html index ad177eec..4a5818be 100644 --- a/modules/blox/layouts/events/page.html +++ b/modules/blox/layouts/events/page.html @@ -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. */}} +
+
+ {{ with $image_params.alt_text }}{{.}}{{ end }} + {{ with $image_params.caption }}{{ . | markdownify | emojify }}{{ end }} +
+
+ {{ else if ne $featured.MediaType.SubType "gif" }} {{/* Pre-fit image to placement constraints, then apply responsive processing */}} {{ $fitted_image := $featured }} {{ if eq $placement 2}} diff --git a/modules/blox/layouts/single.html b/modules/blox/layouts/single.html index d317e5a5..fab49d43 100644 --- a/modules/blox/layouts/single.html +++ b/modules/blox/layouts/single.html @@ -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. */}} +
+
+ {{ with $image_params.alt_text }}{{.}}{{ end }} + {{ with $image_params.caption }}{{ . | markdownify | emojify }}{{ end }} +
+
+ {{ else if ne $featured.MediaType.SubType "gif" }} {{/* Pre-fit image to placement constraints, then apply responsive processing */}} {{ $fitted_image := $featured }} {{ if eq $placement 2}} diff --git a/templates/dev-portfolio/.devcontainer/devcontainer.json b/templates/dev-portfolio/.devcontainer/devcontainer.json new file mode 100644 index 00000000..cba4ea48 --- /dev/null +++ b/templates/dev-portfolio/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.154.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", + "customizations": { + "vscode": { + "extensions": [ + "hugoblox.hugoblox-studio" + ] + } + }, + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", + "forwardPorts": [ + 1313 + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/dev-portfolio/.github/FUNDING.yml b/templates/dev-portfolio/.github/FUNDING.yml new file mode 100644 index 00000000..2ed14d29 --- /dev/null +++ b/templates/dev-portfolio/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: gcushen +custom: https://hugoblox.com/sponsor/ diff --git a/templates/dev-portfolio/.github/preview.png b/templates/dev-portfolio/.github/preview.png new file mode 100644 index 00000000..36a29e9f Binary files /dev/null and b/templates/dev-portfolio/.github/preview.png differ diff --git a/templates/dev-portfolio/.github/workflows/deploy.yml b/templates/dev-portfolio/.github/workflows/deploy.yml new file mode 100644 index 00000000..fc593765 --- /dev/null +++ b/templates/dev-portfolio/.github/workflows/deploy.yml @@ -0,0 +1,125 @@ +name: Deploy website to GitHub Pages + +env: + NODE_VERSION: '20' + +on: + # Trigger the workflow every time you push to the `main` branch + push: + branches: ['main'] + # Allows you to run this workflow manually from the Actions tab on GitHub + workflow_dispatch: + +# Provide permission to clone the repo and deploy it to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: 'pages' + cancel-in-progress: false + +jobs: + # Build website + build: + if: github.repository_owner != 'HugoBlox' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Fetch history for Hugo's .GitInfo and .Lastmod + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + if: hashFiles('package.json') != '' + uses: pnpm/action-setup@v4 + + - name: Get Hugo Version + id: hugo-version + run: | + VERSION=$(grep "hugo_version" hugoblox.yaml | awk '{print $2}' | tr -d "'\"") + echo "HUGO_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Install dependencies + run: | + # Install Tailwind CLI if package.json exists + if [ -f "package.json" ]; then + echo "Installing Tailwind dependencies..." + pnpm install --no-frozen-lockfile || npm install + fi + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: ${{ env.HUGO_VERSION }} + extended: true + + # Cache dependencies (Go modules, node_modules) - stable, rarely changes + - uses: actions/cache@v4 + with: + path: | + /tmp/hugo_cache_runner/ + node_modules/ + modules/*/node_modules/ + key: ${{ runner.os }}-hugo-deps-${{ hashFiles('**/go.mod', '**/package-lock.json', + '**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-hugo-deps- + + # Cache Hugo resources (processed images, CSS) - invalidates only when assets/config change + - uses: actions/cache@v4 + with: + path: resources/ + key: ${{ runner.os }}-hugo-resources-${{ hashFiles('assets/**/*', 'config/**/*', + 'hugo.yaml', 'package.json') }} + restore-keys: | + ${{ runner.os }}-hugo-resources- + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Build with Hugo + env: + HUGO_ENVIRONMENT: production + HUGO_BLOX_LICENSE: ${{ secrets.HUGO_BLOX_LICENSE }} + run: | + echo "Hugo Cache Dir: $(hugo config | grep cachedir)" + hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" + + - name: Generate Pagefind search index (if applicable) + run: | + # Check if site uses Pagefind search + if [ -f "package.json" ] && grep -q "pagefind" package.json; then + pnpm run pagefind || npx pagefind --site "public" + fi + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: ./public + + # Deploy website to GitHub Pages hosting + deploy: + if: github.repository_owner != 'HugoBlox' + needs: build + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/templates/dev-portfolio/.gitignore b/templates/dev-portfolio/.gitignore new file mode 100644 index 00000000..5c572926 --- /dev/null +++ b/templates/dev-portfolio/.gitignore @@ -0,0 +1,69 @@ +# ============================================================================ +# Hugo Site .gitignore +# ============================================================================ + +# ============================================================================ +# Hugo Build Artifacts +# ============================================================================ + +# Generated site output +public/ + +# Hugo resources (processed assets) +resources/ + +# Hugo build lock +.hugo_build.lock + +# Hugo stats +hugo_stats.json + +# Auto-generated JS config +**/assets/jsconfig.json + +# ============================================================================ +# Node.js & Package Managers +# ============================================================================ + +# Dependencies +node_modules/ + +# ============================================================================ +# Environment & Configuration +# ============================================================================ + +# Environment variables +.env +.env.local +.env.*.local + +# ============================================================================ +# Generated Content +# ============================================================================ + +# Pagefind search index +pagefind/ +static/pagefind/ + +# ============================================================================ +# Operating System +# ============================================================================ + +# macOS +.DS_Store +.DS_Store? +._* + +# Windows +Thumbs.db +Desktop.ini + +# Linux +*~ + +# ============================================================================ +# Logs & Temporary Files +# ============================================================================ + +*.log +npm-debug.log* diff --git a/templates/dev-portfolio/LICENSE.md b/templates/dev-portfolio/LICENSE.md new file mode 100644 index 00000000..9da70cd3 --- /dev/null +++ b/templates/dev-portfolio/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present George Cushen (https://neuralgeo.com/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/dev-portfolio/README.md b/templates/dev-portfolio/README.md new file mode 100644 index 00000000..3d5d6896 --- /dev/null +++ b/templates/dev-portfolio/README.md @@ -0,0 +1,124 @@ +# [Developer Portfolio (Free)](https://hugoblox.com/templates/dev-portfolio/start?utm_source=github&utm_medium=readme) + +[![Screenshot](.github/preview.png)](https://hugoblox.com/templates/dev-portfolio/start?utm_source=github&utm_medium=readme) + +

The Dev Portfolio That Gets You Hired

+ +

+ Built for uni students, STEM grads, and software engineers.
+ Showcase real projects, ship a clean blog, and stand out with a modern dark-first design — without a fragile “black box” React app. +

+ +

+ 🚀 Deploy in 60s (Free) +  •  + Live Demo +  •  + HugoBlox Studio (Visual Editor) +

+ + + + +

+ Discord + GitHub Stars + Follow on X +

+ +--- + +## Why this template? + +Most portfolios look the same — and most “quick” site builders produce slow sites you can’t maintain. + +- **⚡ Fast by default**: Hugo + Tailwind 4 + Hugo Modules → great Lighthouse scores without JS bloat. +- **🧠 AI-ready content**: Your projects and posts live in clean Markdown/YAML — readable by humans and LLMs. +- **🎯 Job-hunting optimized**: Projects, tech stack, experience timeline, and a blog layout that looks credible. +- **🌙 Dark-first (but light-mode supported)**: Looks premium in dark mode, still works perfectly in light mode. +- **🧩 Block-based**: Reorder/replace sections without rewriting templates. + +--- + +## Who it’s for + +- **Students & grads** applying to internships / MSc / PhD / junior roles +- **STEM professionals** (research, data, ML, bio, engineering) showcasing projects and writing +- **Software engineers** building a portfolio that feels modern in 2026 + +--- + +## What you get (Free) + +- **Developer hero** with modern gradients + optional typewriter +- **Filterable portfolio** (tags + smooth transitions) +- **Tech stack** grid with icons +- **Experience + education timeline** +- **Blog section** (cards + featured images) +- **Contact section** with social links + copy email + +--- + +## Get started (recommended) + +### Option 1 — Online deployer (fastest) + +Deploy this template in ~60 seconds: + +- [Deploy in browser →](https://hugoblox.com/templates/dev-portfolio/start?utm_source=github&utm_medium=readme) + +### Option 2 — HugoBlox Studio (visual editor in VS Code) + +- [Studio guide →](https://docs.hugoblox.com/guides/studio/?utm_source=github&utm_medium=readme) +- [Studio AI automation plans (Free/Plus/Pro) →](https://hugoblox.com/pricing/?utm_source=github&utm_medium=readme) + +### Option 3 — HugoBlox CLI (developer workflow) + +Install: + +```bash +npm install -g hugoblox@latest +``` + +Create a site: + +```bash +hugoblox create site --template dev-portfolio +``` + +[CLI docs →](https://docs.hugoblox.com/start/cli/?utm_source=github&utm_medium=readme) + +--- + +## Customize in 10 minutes + +- **Your profile**: `data/authors/me.yaml` +- **Homepage sections**: `content/_index.md` +- **Projects**: `content/projects//index.md` +- **Blog posts**: `content/blog//index.md` +- **Theme + identity**: `config/_default/params.yaml` + +--- + +## Want the Pro version? 💎 + +If you want a version that feels meaningfully more premium (and helps fund open source + research): + +- [Premium template →](https://hugoblox.com/templates/dev-portfolio-pro/start?utm_source=github&utm_medium=readme) +- [HugoBlox Pro / Studio →](https://hugoblox.com/pricing?utm_source=github&utm_medium=readme) + +--- + +## Community & support + +- **Docs**: [docs.hugoblox.com →](https://docs.hugoblox.com/?utm_source=github&utm_medium=readme) +- **Discord**: [Join Discord →](https://discord.gg/z8wNYzb) +- **Issues**: [Open an issue →](https://github.com/HugoBlox/kit/issues) + +--- + +MIT © 2016–Present [George Cushen](https://neuralgeo.com/?utm_source=github&utm_medium=readme) diff --git a/templates/dev-portfolio/assets/media/authors/me.png b/templates/dev-portfolio/assets/media/authors/me.png new file mode 100644 index 00000000..5ebc1f3b Binary files /dev/null and b/templates/dev-portfolio/assets/media/authors/me.png differ diff --git a/templates/dev-portfolio/assets/media/icons/custom/.gitkeep b/templates/dev-portfolio/assets/media/icons/custom/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/templates/dev-portfolio/config/_default/hugo.yaml b/templates/dev-portfolio/config/_default/hugo.yaml new file mode 100644 index 00000000..16c633f1 --- /dev/null +++ b/templates/dev-portfolio/config/_default/hugo.yaml @@ -0,0 +1,55 @@ +# Configuration of Hugo +# Guide: https://docs.hugoblox.com/tutorial/ +# Hugo Documentation: https://gohugo.io/getting-started/configuration/#all-configuration-settings +# This file is formatted using YAML syntax - learn more at https://learnxinyminutes.com/docs/yaml/ + +# Website name (set in params.yaml under hugoblox.branding.name) +title: '' +# Website URL +baseURL: 'https://example.com/' + +############################ +## LANGUAGE +############################ + +defaultContentLanguage: en +hasCJKLanguage: false +defaultContentLanguageInSubdir: false +removePathAccents: true + +############################ +## ADVANCED +############################ + +build: + writeStats: true +enableGitInfo: false +summaryLength: 30 +pagination: + pagerSize: 10 +enableEmoji: true +enableRobotsTXT: true +footnotereturnlinkcontents: ^ +ignoreFiles: [".ipynb_checkpoints$", "\\.Rmd$", "\\.Rmarkdown$", "_cache$"] +disableAliases: true +outputs: + home: [HTML, RSS, headers, redirects, backlinks] + section: [HTML, RSS] +imaging: + resampleFilter: lanczos + quality: 90 + anchor: smart + hint: picture +timeout: 600000 +taxonomies: + author: authors + tag: tags + publication_type: publication_types +markup: + _merge: deep + highlight: + lineNos: false +security: + _merge: deep +sitemap: + _merge: deep diff --git a/templates/dev-portfolio/config/_default/languages.yaml b/templates/dev-portfolio/config/_default/languages.yaml new file mode 100644 index 00000000..88fa733d --- /dev/null +++ b/templates/dev-portfolio/config/_default/languages.yaml @@ -0,0 +1,22 @@ +# Languages +# Create a section for each of your site's languages. +# Documentation: https://docs.hugoblox.com/reference/language/ + +# Default language +en: + languageCode: en-us + # Uncomment for multi-lingual sites, and move English content into `en` sub-folder. + #contentDir: content/en + +# Uncomment the lines below to configure your website in a second language. +#zh: +# languageCode: zh-Hans +# contentDir: content/zh +# title: Chinese website title... +# params: +# description: Site description in Chinese... +# menu: +# main: +# - name: 传 +# url: '#about' +# weight: 1 diff --git a/templates/dev-portfolio/config/_default/menus.yaml b/templates/dev-portfolio/config/_default/menus.yaml new file mode 100644 index 00000000..f0480744 --- /dev/null +++ b/templates/dev-portfolio/config/_default/menus.yaml @@ -0,0 +1,20 @@ +# Navigation Links +# To link a homepage section, use a hash `#` followed by the section ID. +# The weight parameter defines the order that the links appear. + +main: + - name: Projects + url: /#projects + weight: 10 + - name: Skills + url: /#skills + weight: 20 + - name: Experience + url: /#experience + weight: 30 + - name: Blog + url: /#blog + weight: 40 + - name: Contact + url: /#contact + weight: 50 diff --git a/templates/dev-portfolio/config/_default/module.yaml b/templates/dev-portfolio/config/_default/module.yaml new file mode 100644 index 00000000..4101cd91 --- /dev/null +++ b/templates/dev-portfolio/config/_default/module.yaml @@ -0,0 +1,26 @@ +############################ +## HUGO MODULES +## Install or uninstall themes and plugins here. +## Docs: https://gohugo.io/hugo-modules/ +############################ + +imports: + - path: github.com/HugoBlox/kit/modules/integrations/netlify + - path: github.com/HugoBlox/kit/modules/blox + - path: github.com/HugoBlox/kit/modules/slides + +# Install any Hugo Blox within the `hugo-blox/blox/` folder +mounts: + - source: hugo-blox/blox/community + target: layouts/_partials/blox/community/ + includeFiles: '**.html' + - source: hugo-blox/blox/all-access + target: layouts/_partials/blox/ + includeFiles: '**.html' + - source: hugo-blox/blox + target: assets/dist/community/blox/ + includeFiles: '**.css' + - source: layouts + target: layouts + - source: assets + target: assets diff --git a/templates/dev-portfolio/config/_default/params.yaml b/templates/dev-portfolio/config/_default/params.yaml new file mode 100644 index 00000000..66ee890f --- /dev/null +++ b/templates/dev-portfolio/config/_default/params.yaml @@ -0,0 +1,291 @@ +# ══════════════════════════════════════════════════════════════════════════════ +# HUGO BLOX CONFIGURATION +# Schema: hugoblox.com/schema/v2 +# Tutorial: https://docs.hugoblox.com/tutorial/ +# Documentation: https://docs.hugoblox.com/ +# ══════════════════════════════════════════════════════════════════════════════ +hugoblox: + schema: "2.0" + + # ──────────────────────────────────────────────────────────────────────────── + # IDENTITY + # Site-level branding (for people/profiles, use the author system in content/authors/) + # ──────────────────────────────────────────────────────────────────────────── + identity: + # Site name - shown in navbar, footer, copyright, and browser title by default + name: "Your Name" + # Legal entity for copyright notices (defaults to name if not set) + organization: "" + # Schema.org type for structured data + type: person # person | organization | local_business + # Short tagline displayed on your site + tagline: "Full-Stack Developer" + # Site meta description for search engines and social sharing + description: "Modern developer portfolio showcasing web development projects, technical blog posts, and open source contributions. Built with Hugo Blox." + # Social accounts (used for meta tags like Twitter Cards) + social: + twitter: "yourusername" # Twitter/X handle for Twitter Cards (without @) + + # ──────────────────────────────────────────────────────────────────────────── + # THEME + # Color and visual design system + # ──────────────────────────────────────────────────────────────────────────── + theme: + # Mode: 'light', 'dark', or 'system' (follows OS preference) + mode: dark + # Theme pack: name from data/themes/ or { light: "x", dark: "y" } for mix-and-match + pack: "default" + # Optional: Override theme pack colors + colors: + primary: "" # Tailwind palette name or hex (e.g., "indigo" or "#6366f1") + secondary: "" + neutral: "" # Affects gray utilities (text-gray-*, bg-gray-*) + # Optional: Mode-specific color overrides + colors_light: {} + colors_dark: {} + # Optional: Semantic surface color overrides + surfaces: + background: "" + foreground: "" + header: + background: "" + foreground: "" + footer: + background: "" + foreground: "" + + # ──────────────────────────────────────────────────────────────────────────── + # TYPOGRAPHY + # Font and text sizing + # ──────────────────────────────────────────────────────────────────────────── + typography: + # Font: 'sans' (Inter), 'serif' (Roboto Slab), or 'native' (system fonts) + font: "sans" + # Size: 'sm', 'md', 'lg', or 'xl' (affects base font size and scale) + size: "md" + + # ──────────────────────────────────────────────────────────────────────────── + # LAYOUT + # Spacing and shape tokens + # ──────────────────────────────────────────────────────────────────────────── + layout: + # Border radius: 'none', 'sm', 'md', 'lg', or 'full' + radius: "md" + # Spacing density: 'compact', 'comfortable', or 'spacious' + spacing: "comfortable" + # Avatar shape in author profiles + avatar_shape: circle # circle | square | rounded + + # ──────────────────────────────────────────────────────────────────────────── + # HEADER + # Site header and navigation bar + # ──────────────────────────────────────────────────────────────────────────── + header: + enable: true + # Header style variant + style: navbar # navbar | navbar-simple | minimal + # Stick to top on scroll + sticky: true + # Menu alignment + align: center # left | center | right + # Feature toggles + search: true + theme_toggle: true + theme_picker: true + language_switcher: true + # Call-to-action button + cta: + enable: false + text: "" + url: "" + + # ──────────────────────────────────────────────────────────────────────────── + # FOOTER + # Site footer + # ──────────────────────────────────────────────────────────────────────────── + footer: + # Footer style variant + style: minimal # minimal | columns | centered + language_switcher: true + # Custom footer text (HTML supported) + text: "" + + # ──────────────────────────────────────────────────────────────────────────── + # COPYRIGHT + # Legal notices and licensing + # ──────────────────────────────────────────────────────────────────────────── + copyright: + # Copyright notice - supports {year}, {name}, {license} placeholders + notice: "© {year} {name}. This work is licensed under {license}" + license: + type: cc # cc | custom + allow_derivatives: false + share_alike: true + allow_commercial: false + + # ──────────────────────────────────────────────────────────────────────────── + # SEO + # Search engine optimization - overrides and technical settings + # ──────────────────────────────────────────────────────────────────────────── + seo: + # Browser tab title override (defaults to identity.name if not set) + title: "" + # For local_business identity type only + location: + address: + street: "" + city: "" + region: "" + postal_code: "" + country: "" + coordinates: + latitude: null + longitude: null + phone: "" + # AI/LLM crawler guidance (llms.txt) + ai: + allow: + - / + disallow: [] + note: "Guidance for AI crawlers - customize to match your public content." + contact: "" + + # ──────────────────────────────────────────────────────────────────────────── + # CONTENT + # Content rendering and behavior + # ──────────────────────────────────────────────────────────────────────────── + content: + # Math rendering + math: + enable: false + # Table of contents + toc: + enable: true + # Reading time estimates + reading_time: + enable: true + # Academic citations (disabled for developer portfolio) + citations: + style: apa # apa | mla | chicago | ieee + + # ──────────────────────────────────────────────────────────────────────────── + # SEARCH + # Site search + # ──────────────────────────────────────────────────────────────────────────── + search: + enable: true + # Trending/suggested search terms + suggestions: [] + # Quick action shortcuts in search modal + quick_actions: [] + + # ──────────────────────────────────────────────────────────────────────────── + # COMMENTS + # User comments system + # ──────────────────────────────────────────────────────────────────────────── + comments: + enable: false + provider: "" # giscus | disqus + giscus: + repo: "" + repo_id: "" + category: "" + category_id: "" + disqus: + shortname: "" + + # ──────────────────────────────────────────────────────────────────────────── + # ANALYTICS + # Traffic and behavior analytics + # ──────────────────────────────────────────────────────────────────────────── + analytics: + google: + measurement_id: "" # Google Analytics 4: G-XXXXXXXXXX + google_tag_manager: + container_id: "" # GTM-XXXXXXX + plausible: + domain: "" + fathom: + site_id: "" + pirsch: + site_id: "" + clarity: + project_id: "" + baidu: + site_id: "" + + # ──────────────────────────────────────────────────────────────────────────── + # VERIFICATION + # Search engine site verification codes + # ──────────────────────────────────────────────────────────────────────────── + verification: + google: "" + bing: "" + baidu: "" + yandex: "" + pinterest: "" + naver: "" + + # ──────────────────────────────────────────────────────────────────────────── + # REPOSITORY + # Source code repository for "Edit this page" links + # ──────────────────────────────────────────────────────────────────────────── + repository: + enable: false + url: "" # e.g., https://github.com/username/repo + branch: main + content_dir: content + + # ──────────────────────────────────────────────────────────────────────────── + # LOCALE + # Regional formatting preferences + # ──────────────────────────────────────────────────────────────────────────── + locale: + date_format: "Jan 2, 2006" # Go time format + time_format: "3:04 PM" + address_format: en-us + + # ──────────────────────────────────────────────────────────────────────────── + # SECURITY + # Security headers and policies (requires Netlify integration) + # ──────────────────────────────────────────────────────────────────────────── + security: + csp: + policy: "" + report_only: false + frame_options: sameorigin # deny | sameorigin | allow - sameorigin allows slides embedding + permissions_policy: "" + + # ──────────────────────────────────────────────────────────────────────────── + # PRIVACY + # Privacy and compliance features + # ──────────────────────────────────────────────────────────────────────────── + privacy: + enable: false + anonymize_analytics: true + + # ──────────────────────────────────────────────────────────────────────────── + # DEBUG + # Development and debugging (hidden in production) + # ──────────────────────────────────────────────────────────────────────────── + debug: + enable: false + hud: + position: bottom-left # top-left | top-right | bottom-left | bottom-right + opacity: 1.0 + export_logs: true + + # ──────────────────────────────────────────────────────────────────────────── + # PRO + # Pro subscription features and affiliate program + # ──────────────────────────────────────────────────────────────────────────── + pro: + # Hide "Powered by HugoBlox" attribution + # Requires: Pro subscription to support HugoBlox via alternative means + # Note: Even with Pro, you can keep this false to support open source! + # Get Pro: https://hugoblox.com/pro + hide_attribution: false + + # Affiliate referral code for rewards program + # Join affiliate program: https://hugoblox.com/affiliates + affiliate_code: "" diff --git a/templates/dev-portfolio/content/_index.md b/templates/dev-portfolio/content/_index.md new file mode 100644 index 00000000..a1d5b2e2 --- /dev/null +++ b/templates/dev-portfolio/content/_index.md @@ -0,0 +1,259 @@ +--- +# Leave the homepage title empty to use the site title +title: '' +summary: '' +date: 2026-01-05 +type: landing + +design: + # Default section spacing + spacing: '0' + +sections: + # Developer Hero - Gradient background with name, role, social, and CTAs + - block: dev-hero + id: hero + content: + username: me + greeting: "Hi, I'm" + show_status: true + show_scroll_indicator: true + typewriter: + enable: true + prefix: "I build" + strings: + - "full-stack web apps" + - "scalable APIs" + - "beautiful UIs" + - "open source tools" + type_speed: 70 + delete_speed: 40 + pause_time: 2500 + 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 + animations: true + background: + color: + light: "#fafafa" + dark: "#0a0a0f" + spacing: + padding: ["6rem", "0", "4rem", "0"] + + # Filterable Portfolio - Alpine.js powered project filtering + - 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 link auto-shown if more projects exist than 'count' above + # archive: + # enable: false # Set to false to explicitly hide + # text: "Browse All" # Customize text + # link: "/work/" # Custom URL + design: + columns: 3 + background: + color: + light: "#ffffff" + dark: "#0d0d12" + spacing: + padding: ["4rem", "0", "4rem", "0"] + + # Visual Tech Stack - Icons organized by category + - 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 + - name: JavaScript + icon: devicon/javascript + - name: Python + icon: devicon/python + - name: Go + icon: devicon/go + - name: Frontend + items: + - name: React + icon: devicon/react + - name: Next.js + icon: devicon/nextjs + - name: Tailwind CSS + icon: devicon/tailwindcss + - name: Alpine.js + icon: devicon/alpinejs + - name: Backend + items: + - name: Node.js + icon: devicon/nodejs + - name: Express + icon: devicon/express + - name: PostgreSQL + icon: devicon/postgresql + - name: Redis + icon: devicon/redis + - name: DevOps + items: + - name: Docker + icon: devicon/docker + - name: AWS + icon: devicon/amazonwebservices-wordmark + - name: GitHub Actions + icon: brands/github + - name: Vercel + icon: devicon/vercel + design: + style: grid + show_levels: false + background: + color: + light: "#f5f5f5" + dark: "#08080c" + spacing: + padding: ["4rem", "0", "4rem", "0"] + + # Experience Timeline + - block: resume-experience + id: experience + content: + title: Experience + date_format: Jan 2006 + items: + - title: Senior Software Engineer + company: Tech Corp + company_url: '' + company_logo: '' + location: San Francisco, CA + date_start: '2023-01-01' + date_end: '' + description: |2- + * Lead development of microservices architecture serving 1M+ users + * Improved API response time by 40% through optimization + * Mentored team of 5 junior developers + * Tech stack: React, Node.js, PostgreSQL, AWS + - title: Full-Stack Developer + company: Startup Inc + company_url: '' + company_logo: '' + location: Remote + date_start: '2021-06-01' + date_end: '2022-12-31' + description: |2- + * Built and deployed 3 production applications from scratch + * Implemented CI/CD pipeline reducing deployment time by 60% + * Collaborated with design team on UI/UX improvements + * Tech stack: Next.js, Express, MongoDB, Docker + - title: Junior Developer + company: Web Agency + company_url: '' + company_logo: '' + location: New York, NY + date_start: '2020-01-01' + date_end: '2021-05-31' + description: |2- + * Developed client websites using modern web technologies + * Maintained and updated legacy codebases + * Participated in code reviews and agile ceremonies + * Tech stack: React, WordPress, PHP, MySQL + design: + columns: '1' + background: + color: + light: "#ffffff" + dark: "#0d0d12" + spacing: + padding: ["4rem", "0", "4rem", "0"] + + # Recent Blog Posts + - block: collection + id: blog + content: + title: Recent Posts + subtitle: 'Thoughts on web development, tech, and more' + text: '' + filters: + folders: + - blog + exclude_featured: false + count: 3 + order: desc + design: + view: card + columns: 3 + background: + color: + light: "#f5f5f5" + dark: "#08080c" + spacing: + padding: ["4rem", "0", "4rem", "0"] + + # Contact Section + - block: contact-info + id: contact + content: + title: Get In Touch + subtitle: "Let's build something amazing together" + text: |- + I'm always interested in hearing about new projects and opportunities. + Whether you're looking to hire, collaborate, or just want to say hi, feel free to reach out! + email: alex@example.com + autolink: true + design: + columns: '1' + background: + color: + light: "#ffffff" + dark: "#0d0d12" + spacing: + padding: ["4rem", "0", "4rem", "0"] + + # CTA Card + - block: cta-card + content: + title: "Open to Opportunities" + text: |- + I'm currently looking for **senior engineering** or **tech lead** roles. + + Let's connect and discuss how I can help your team. + button: + text: 'Download Resume' + url: uploads/resume.pdf + new_tab: true + design: + card: + # Light mode: soft pastel theme gradient | Dark mode: rich deep gradient + css_class: 'bg-gradient-to-br from-primary-200 via-primary-100 to-secondary-200 dark:from-primary-600 dark:via-primary-700 dark:to-secondary-700' + text_color: dark + background: + color: + light: "#f5f5f5" + dark: "#08080c" + spacing: + padding: ["4rem", "0", "6rem", "0"] +--- diff --git a/templates/dev-portfolio/content/authors/_index.md b/templates/dev-portfolio/content/authors/_index.md new file mode 100644 index 00000000..853f3eda --- /dev/null +++ b/templates/dev-portfolio/content/authors/_index.md @@ -0,0 +1,9 @@ +--- +# To publish author profile pages, remove all the `build` and `cascade` settings below. +build: + render: never +cascade: + build: + render: never + list: always +--- diff --git a/templates/dev-portfolio/content/blog/_index.md b/templates/dev-portfolio/content/blog/_index.md new file mode 100644 index 00000000..c067352b --- /dev/null +++ b/templates/dev-portfolio/content/blog/_index.md @@ -0,0 +1,4 @@ +--- +title: Blog +view: article-grid +--- diff --git a/templates/dev-portfolio/content/blog/building-rest-api/featured.png b/templates/dev-portfolio/content/blog/building-rest-api/featured.png new file mode 100644 index 00000000..6e596845 Binary files /dev/null and b/templates/dev-portfolio/content/blog/building-rest-api/featured.png differ diff --git a/templates/dev-portfolio/content/blog/building-rest-api/index.md b/templates/dev-portfolio/content/blog/building-rest-api/index.md new file mode 100644 index 00000000..dc56495f --- /dev/null +++ b/templates/dev-portfolio/content/blog/building-rest-api/index.md @@ -0,0 +1,455 @@ +--- +title: "Building a Production-Ready REST API with Node.js and Express" +date: 2024-12-10 +summary: "A comprehensive guide to building scalable, secure REST APIs with proper error handling, validation, and documentation" +tags: + - Node.js + - Express + - REST API + - Backend + - Tutorial +authors: + - me +featured: true +--- + +Building a REST API seems straightforward until you need to handle authentication, validation, error handling, and documentation. This guide covers best practices for production-ready APIs. + +## Table of Contents + +1. [Project Setup](#project-setup) +2. [Folder Structure](#folder-structure) +3. [Database Integration](#database) +4. [Authentication & Authorization](#auth) +5. [Error Handling](#errors) +6. [Validation](#validation) +7. [API Documentation](#docs) +8. [Testing](#testing) + +## Project Setup {#project-setup} + +Start with a solid foundation: + +```bash +mkdir my-api && cd my-api +npm init -y +npm install express dotenv cors helmet compression +npm install -D typescript @types/node @types/express ts-node-dev +``` + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} +``` + +## Folder Structure {#folder-structure} + +Organize your code for maintainability: + +``` +src/ +├── config/ # Configuration files +├── controllers/ # Route controllers +├── middleware/ # Custom middleware +├── models/ # Database models +├── routes/ # API routes +├── services/ # Business logic +├── utils/ # Helper functions +├── validators/ # Input validation +└── app.ts # App setup +``` + +## Database Integration {#database} + +Using Prisma for type-safe database access: + +```typescript +// prisma/schema.prisma +model User { + id String @id @default(uuid()) + email String @unique + name String + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +```typescript +// src/config/database.ts +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query'] : [] +}) + +export default prisma +``` + +## Authentication & Authorization {#auth} + +Implement JWT-based authentication: + +```typescript +// src/middleware/auth.ts +import jwt from 'jsonwebtoken' +import { Request, Response, NextFunction } from 'express' + +interface JWTPayload { + userId: string + email: string +} + +export const authenticate = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const token = req.headers.authorization?.split(' ')[1] + + if (!token) { + return res.status(401).json({ error: 'Authentication required' }) + } + + const payload = jwt.verify( + token, + process.env.JWT_SECRET! + ) as JWTPayload + + req.user = payload + next() + } catch (error) { + res.status(401).json({ error: 'Invalid or expired token' }) + } +} +``` + +## Error Handling {#errors} + +Centralized error handling: + +```typescript +// src/middleware/errorHandler.ts +import { Request, Response, NextFunction } from 'express' + +class AppError extends Error { + statusCode: number + isOperational: boolean + + constructor(message: string, statusCode: number) { + super(message) + this.statusCode = statusCode + this.isOperational = true + Error.captureStackTrace(this, this.constructor) + } +} + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction +) => { + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + status: 'error', + message: err.message + }) + } + + // Log unexpected errors + console.error('Unexpected error:', err) + + res.status(500).json({ + status: 'error', + message: 'Internal server error' + }) +} + +export { AppError } +``` + +## Validation {#validation} + +Use Zod for runtime validation: + +```typescript +// src/validators/userValidator.ts +import { z } from 'zod' + +export const createUserSchema = z.object({ + body: z.object({ + email: z.string().email('Invalid email address'), + name: z.string().min(2, 'Name must be at least 2 characters'), + password: z.string().min(8, 'Password must be at least 8 characters') + }) +}) + +export const validate = (schema: any) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await schema.parseAsync({ + body: req.body, + query: req.query, + params: req.params + }) + next() + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + status: 'error', + errors: error.errors + }) + } + next(error) + } + } +} +``` + +## API Documentation {#docs} + +Auto-generate docs with Swagger: + +```typescript +// src/config/swagger.ts +import swaggerJsdoc from 'swagger-jsdoc' + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'My API', + version: '1.0.0', + description: 'API documentation' + }, + servers: [ + { + url: 'http://localhost:3000', + description: 'Development server' + } + ] + }, + apis: ['./src/routes/*.ts'] +} + +export const swaggerSpec = swaggerJsdoc(options) +``` + +Document endpoints in route files: + +```typescript +/** + * @openapi + * /api/users: + * post: + * summary: Create a new user + * tags: [Users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * name: + * type: string + * password: + * type: string + * responses: + * 201: + * description: User created successfully + */ +router.post('/users', validate(createUserSchema), createUser) +``` + +## Testing {#testing} + +Write tests with Jest and Supertest: + +```typescript +// tests/users.test.ts +import request from 'supertest' +import app from '../src/app' +import prisma from '../src/config/database' + +describe('User API', () => { + beforeEach(async () => { + await prisma.user.deleteMany() + }) + + afterAll(async () => { + await prisma.$disconnect() + }) + + describe('POST /api/users', () => { + it('should create a new user', async () => { + const res = await request(app) + .post('/api/users') + .send({ + email: 'test@example.com', + name: 'Test User', + password: 'password123' + }) + .expect(201) + + expect(res.body).toHaveProperty('id') + expect(res.body.email).toBe('test@example.com') + }) + + it('should return 400 for invalid email', async () => { + const res = await request(app) + .post('/api/users') + .send({ + email: 'invalid-email', + name: 'Test User', + password: 'password123' + }) + .expect(400) + + expect(res.body).toHaveProperty('errors') + }) + }) +}) +``` + +## Security Best Practices + +1. **Rate Limiting** + ```typescript + import rateLimit from 'express-rate-limit' + + const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100 // limit each IP to 100 requests per windowMs + }) + + app.use('/api/', limiter) + ``` + +2. **Helmet** for security headers + ```typescript + import helmet from 'helmet' + app.use(helmet()) + ``` + +3. **CORS** configuration + ```typescript + import cors from 'cors' + app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(','), + credentials: true + })) + ``` + +4. **Input Sanitization** + ```typescript + import mongoSanitize from 'express-mongo-sanitize' + app.use(mongoSanitize()) + ``` + +## Performance Optimization + +### 1. Response Compression +```typescript +import compression from 'compression' +app.use(compression()) +``` + +### 2. Caching +```typescript +import Redis from 'ioredis' +const redis = new Redis(process.env.REDIS_URL) + +async function getCachedData(key: string) { + const cached = await redis.get(key) + if (cached) return JSON.parse(cached) + + const data = await fetchFromDatabase() + await redis.setex(key, 3600, JSON.stringify(data)) + return data +} +``` + +### 3. Database Query Optimization +```typescript +// Use pagination +const users = await prisma.user.findMany({ + skip: (page - 1) * limit, + take: limit, + select: { + id: true, + email: true, + name: true + // Don't select sensitive fields like password + } +}) + +// Use indexes +@@index([email]) +@@index([createdAt]) +``` + +## Deployment Checklist + +- [ ] Environment variables configured +- [ ] Database migrations run +- [ ] SSL/TLS certificates installed +- [ ] Rate limiting enabled +- [ ] Logging configured (Winston, Morgan) +- [ ] Error tracking (Sentry) +- [ ] Health check endpoint +- [ ] API documentation deployed +- [ ] Load testing completed +- [ ] Backup strategy implemented + +## Monitoring + +```typescript +// Health check endpoint +app.get('/health', async (req, res) => { + try { + await prisma.$queryRaw`SELECT 1` + res.status(200).json({ status: 'healthy', timestamp: new Date() }) + } catch (error) { + res.status(503).json({ status: 'unhealthy', error: error.message }) + } +}) +``` + +## Conclusion + +Building production-ready APIs requires attention to: +- **Structure**: Organized code is maintainable code +- **Security**: Authentication, validation, and rate limiting +- **Errors**: Proper error handling and logging +- **Testing**: Comprehensive test coverage +- **Documentation**: Clear API docs for consumers +- **Performance**: Caching and optimization + +The complete example code is available on [GitHub](https://github.com/alexjohnson/rest-api-guide). + +## Resources + +- [Express Best Practices](https://expressjs.com/en/advanced/best-practice-performance.html) +- [Node.js Security Checklist](https://github.com/goldbergyoni/nodebestpractices#6-security-best-practices) +- [Prisma Documentation](https://www.prisma.io/docs) + +--- + +Questions? Leave a comment below or reach out on [Twitter](https://twitter.com/alexjohnson)! diff --git a/templates/dev-portfolio/content/blog/docker-deployment/featured.png b/templates/dev-portfolio/content/blog/docker-deployment/featured.png new file mode 100644 index 00000000..dc088cd3 Binary files /dev/null and b/templates/dev-portfolio/content/blog/docker-deployment/featured.png differ diff --git a/templates/dev-portfolio/content/blog/docker-deployment/index.md b/templates/dev-portfolio/content/blog/docker-deployment/index.md new file mode 100644 index 00000000..a1580f86 --- /dev/null +++ b/templates/dev-portfolio/content/blog/docker-deployment/index.md @@ -0,0 +1,526 @@ +--- +title: "Docker Deployment Guide: From Development to Production" +date: 2024-10-15 +summary: "Learn how to containerize your applications with Docker, optimize images, and deploy to production with best practices" +tags: + - Docker + - DevOps + - Deployment + - Tutorial +authors: + - me +featured: false +--- + +Docker simplifies deployment by packaging your application with all dependencies. This guide covers containerizing apps, optimizing images, and production deployment. + +## Why Docker? + +**Problems Docker solves**: +- ❌ "Works on my machine" syndrome +- ❌ Complex dependency management +- ❌ Environment inconsistencies +- ❌ Difficult scaling + +**Benefits**: +- ✅ Consistent environments (dev = prod) +- ✅ Easy scaling +- ✅ Simplified deployment +- ✅ Isolation and security + +## Basic Dockerfile + +For a Node.js application: + +```dockerfile +# Use official Node image +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application code +COPY . . + +# Expose port +EXPOSE 3000 + +# Start application +CMD ["node", "server.js"] +``` + +Build and run: + +```bash +docker build -t my-app . +docker run -p 3000:3000 my-app +``` + +## Multi-Stage Builds (Optimization) + +Reduce image size with multi-stage builds: + +```dockerfile +# Stage 1: Build +FROM node:18-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Stage 2: Production +FROM node:18-alpine + +WORKDIR /app + +# Copy only necessary files from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY package*.json ./ + +EXPOSE 3000 +CMD ["node", "dist/server.js"] +``` + +**Result**: Image size reduced from 800MB → 150MB! + +## Docker Compose for Local Development + +`docker-compose.yml`: + +```yaml +version: '3.8' + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - DATABASE_URL=postgresql://user:pass@db:5432/mydb + volumes: + - .:/app + - /app/node_modules + depends_on: + - db + - redis + + db: + image: postgres:15-alpine + environment: + - POSTGRES_USER=user + - POSTGRES_PASSWORD=pass + - POSTGRES_DB=mydb + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + +volumes: + postgres_data: +``` + +Start everything: + +```bash +docker-compose up -d +``` + +## Environment Variables + +Never hardcode secrets! Use `.env` file: + +```env +# .env (add to .gitignore!) +DATABASE_URL=postgresql://user:pass@localhost:5432/mydb +JWT_SECRET=your-secret-key +API_KEY=your-api-key +``` + +Reference in docker-compose: + +```yaml +services: + app: + env_file: + - .env +``` + +## Production Dockerfile + +Optimized for production: + +```dockerfile +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including devDependencies) +RUN npm ci + +# Copy source +COPY . . + +# Build application +RUN npm run build + +# Remove devDependencies +RUN npm prune --production + +# Production stage +FROM node:18-alpine + +# Add non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +WORKDIR /app + +# Copy built app from builder +COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist +COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./ + +# Use non-root user +USER nodejs + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node healthcheck.js + +EXPOSE 3000 + +CMD ["node", "dist/server.js"] +``` + +## Image Optimization Tips + +### 1. Use .dockerignore + +``` +node_modules +npm-debug.log +.git +.env +.DS_Store +*.md +.vscode +coverage +.github +``` + +### 2. Layer Caching + +```dockerfile +# ❌ Bad: Changes to code invalidate ALL layers +COPY . . +RUN npm ci + +# ✅ Good: Package changes don't rebuild code +COPY package*.json ./ +RUN npm ci +COPY . . +``` + +### 3. Use Alpine Images + +```dockerfile +# Large: node:18 (900MB) +FROM node:18 + +# Small: node:18-alpine (150MB) +FROM node:18-alpine +``` + +### 4. Combine RUN Commands + +```dockerfile +# ❌ Bad: Multiple layers +RUN apt-get update +RUN apt-get install -y curl +RUN apt-get clean + +# ✅ Good: Single layer +RUN apt-get update && \ + apt-get install -y curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +``` + +## Deploying to AWS ECS + +### 1. Build and Push to ECR + +```bash +# Authenticate Docker to ECR +aws ecr get-login-password --region us-east-1 | \ + docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com + +# Build image +docker build -t my-app . + +# Tag image +docker tag my-app:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest + +# Push to ECR +docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest +``` + +### 2. ECS Task Definition + +```json +{ + "family": "my-app", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "256", + "memory": "512", + "containerDefinitions": [ + { + "name": "my-app", + "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest", + "portMappings": [ + { + "containerPort": 3000, + "protocol": "tcp" + } + ], + "environment": [ + { + "name": "NODE_ENV", + "value": "production" + } + ], + "secrets": [ + { + "name": "DATABASE_URL", + "valueFrom": "arn:aws:secretsmanager:..." + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/my-app", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "ecs" + } + } + } + ] +} +``` + +## Docker Security Best Practices + +### 1. Use Non-Root User + +```dockerfile +# Add user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Switch to user +USER nodejs +``` + +### 2. Scan for Vulnerabilities + +```bash +# Using Trivy +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy image my-app:latest +``` + +### 3. Keep Base Images Updated + +```dockerfile +# ✅ Pin specific version, update regularly +FROM node:18.19.0-alpine + +# ❌ Avoid 'latest' tag +FROM node:latest +``` + +### 4. Read-Only Filesystem + +```yaml +services: + app: + read_only: true + tmpfs: + - /tmp +``` + +## Health Checks + +Simple health check endpoint: + +```javascript +// healthcheck.js +const http = require('http') + +const options = { + host: 'localhost', + port: 3000, + path: '/health', + timeout: 2000 +} + +const request = http.request(options, (res) => { + if (res.statusCode === 200) { + process.exit(0) + } else { + process.exit(1) + } +}) + +request.on('error', () => { + process.exit(1) +}) + +request.end() +``` + +## Monitoring & Logging + +### Container Logs + +```bash +# View logs +docker logs my-app + +# Follow logs +docker logs -f my-app + +# Last 100 lines +docker logs --tail 100 my-app +``` + +### Docker Stats + +```bash +# View resource usage +docker stats + +# Specific container +docker stats my-app +``` + +## Common Issues & Solutions + +### Issue: Image Too Large + +**Solution**: Multi-stage builds, Alpine images, .dockerignore + +### Issue: Slow Builds + +**Solution**: Optimize layer caching, use BuildKit + +```bash +DOCKER_BUILDKIT=1 docker build -t my-app . +``` + +### Issue: Container Won't Start + +**Solution**: Check logs, verify CMD/ENTRYPOINT + +```bash +docker logs container-id +docker inspect container-id +``` + +## CI/CD Pipeline Example (GitHub Actions) + +```yaml +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build and push image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: my-app + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + - name: Update ECS service + run: | + aws ecs update-service --cluster my-cluster \ + --service my-app --force-new-deployment +``` + +## Best Practices Checklist + +- [ ] Use multi-stage builds +- [ ] Use .dockerignore +- [ ] Run as non-root user +- [ ] Pin base image versions +- [ ] Scan for vulnerabilities +- [ ] Implement health checks +- [ ] Use environment variables for config +- [ ] Keep images small (<500MB) +- [ ] Tag images properly (not just 'latest') +- [ ] Set up logging and monitoring + +## Resources + +- [Docker Documentation](https://docs.docker.com/) +- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/) +- [Dockerfile Reference](https://docs.docker.com/engine/reference/builder/) +- [Docker Compose](https://docs.docker.com/compose/) + +## Conclusion + +Docker streamlines deployment by: +1. Creating consistent environments +2. Simplifying dependency management +3. Enabling easy scaling +4. Improving security through isolation + +Start small, optimize incrementally, and always test in staging before production! + +--- + +**Example repo**: [github.com/alexjohnson/docker-guide](https://github.com/alexjohnson/docker-guide) + +Questions? Reach out on [Twitter](https://twitter.com/alexjohnson)! diff --git a/templates/dev-portfolio/content/blog/react-performance/featured.png b/templates/dev-portfolio/content/blog/react-performance/featured.png new file mode 100644 index 00000000..d0a17d39 Binary files /dev/null and b/templates/dev-portfolio/content/blog/react-performance/featured.png differ diff --git a/templates/dev-portfolio/content/blog/react-performance/index.md b/templates/dev-portfolio/content/blog/react-performance/index.md new file mode 100644 index 00000000..b63133cb --- /dev/null +++ b/templates/dev-portfolio/content/blog/react-performance/index.md @@ -0,0 +1,397 @@ +--- +title: "React Performance Optimization: A Practical Guide" +date: 2024-11-22 +summary: "Learn how to identify and fix performance bottlenecks in React applications with real-world examples and profiling techniques" +tags: + - React + - Performance + - JavaScript + - Frontend + - Optimization +authors: + - me +featured: true +--- + +React apps can become slow as they grow. This guide covers practical techniques to identify bottlenecks and optimize performance, backed by real-world examples. + +## Common Performance Issues + +Before optimizing, measure. Use React DevTools Profiler to identify: +- Unnecessary re-renders +- Heavy computations +- Large component trees +- Inefficient list rendering + +## 1. Prevent Unnecessary Re-renders + +### Problem: Parent Re-renders Cascade + +```jsx +// ❌ Bad: Child re-renders on every parent update +function Parent() { + const [count, setCount] = useState(0) + + return ( +
+ + {/* Re-renders even though it doesn't use count! */} +
+ ) +} +``` + +### Solution: React.memo + +```jsx +// ✅ Good: Child only re-renders when props change +const ExpensiveChild = React.memo(() => { + console.log('ExpensiveChild rendered') + return
{/* expensive rendering */}
+}) +``` + +**When to use**: Components that render often but rarely change props. + +## 2. Optimize Expensive Calculations + +### Problem: Recalculating on Every Render + +```jsx +// ❌ Bad: filterItems runs on every render +function ProductList({ products, searchTerm }) { + const filtered = products.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) // Runs even when products/searchTerm haven't changed! + + return
    {filtered.map(p =>
  • {p.name}
  • )}
+} +``` + +### Solution: useMemo + +```jsx +// ✅ Good: Only recalculates when dependencies change +function ProductList({ products, searchTerm }) { + const filtered = useMemo(() => + products.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) + ), + [products, searchTerm] + ) + + return
    {filtered.map(p =>
  • {p.name}
  • )}
+} +``` + +**When to use**: Expensive calculations that don't need to run on every render. + +## 3. Stabilize Function References + +### Problem: New Function on Every Render + +```jsx +// ❌ Bad: handleClick creates new function every render +function Parent() { + const [count, setCount] = useState(0) + + const handleClick = () => setCount(count + 1) + + return // Child re-renders! +} + +const Child = React.memo(({ onClick }) => { + return +}) +``` + +### Solution: useCallback + +```jsx +// ✅ Good: handleClick reference stays stable +function Parent() { + const [count, setCount] = useState(0) + + const handleClick = useCallback(() => { + setCount(c => c + 1) // Use functional update! + }, []) // Empty deps because we use functional update + + return +} +``` + +**When to use**: Passing callbacks to memoized child components. + +## 4. Virtual Lists for Large Datasets + +### Problem: Rendering 10,000 Items + +```jsx +// ❌ Bad: Renders all 10,000 items (slow!) +function LargeList({ items }) { + return ( +
+ {items.map(item => ( +
{item.name}
+ ))} +
+ ) +} +``` + +### Solution: react-window + +```jsx +// ✅ Good: Only renders visible items +import { FixedSizeList } from 'react-window' + +function LargeList({ items }) { + const Row = ({ index, style }) => ( +
{items[index].name}
+ ) + + return ( + + {Row} + + ) +} +``` + +**Result**: 10,000 items render in <50ms instead of 2000ms! + +## 5. Code Splitting & Lazy Loading + +### Problem: Large Bundle Size + +```jsx +// ❌ Bad: Everything loads upfront +import AdminPanel from './AdminPanel' // 500KB! +import UserDashboard from './UserDashboard' + +function App() { + return isAdmin ? : +} +``` + +### Solution: React.lazy + +```jsx +// ✅ Good: Load components on demand +const AdminPanel = React.lazy(() => import('./AdminPanel')) +const UserDashboard = React.lazy(() => import('./UserDashboard')) + +function App() { + return ( + }> + {isAdmin ? : } + + ) +} +``` + +**Result**: Initial bundle: 150KB instead of 650KB! + +## 6. Optimize Context Usage + +### Problem: Context Causes Mass Re-renders + +```jsx +// ❌ Bad: Every consumer re-renders on any state change +const AppContext = createContext() + +function AppProvider({ children }) { + const [user, setUser] = useState(null) + const [theme, setTheme] = useState('light') + const [notifications, setNotifications] = useState([]) + + return ( + + {children} + + ) +} +``` + +### Solution: Split Contexts + +```jsx +// ✅ Good: Separate contexts for different concerns +const UserContext = createContext() +const ThemeContext = createContext() +const NotificationContext = createContext() + +function AppProvider({ children }) { + const [user, setUser] = useState(null) + const [theme, setTheme] = useState('light') + const [notifications, setNotifications] = useState([]) + + return ( + + + + {children} + + + + ) +} +``` + +**Result**: Components only re-render when their specific context changes! + +## 7. Debounce Expensive Operations + +### Problem: Search Triggers on Every Keystroke + +```jsx +// ❌ Bad: API call on every keystroke +function SearchBox() { + const [query, setQuery] = useState('') + + useEffect(() => { + searchAPI(query) // Called 10+ times as user types "javascript"! + }, [query]) + + return setQuery(e.target.value)} /> +} +``` + +### Solution: Custom Debounce Hook + +```jsx +// ✅ Good: API call only after user stops typing +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay) + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +function SearchBox() { + const [query, setQuery] = useState('') + const debouncedQuery = useDebounce(query, 300) + + useEffect(() => { + if (debouncedQuery) { + searchAPI(debouncedQuery) // Called once after 300ms pause! + } + }, [debouncedQuery]) + + return setQuery(e.target.value)} /> +} +``` + +## 8. Key Props Matter + +### Problem: Poor Key Choices + +```jsx +// ❌ Bad: Using index as key causes issues +{items.map((item, index) => ( +
{item.name}
+))} + +// When items reorder, React thinks they're different components! +``` + +### Solution: Stable, Unique Keys + +```jsx +// ✅ Good: Use stable IDs +{items.map(item => ( +
{item.name}
+))} +``` + +## Real-World Example: Optimizing a Dashboard + +**Before**: Dashboard with 20 widgets, each fetching data, all re-rendering on any state change. + +**After optimization**: +1. Split into separate contexts for user, theme, data +2. Memoized individual widgets with React.memo +3. Used useMemo for data transformations +4. Lazy loaded heavy charts +5. Debounced filter inputs + +**Result**: +- Initial render: 3000ms → 800ms +- Interactions: 200ms → 50ms +- Bundle size: 800KB → 250KB (initial) + 550KB (lazy) + +## Profiling Workflow + +1. **React DevTools Profiler** + - Record interaction + - Identify components taking longest + - Check why they rendered (props change? parent render?) + +2. **Chrome DevTools Performance** + - Record performance + - Look for long tasks (>50ms) + - Check FPS during animations + +3. **Lighthouse** + - Run audit + - Focus on "Time to Interactive" + - Check bundle sizes + +## Common Pitfalls + +❌ **Don't** optimize prematurely +✅ **Do** measure first, optimize bottlenecks + +❌ **Don't** memo everything +✅ **Do** memo expensive components + +❌ **Don't** use useMemo for simple calculations +✅ **Do** use for actual performance issues + +❌ **Don't** forget dependency arrays +✅ **Do** include all dependencies (or use ESLint plugin) + +## Performance Checklist + +- [ ] Use React DevTools Profiler to identify issues +- [ ] Memoize expensive child components +- [ ] Use useMemo for heavy calculations +- [ ] Use useCallback for callbacks to memoized children +- [ ] Implement code splitting for large components +- [ ] Use virtual lists for large datasets +- [ ] Debounce expensive operations (search, API calls) +- [ ] Use proper key props for lists +- [ ] Split contexts to minimize re-renders +- [ ] Optimize bundle size (tree shaking, minification) + +## Tools & Resources + +- [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools) +- [react-window](https://react-window.vercel.app/) +- [Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) +- [Why Did You Render](https://github.com/welldone-software/why-did-you-render) + +## Conclusion + +Performance optimization is about: +1. **Measuring** before optimizing +2. **Identifying** actual bottlenecks +3. **Applying** targeted fixes +4. **Verifying** improvements + +Don't optimize blindly—profile, fix, measure, repeat! + +--- + +**Want to dive deeper?** Check out my [React Performance Workshop](https://github.com/alexjohnson/react-perf-workshop). + +Questions? Reach out on [Twitter](https://twitter.com/alexjohnson)! diff --git a/templates/dev-portfolio/content/projects/_index.md b/templates/dev-portfolio/content/projects/_index.md new file mode 100644 index 00000000..45fba71f --- /dev/null +++ b/templates/dev-portfolio/content/projects/_index.md @@ -0,0 +1,26 @@ +--- +title: 'Projects' +date: 2024-05-19 +type: landing + +design: + # Section spacing + spacing: '5rem' + +# Page sections +sections: + - block: collection + content: + title: Selected Projects + text: I enjoy making things. Here are a selection of projects that I have worked on over the years. + filters: + folders: + - projects + design: + view: article-grid + fill_image: false + columns: 3 + show_date: false + show_read_time: false + show_read_more: false +--- diff --git a/templates/dev-portfolio/content/projects/ecommerce-platform/featured.png b/templates/dev-portfolio/content/projects/ecommerce-platform/featured.png new file mode 100644 index 00000000..5485fa8f Binary files /dev/null and b/templates/dev-portfolio/content/projects/ecommerce-platform/featured.png differ diff --git a/templates/dev-portfolio/content/projects/ecommerce-platform/index.md b/templates/dev-portfolio/content/projects/ecommerce-platform/index.md new file mode 100644 index 00000000..c30d7b05 --- /dev/null +++ b/templates/dev-portfolio/content/projects/ecommerce-platform/index.md @@ -0,0 +1,167 @@ +--- +title: "E-Commerce Platform" +date: 2024-11-15 +summary: "E-commerce API backend with Stripe payments, inventory management, and real-time webhooks" +tags: + - Backend + - Node.js + - API + - E-Commerce +tech_stack: + - React + - TypeScript + - Node.js + - Express + - PostgreSQL + - Stripe + - Redis + - Docker +links: + - type: github + url: https://github.com/alexjohnson/ecommerce-platform + label: Code + - type: live + url: https://shop-demo.example.com + label: Demo +featured: true +status: "Live" +role: "Lead Developer" +duration: "4 months" +team_size: 2 +highlights: + - "Handles 10k+ concurrent users" + - "99.9% uptime SLA" + - "Processing $50k+ monthly transactions" + - "60% faster page load vs competitors" +--- + +A modern, scalable e-commerce platform built from scratch with performance and user experience as top priorities. + +## Overview + +Built a complete e-commerce solution for a mid-sized retail company looking to expand online. The platform handles everything from product catalog management to payment processing and order fulfillment. + +## Key Features + +### Customer-Facing +- **Product Catalog** - Dynamic filtering, sorting, and search with instant results +- **Shopping Cart** - Real-time inventory checking and price calculations +- **Checkout** - Secure payment processing via Stripe with Apple Pay/Google Pay support +- **Order Tracking** - Real-time order status updates with email notifications +- **User Accounts** - Profile management, order history, and saved addresses + +### Admin Dashboard +- **Inventory Management** - Real-time stock tracking and low-stock alerts +- **Order Management** - Bulk order processing and fulfillment workflow +- **Analytics** - Sales dashboards, customer insights, and revenue reporting +- **Product Management** - Easy product creation with image uploads and variants + +## Technical Highlights + +### Performance Optimization +- Implemented Redis caching reducing database queries by 70% +- Optimized images with WebP format and lazy loading +- Server-side rendering for critical pages improving SEO and load times +- CDN integration for global content delivery + +### Scalability +- Microservices architecture allowing independent scaling +- Horizontal scaling with load balancing +- Database read replicas for improved query performance +- Message queues for async processing (order emails, inventory updates) + +### Security +- JWT authentication with refresh tokens +- Rate limiting to prevent abuse +- Input validation and sanitization +- PCI-compliant payment processing via Stripe + +## Architecture + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ React SPA │────▶│ REST API │────▶│ PostgreSQL │ +└─────────────┘ │ (Express) │ └─────────────┘ + └──────┬───────┘ + │ + ┌──────▼───────┐ + │ Redis │ + │ (Cache) │ + └──────────────┘ +``` + +## Challenges & Solutions + +### Challenge 1: Inventory Sync +**Problem**: Multiple users buying same product simultaneously causing overselling + +**Solution**: Implemented optimistic locking with Redis to ensure inventory accuracy during concurrent purchases + +### Challenge 2: Payment Processing +**Problem**: Handling payment failures gracefully while maintaining order integrity + +**Solution**: Built robust state machine for order processing with automatic retry logic and customer notifications + +### Challenge 3: Performance at Scale +**Problem**: Slow page loads during traffic spikes + +**Solution**: Implemented multi-layer caching strategy (CDN, Redis, in-memory) and database query optimization + +## Results + +- **Performance**: 60% faster page load times compared to previous platform +- **Conversion**: 25% increase in conversion rate due to improved UX +- **Uptime**: 99.9% uptime over 6 months in production +- **Scale**: Successfully handled Black Friday with 10k concurrent users +- **Revenue**: Processing over $50k in monthly transactions + +## Tech Stack Details + +**Frontend** +- React 18 with TypeScript +- Tailwind CSS for styling +- React Query for data fetching +- React Hook Form for forms + +**Backend** +- Node.js with Express +- PostgreSQL with Prisma ORM +- Redis for caching and sessions +- Bull for job queues + +**Infrastructure** +- Docker containers +- AWS EC2 for hosting +- AWS S3 for image storage +- Cloudflare CDN +- GitHub Actions for CI/CD + +**Payment & Services** +- Stripe for payments +- SendGrid for emails +- Sentry for error tracking + +## Future Improvements + +- [ ] Mobile app (React Native) +- [ ] Multi-language support +- [ ] Wishlist and product recommendations +- [ ] Live chat support +- [ ] Advanced analytics dashboard + +## Screenshots + +*(Screenshots would go here in production)* + +## Lessons Learned + +1. **Start with Performance**: Built with performance in mind from day one rather than optimizing later +2. **Testing Matters**: Comprehensive test suite caught critical bugs before production +3. **Monitor Everything**: Proper logging and monitoring essential for maintaining uptime +4. **User Feedback**: Regular user testing revealed UX issues we wouldn't have found otherwise + +--- + +**Project Status**: ✅ Live in Production +**GitHub**: [View Source Code](https://github.com/alexjohnson/ecommerce-platform) +**Demo**: [Try it Live](https://shop-demo.example.com) diff --git a/templates/dev-portfolio/content/projects/task-manager/featured.png b/templates/dev-portfolio/content/projects/task-manager/featured.png new file mode 100644 index 00000000..6aeea1c4 Binary files /dev/null and b/templates/dev-portfolio/content/projects/task-manager/featured.png differ diff --git a/templates/dev-portfolio/content/projects/task-manager/index.md b/templates/dev-portfolio/content/projects/task-manager/index.md new file mode 100644 index 00000000..96e72598 --- /dev/null +++ b/templates/dev-portfolio/content/projects/task-manager/index.md @@ -0,0 +1,170 @@ +--- +title: "TaskFlow - Project Management Tool" +date: 2024-09-20 +summary: "Real-time collaborative task management application with drag-and-drop Kanban boards and team features" +tags: + - Full-Stack + - Next.js + - Real-Time + - Productivity +tech_stack: + - Next.js + - TypeScript + - Prisma + - PostgreSQL + - WebSockets + - Tailwind CSS +links: + - type: github + url: https://github.com/alexjohnson/taskflow + label: Code + - type: live + url: https://taskflow-demo.example.com + label: Demo +featured: true +status: "Live" +role: "Solo Developer" +duration: "2 months" +team_size: 1 +highlights: + - "Real-time collaboration with WebSockets" + - "2000+ active users" + - "Featured on Product Hunt" +--- + +A modern, intuitive task management tool built for remote teams. Features real-time collaboration, customizable workflows, and beautiful UI. + +## Overview + +TaskFlow was born out of frustration with existing project management tools being either too complex or lacking essential features. I built a solution that's powerful yet simple to use. + +## Key Features + +### Core Functionality +- **Kanban Boards** - Drag-and-drop interface for visual task management +- **Real-Time Sync** - See changes instantly as team members update tasks +- **Multiple Views** - Switch between Kanban, List, and Calendar views +- **Task Details** - Rich descriptions, attachments, comments, and checklists +- **Labels & Filters** - Organize and find tasks quickly + +### Collaboration +- **Team Workspaces** - Separate spaces for different projects/teams +- **@Mentions** - Tag team members in comments for notifications +- **Activity Feed** - Track all changes and updates +- **Permissions** - Role-based access control (admin, member, viewer) + +### Productivity +- **Keyboard Shortcuts** - Power user features for faster navigation +- **Templates** - Reusable board templates for common workflows +- **Due Dates & Reminders** - Never miss a deadline +- **Time Tracking** - Built-in timer for task duration tracking + +## Technical Implementation + +### Real-Time Features +Used WebSockets (Socket.io) for instant updates across all connected clients. Implemented optimistic UI updates for snappy user experience even before server confirmation. + +### Drag & Drop +Built custom drag-and-drop using react-beautiful-dnd with smooth animations and mobile touch support. + +### Performance +- Implemented virtual scrolling for boards with 1000+ tasks +- Optimized database queries with proper indexing +- Used Redis for session storage and caching +- Image optimization with Next.js Image component + +### Authentication +- Secure auth with NextAuth.js +- Support for email/password and OAuth (Google, GitHub) +- JWT tokens with automatic refresh + +## Architecture + +Built as a modern monolith with Next.js API routes: + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Next.js │────▶│ API Routes │────▶│ PostgreSQL │ +│ (React) │ │ (REST/WS) │ │ + Prisma │ +└─────────────┘ └──────────────┘ └─────────────┘ + │ │ + │ ┌──────▼───────┐ + └────────────▶│ Socket.io │ + │ (Real-Time) │ + └──────────────┘ +``` + +## Challenges Solved + +### Real-Time Conflicts +**Problem**: Multiple users editing same task simultaneously + +**Solution**: Implemented operational transformation (OT) for conflict resolution and last-write-wins strategy with conflict notifications + +### Mobile Performance +**Problem**: Drag-and-drop laggy on mobile devices + +**Solution**: Optimized touch handlers and reduced re-renders using React.memo and useMemo + +### Scale +**Problem**: Growing user base causing performance issues + +**Solution**: Added Redis caching layer and optimized database queries, reducing response time by 65% + +## Results + +- 📈 **Users**: 2000+ active users within 3 months +- ⭐ **Product Hunt**: Featured and received 200+ upvotes +- 🚀 **Performance**: Sub-100ms API response times +- 💯 **Uptime**: 99.8% uptime since launch +- 📱 **Mobile**: 40% of traffic from mobile devices + +## Tech Stack + +**Frontend** +- Next.js 14 (App Router) +- TypeScript +- Tailwind CSS +- Framer Motion (animations) +- React Beautiful DnD + +**Backend** +- Next.js API Routes +- Prisma ORM +- PostgreSQL +- Socket.io for WebSockets +- NextAuth.js for authentication + +**Infrastructure** +- Vercel for hosting +- Supabase for PostgreSQL +- Redis Cloud for caching +- AWS S3 for file storage + +## User Feedback + +> "Finally, a task manager that doesn't get in my way. The real-time updates are magical!" - Sarah K., Product Manager + +> "We switched from Trello and haven't looked back. TaskFlow is faster and more intuitive." - Mike R., Engineering Lead + +## Open Source + +TaskFlow is open source! Contributions welcome. + +**License**: MIT +**GitHub**: [alexjohnson/taskflow](https://github.com/alexjohnson/taskflow) +**Demo**: [Try it live](https://taskflow-demo.example.com) + +## What's Next + +Currently working on: +- [ ] Mobile apps (iOS & Android) +- [ ] Gantt chart view +- [ ] Advanced reporting and analytics +- [ ] API for third-party integrations +- [ ] Offline mode support + +--- + +**Status**: ✅ Live & Actively Maintained +**Try it**: [taskflow-demo.example.com](https://taskflow-demo.example.com) diff --git a/templates/dev-portfolio/content/projects/weather-app/featured.png b/templates/dev-portfolio/content/projects/weather-app/featured.png new file mode 100644 index 00000000..8a059e80 Binary files /dev/null and b/templates/dev-portfolio/content/projects/weather-app/featured.png differ diff --git a/templates/dev-portfolio/content/projects/weather-app/index.md b/templates/dev-portfolio/content/projects/weather-app/index.md new file mode 100644 index 00000000..713bed0d --- /dev/null +++ b/templates/dev-portfolio/content/projects/weather-app/index.md @@ -0,0 +1,179 @@ +--- +title: "WeatherNow - Real-Time Weather App" +date: 2024-06-10 +summary: "Beautiful weather application with real-time data, 7-day forecasts, and interactive maps" +tags: + - Frontend + - React + - API Integration + - PWA +tech_stack: + - React + - TypeScript + - OpenWeather API + - Mapbox + - Tailwind CSS + - Vite +links: + - type: github + url: https://github.com/alexjohnson/weather-now + label: Code + - type: live + url: https://weathernow-demo.netlify.app + label: Demo +featured: false +status: "Live" +role: "Solo Developer" +duration: "3 weeks" +team_size: 1 +highlights: + - "PWA with offline support" + - "5000+ monthly active users" + - "Lighthouse score: 100" +--- + +A fast, beautiful weather application that provides real-time weather data, forecasts, and interactive maps. Built as a Progressive Web App with offline support. + +## Overview + +WeatherNow started as a weekend project to learn PWA development. It evolved into a fully-featured weather app used by thousands of people daily. + +## Features + +### Weather Data +- **Current Conditions** - Real-time weather for any location +- **7-Day Forecast** - Detailed daily forecasts with hourly breakdown +- **Weather Alerts** - Severe weather notifications +- **Historical Data** - Past weather data and trends + +### User Experience +- **Location Detection** - Automatic location based on GPS or IP +- **Search** - Find weather for any city worldwide +- **Favorites** - Save frequently checked locations +- **Units** - Toggle between metric and imperial +- **Dark Mode** - Automatic or manual theme switching + +### Progressive Web App +- **Offline Support** - Access cached data without internet +- **Install** - Add to home screen like a native app +- **Fast** - Optimized for performance (Lighthouse 100) +- **Responsive** - Perfect on any device + +## Technical Highlights + +### Performance +- Achieved **100/100 Lighthouse score** across all categories +- Implemented service workers for offline functionality +- Optimized bundle size: < 150KB gzipped +- Lazy loading for images and components +- Prefetching for instant navigation + +### Data Management +- Smart caching strategy with stale-while-revalidate +- Background sync for updated forecasts +- Efficient API usage with request batching +- Local storage for user preferences + +### UI/UX +- Smooth animations with Framer Motion +- Interactive weather map with Mapbox +- Weather icons that match current conditions +- Accessible (WCAG AA compliant) + +## API Integration + +Integrated multiple weather APIs for comprehensive data: + +```typescript +// Example: Fetching weather data +const fetchWeather = async (lat: number, lon: number) => { + const response = await fetch( + `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}` + ) + return response.json() +} +``` + +## Architecture + +``` +┌─────────────┐ ┌──────────────┐ +│ React App │────▶│ OpenWeather │ +│ (Vite) │ │ API │ +└──────┬──────┘ └──────────────┘ + │ + │ ┌──────────────┐ + └───────────▶│ Mapbox │ + │ API │ + └──────────────┘ +``` + +## Challenges + +### Challenge: API Rate Limits +**Solution**: Implemented intelligent caching and request batching to stay within free tier limits while maintaining data freshness + +### Challenge: Offline Experience +**Solution**: Service workers with cache-first strategy for UI, network-first for data with graceful fallbacks + +### Challenge: Performance on Slow Networks +**Solution**: Image optimization, code splitting, and Progressive rendering for instant perceived performance + +## Results + +- 📈 **Users**: 5000+ monthly active users +- ⚡ **Performance**: 100 Lighthouse score +- 📱 **PWA**: 40% of users installed as app +- 🌍 **Global**: Users from 50+ countries +- ⭐ **Rating**: 4.8/5 average user rating + +## Tech Stack + +**Frontend** +- React 18 +- TypeScript +- Tailwind CSS +- Framer Motion +- Vite (build tool) + +**APIs** +- OpenWeather API +- Mapbox GL JS +- IP Geolocation API + +**Tools** +- Workbox (PWA) +- React Query (data fetching) +- Zustand (state management) + +**Hosting** +- Netlify +- Cloudflare CDN + +## Open Source + +This project is open source and welcomes contributions! + +**License**: MIT +**GitHub**: [alexjohnson/weather-now](https://github.com/alexjohnson/weather-now) + +## Lessons Learned + +1. **PWAs are powerful**: Service workers enable app-like experiences on the web +2. **Performance matters**: Users notice and appreciate fast apps +3. **Caching strategy**: Critical for offline support and API cost management +4. **Simplicity wins**: Started with core features, added more based on user feedback + +## Future Plans + +- [ ] Weather widgets for websites +- [ ] Social features (share weather conditions) +- [ ] Weather photography integration +- [ ] Apple Watch & Android Wear apps +- [ ] Premium features (ad-free, extended forecasts) + +--- + +**Status**: ✅ Live & Maintained +**Try it**: [weathernow-demo.netlify.app](https://weathernow-demo.netlify.app) +**GitHub**: [@alexjohnson/weather-now](https://github.com/alexjohnson/weather-now) diff --git a/templates/dev-portfolio/data/authors/me.yaml b/templates/dev-portfolio/data/authors/me.yaml new file mode 100644 index 00000000..17478ba8 --- /dev/null +++ b/templates/dev-portfolio/data/authors/me.yaml @@ -0,0 +1,100 @@ +schema: hugoblox/author/v1 +slug: me +is_owner: true +name: + display: Alex Johnson + given: Alex + family: Johnson +postnominals: [] +status: + icon: "💻" # Emoji shown on avatar badge: 💻 developer, 🎨 designer, 🚀 startup, 🔬 researcher, 🎓 student +role: Full-Stack Developer +email: alex@example.com +bio: | + Full-stack developer with 5+ years of experience building scalable web applications. + Passionate about modern JavaScript, cloud architecture, and creating delightful user experiences. + +affiliations: + - name: Tech Corp + url: https://techcorp.example.com + +links: + - icon: at-symbol + url: mailto:alex@example.com + label: E-mail Me + - icon: brands/github + url: https://github.com/alexjohnson + - icon: brands/linkedin + url: https://www.linkedin.com/in/alexjohnson/ + - icon: brands/x + url: https://x.com/alexjohnson + +interests: + - Web Development + - Cloud Architecture + - Open Source + - Developer Experience + +education: + - degree: B.S. Computer Science + institution: State University + start: 2015-09-01 + end: 2019-05-31 + summary: | + Focus on software engineering and distributed systems. + Senior project on distributed caching systems. + button: + text: View Project + url: https://github.com/alexjohnson/senior-project + icon: brands/github + +experience: + - role: Senior Software Engineer + org: Tech Corp + start: 2023-01-01 + summary: | + Lead development of microservices architecture serving 1M+ users. + Improved API response time by 40% through optimization. + Mentored team of 5 junior developers. + - role: Full-Stack Developer + org: Startup Inc + start: 2021-06-01 + end: 2022-12-31 + summary: | + Built and deployed 3 production applications from scratch. + Implemented CI/CD pipeline reducing deployment time by 60%. + Led migration from monolith to microservices architecture. + - role: Junior Developer + org: Web Agency + start: 2020-01-01 + end: 2021-05-31 + summary: | + Developed client websites using React and modern web technologies. + Participated in code reviews and agile ceremonies. + +skills: + - name: Development + items: + - label: TypeScript + level: 5 + - label: React + level: 5 + - label: Node.js + level: 5 + - label: Python + level: 4 + - name: DevOps + items: + - label: Docker + level: 4 + - label: AWS + level: 4 + - label: CI/CD + level: 5 + +languages: + - name: English + level: 5 + label: Native + +awards: [] diff --git a/templates/dev-portfolio/go.mod b/templates/dev-portfolio/go.mod new file mode 100644 index 00000000..41fbe9cf --- /dev/null +++ b/templates/dev-portfolio/go.mod @@ -0,0 +1,8 @@ +module github.com/HugoBlox/kit/templates/dev-portfolio-free + +go 1.19 + +require ( + github.com/HugoBlox/kit/modules/integrations/netlify v0.0.0-20260102210655-66cba27dc652 + github.com/HugoBlox/kit/modules/blox v0.0.0-20260102211810-cdaf97afb270 +) diff --git a/templates/dev-portfolio/hugoblox.yaml b/templates/dev-portfolio/hugoblox.yaml new file mode 100644 index 00000000..9fb28766 --- /dev/null +++ b/templates/dev-portfolio/hugoblox.yaml @@ -0,0 +1,5 @@ +build: + hugo_version: '0.154.2' +template: + id: "dev-portfolio" + name: "Developer Portfolio" diff --git a/templates/dev-portfolio/netlify.toml b/templates/dev-portfolio/netlify.toml new file mode 100644 index 00000000..1d11f881 --- /dev/null +++ b/templates/dev-portfolio/netlify.toml @@ -0,0 +1,61 @@ +[build] + command = """ + set -e + echo "=== Starting Blog build process ===" + echo "Node version: $(node --version)" + echo "pnpm version: $(pnpm --version)" + echo "Hugo version: $(hugo version)" + echo "Go version: $(go version || echo 'Go not available')" + + echo "=== Installing dependencies ===" + pnpm install --verbose --no-frozen-lockfile + + echo "=== Running Hugo build ===" + hugo --gc --minify -b $URL --logLevel debug --printI18nWarnings --printPathWarnings + + echo "=== Running Pagefind indexing ===" + pnpm run pagefind + + echo "=== Build completed successfully ===" + """ + publish = "public" + +[build.environment] + HUGO_VERSION = "0.154.2" + GO_VERSION = "1.21.5" + NODE_VERSION = "22" + # Netlify runs an implicit install step; ensure it never enforces frozen lockfiles + PNPM_FLAGS = "--no-frozen-lockfile" + HUGO_ENABLEGITINFO = "true" + HUGO_LOG_I18N_WARNINGS = "true" + HUGO_LOG_WARNINGS = "true" + FORCE_COLOR = "1" + PNPM_LOG_LEVEL = "debug" + +[context.production.environment] + HUGO_ENV = "production" + +[context.deploy-preview] + command = """ + set -e + echo "=== Deploy Preview Build ===" + echo "Deploy URL: $DEPLOY_PRIME_URL" + pnpm install --verbose --no-frozen-lockfile + hugo --gc --minify --buildFuture -b $DEPLOY_PRIME_URL --logLevel debug --printI18nWarnings --printPathWarnings + pnpm run pagefind + """ + +[context.branch-deploy] + command = """ + set -e + echo "=== Branch Deploy Build ===" + echo "Deploy URL: $DEPLOY_PRIME_URL" + pnpm install --verbose --no-frozen-lockfile + hugo --gc --minify -b $DEPLOY_PRIME_URL --logLevel debug --printI18nWarnings --printPathWarnings + pnpm run pagefind + """ + +[[plugins]] + package = "netlify-plugin-hugo-cache-resources" + [plugins.inputs] + debug = true diff --git a/templates/dev-portfolio/package.json b/templates/dev-portfolio/package.json new file mode 100644 index 00000000..a154de2c --- /dev/null +++ b/templates/dev-portfolio/package.json @@ -0,0 +1,19 @@ +{ + "name": "hugo-blox-dev-portfolio-free", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@10.14.0", + "description": "Modern developer portfolio template for Hugo Blox with Tailwind CSS v4 - Free version", + "scripts": { + "dev": "hugo server --disableFastRender", + "build": "hugo --minify && pnpm run pagefind", + "pagefind": "pagefind --site public" + }, + "dependencies": { + "@tailwindcss/cli": "^4.1.12", + "@tailwindcss/typography": "^0.5.10", + "pagefind": "^1.4.0", + "preact": "^10.27.2", + "tailwindcss": "^4.1.12" + } +} diff --git a/templates/dev-portfolio/pnpm-lock.yaml b/templates/dev-portfolio/pnpm-lock.yaml new file mode 100644 index 00000000..2049b436 --- /dev/null +++ b/templates/dev-portfolio/pnpm-lock.yaml @@ -0,0 +1,714 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/cli': + specifier: ^4.1.12 + version: 4.1.18 + '@tailwindcss/typography': + specifier: ^0.5.10 + version: 0.5.19(tailwindcss@4.1.18) + pagefind: + specifier: ^1.4.0 + version: 1.4.0 + preact: + specifier: ^10.27.2 + version: 10.28.1 + tailwindcss: + specifier: ^4.1.12 + version: 4.1.18 + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@pagefind/darwin-arm64@1.4.0': + resolution: {integrity: sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.4.0': + resolution: {integrity: sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.4.0': + resolution: {integrity: sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.4.0': + resolution: {integrity: sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.4.0': + resolution: {integrity: sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-x64@1.4.0': + resolution: {integrity: sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==} + cpu: [x64] + os: [win32] + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.18': + resolution: {integrity: sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==} + hasBin: true + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + pagefind@1.4.0: + resolution: {integrity: sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + preact@10.28.1: + resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@pagefind/darwin-arm64@1.4.0': + optional: true + + '@pagefind/darwin-x64@1.4.0': + optional: true + + '@pagefind/freebsd-x64@1.4.0': + optional: true + + '@pagefind/linux-arm64@1.4.0': + optional: true + + '@pagefind/linux-x64@1.4.0': + optional: true + + '@pagefind/windows-x64@1.4.0': + optional: true + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.18': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + enhanced-resolve: 5.18.4 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.18 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.18 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cssesc@3.0.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + node-addon-api@7.1.1: {} + + pagefind@1.4.0: + optionalDependencies: + '@pagefind/darwin-arm64': 1.4.0 + '@pagefind/darwin-x64': 1.4.0 + '@pagefind/freebsd-x64': 1.4.0 + '@pagefind/linux-arm64': 1.4.0 + '@pagefind/linux-x64': 1.4.0 + '@pagefind/windows-x64': 1.4.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + preact@10.28.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + util-deprecate@1.0.2: {} diff --git a/templates/dev-portfolio/static/uploads/resume.pdf b/templates/dev-portfolio/static/uploads/resume.pdf new file mode 100644 index 00000000..d0489a3a Binary files /dev/null and b/templates/dev-portfolio/static/uploads/resume.pdf differ