feat: add new blocks

For contact-info, logos, research-areas, team-showcase

Update:
- hero block: to prevent wide sub-title text on large screen
- stats block: re-designed stats block with new styles
This commit is contained in:
George Cushen committed 2025-10-24 23:37:08 +01:00
1 parent 5f2290d590
commit c34ccd27b0
22 files changed
+2628 -63

No files matched your search

@@ -0,0 +1,139 @@
# Contact Info Block
Display comprehensive contact information for your lab or organization with a modern, responsive card layout.
## Features
- **Dual Card Layout**: Separate cards for location and contact methods
- **Rich Information Display**: Address, office hours, phone, email, social media
- **Interactive Elements**: Click-to-call, email links, map integration
- **Optional Contact Form**: Built-in contact form support
- **Map Integration**: External map links or embedded maps
- **Social Media Links**: Connect your social profiles
- **Prospective Members Section**: Dedicated area for recruitment
- **Fully Responsive**: Beautiful on all devices
- **Dark Mode Support**: Seamless light/dark theme switching
## Usage
```yaml
sections:
- block: contact-info
content:
title: Contact Us
subtitle: Get in touch with our research team
visit_title: Visit Our Lab
connect_title: Connect With Us
address:
lines:
- Smith Laboratory
- Department of Computer Science
- University of Excellence
- 123 Science Drive
- Excellence City, EC 12345
- United States
office_hours:
- "Monday - Friday: 9:00 AM - 5:00 PM"
- "Lab Meetings: Fridays 2:00 PM"
email: lab@example.edu
phone: "+1 (555) 123-4567"
social:
- icon: brands/x
url: https://twitter.com/SmithLabResearch
- icon: brands/linkedin
url: https://linkedin.com/company/smith-lab
- icon: brands/github
url: https://github.com/smith-lab
prospective:
title: Prospective Members
text: Interested in joining our lab? We're looking for motivated researchers.
button:
text: View Open Positions
url: /opportunities
map_url: https://maps.google.com/?q=University+of+Excellence
show_form: false
design:
css_class: "bg-gray-50 dark:bg-gray-900"
spacing:
padding: ["3rem", 0, "3rem", 0]
```
## Options
### Content Options
- **title**: Main heading for the contact section
- **subtitle**: Optional subtitle text
- **visit_title**: Heading for the location card (default: "Visit Us")
- **connect_title**: Heading for the contact card (default: "Connect")
- **address**: Physical address with multiple lines
- `lines`: Array of address line strings
- **office_hours**: Array of office hour strings
- **email**: Contact email address
- **phone**: Contact phone number
- **social**: Array of social media links
- `icon`: Icon identifier (e.g., "brands/x", "brands/linkedin")
- `url`: Social media profile URL
- **prospective**: Information for prospective members
- `title`: Section heading
- `text`: Description text
- `button`: CTA button with `text` and `url`
- **map_url**: External map service link
- **map_embed**: HTML embed code for inline map
- **show_form**: Display contact form (default: false)
- **form_action**: Form submission endpoint
### Design Options
Standard design options apply including:
- `css_class`: Background and styling classes
- `spacing`: Padding configuration
## Map Integration
### External Map Link
Provide a `map_url` to link to Google Maps, Apple Maps, or other services:
```yaml
map_url: "https://maps.google.com/?q=Your+Location"
```
### Embedded Map
For inline maps, provide the iframe HTML in `map_embed`:
```yaml
map_embed: |
<iframe src="https://www.google.com/maps/embed?..."
width="100%" height="450" style="border:0;"
allowfullscreen="" loading="lazy"></iframe>
```
## Contact Form
Enable the built-in contact form:
```yaml
show_form: true
form_action: "https://formspree.io/f/YOUR_FORM_ID"
```
Note: You'll need to set up a form handler service like Formspree, Netlify Forms, or custom endpoint.
## Styling
The block uses modern card-based design with:
- Subtle shadows and hover effects
- Icon accents for visual hierarchy
- Consistent spacing and typography
- Fully responsive grid layout
- Dark mode optimized colors
## Best Practices
1. **Complete Information**: Provide all relevant contact methods
2. **Office Hours**: Clearly state availability
3. **Social Proof**: Link active social media profiles
4. **Map Integration**: Help visitors find your location
5. **Recruitment CTA**: Include prospective member information
6. **Form Handler**: Set up proper form submission handling
7. **Mobile Testing**: Verify all links work on mobile devices
@@ -0,0 +1,184 @@
{{/* Hugo Blox: Contact Info Block */}}
{{/* Documentation: https://hugoblox.com/blocks/ */}}
{{/* License: https://github.com/HugoBlox/hugo-blox-builder/blob/main/LICENSE.md */}}
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $title := $block.content.title | default "Contact Us" | emojify | $page.RenderString }}
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16 lg:py-20">
{{/* Section Header */}}
<div class="text-center mb-12">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white tracking-tight mb-4">
{{ . }}
</h2>
{{ end }}
{{ with $subtitle }}
<p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto">
{{ . }}
</p>
{{ end }}
</div>
{{/* Contact Cards Grid */}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
{{/* Visit Us Card */}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300">
<div class="flex items-center mb-6">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "hero/map-pin" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
</div>
<h3 class="ml-4 text-2xl font-bold text-gray-900 dark:text-white">
{{ $block.content.visit_title | default "Visit Us" }}
</h3>
</div>
<div class="space-y-4">
{{ with $block.content.address }}
<div class="text-gray-600 dark:text-gray-300">
{{ range .lines }}
<p>{{ . }}</p>
{{ end }}
</div>
{{ end }}
{{ with $block.content.office_hours }}
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Office Hours</h4>
<div class="text-gray-600 dark:text-gray-300 space-y-1">
{{ range . }}
<p>{{ . }}</p>
{{ end }}
</div>
</div>
{{ end }}
{{ with $block.content.map_url }}
<a href="{{ . }}" target="_blank" rel="noopener"
class="inline-flex items-center mt-4 px-4 py-2 bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/50 transition-colors">
{{ partial "functions/get_icon" (dict "name" "hero/map" "attributes" "class=\"w-5 h-5 mr-2\"") }}
View on Map
</a>
{{ end }}
</div>
</div>
{{/* Connect Card */}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300">
<div class="flex items-center mb-6">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "hero/chat-bubble-left-right" "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
</div>
<h3 class="ml-4 text-2xl font-bold text-gray-900 dark:text-white">
{{ $block.content.connect_title | default "Connect" }}
</h3>
</div>
<div class="space-y-4">
{{/* Contact Methods */}}
<div class="space-y-3">
{{ with $block.content.email }}
<div class="flex items-center">
{{ partial "functions/get_icon" (dict "name" "hero/envelope" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }}
<a href="mailto:{{ . }}" class="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ . }}
</a>
</div>
{{ end }}
{{ with $block.content.phone }}
<div class="flex items-center">
{{ partial "functions/get_icon" (dict "name" "hero/phone" "attributes" "class=\"w-5 h-5 text-gray-500 dark:text-gray-400 mr-3\"") }}
<a href="tel:{{ . }}" class="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ . }}
</a>
</div>
{{ end }}
{{/* Social Media */}}
{{ with $block.content.social }}
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">Follow Us</h4>
<div class="flex gap-3">
{{ range . }}
<a href="{{ .url }}" target="_blank" rel="noopener"
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-primary-100 dark:hover:bg-primary-900/50 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{/* Prospective Members */}}
{{ with $block.content.prospective }}
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">{{ .title | default "Prospective Members" }}</h4>
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ .text }}</p>
{{ with .button }}
<a href="{{ .url }}"
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg shadow hover:shadow-lg transition-all">
{{ partial "functions/get_icon" (dict "name" "hero/user-plus" "attributes" "class=\"w-5 h-5 mr-2\"") }}
{{ .text }}
</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
{{/* Optional Map Embed */}}
{{ with $block.content.map_embed }}
<div class="mt-12 rounded-2xl overflow-hidden shadow-xl">
<div class="aspect-w-16 aspect-h-9 lg:aspect-h-6">
{{ . | safeHTML }}
</div>
</div>
{{ end }}
{{/* Contact Form (optional) */}}
{{ if $block.content.show_form }}
<div class="mt-12 max-w-2xl mx-auto">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Send us a Message</h3>
<form action="{{ $block.content.form_action | default "#" }}" method="POST" class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
<input type="text" id="name" name="name" required
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email</label>
<input type="email" id="email" name="email" required
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
</div>
</div>
<div>
<label for="subject" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Subject</label>
<input type="text" id="subject" name="subject" required
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
</div>
<div>
<label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message</label>
<textarea id="message" name="message" rows="5" required
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"></textarea>
</div>
<button type="submit"
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow hover:shadow-lg transition-all">
Send Message
</button>
</form>
</div>
</div>
{{ end }}
</div>
@@ -0,0 +1,13 @@
{
"id": "contact-info",
"name": "Contact Info",
"version": "1.0.0",
"license": "MIT",
"category": "content",
"tags": ["contact", "information", "address", "location", "lab", "research", "organization"],
"description": "Display contact information with modern card layout, including address, phone, email, social media, and optional contact form",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/hugo-blox-builder",
"keywords": ["hugo", "static-site", "contact", "information", "address", "research-lab"]
}
@@ -0,0 +1,130 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://hugoblox.com/schemas/blocks/contact-info.json",
"title": "Contact Info Block Schema",
"description": "Schema for the Contact Info block - displays lab contact information in a modern card layout",
"allOf": [
{
"$ref": "../shared/schemas/base-block.json"
},
{
"type": "object",
"properties": {
"block": {
"const": "contact-info"
},
"content": {
"type": "object",
"description": "Content configuration for the contact info block",
"properties": {
"title": {
"type": "string",
"description": "Main section title",
"default": "Contact Us"
},
"subtitle": {
"type": "string",
"description": "Section subtitle"
},
"visit_title": {
"type": "string",
"description": "Title for the visit section",
"default": "Visit Us"
},
"connect_title": {
"type": "string",
"description": "Title for the connect section",
"default": "Connect"
},
"address": {
"type": "object",
"description": "Physical address information",
"properties": {
"lines": {
"type": "array",
"description": "Address lines",
"items": {
"type": "string"
}
}
}
},
"office_hours": {
"type": "array",
"description": "Office hours information",
"items": {
"type": "string"
}
},
"email": {
"type": "string",
"description": "Contact email address"
},
"phone": {
"type": "string",
"description": "Contact phone number"
},
"social": {
"type": "array",
"description": "Social media links",
"items": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"description": "Icon name (e.g., 'brands/x', 'brands/linkedin')"
},
"url": {
"type": "string",
"description": "Social media URL"
}
}
}
},
"prospective": {
"type": "object",
"description": "Information for prospective members",
"properties": {
"title": {
"type": "string",
"default": "Prospective Members"
},
"text": {
"type": "string"
},
"button": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
},
"map_url": {
"type": "string",
"description": "URL to external map service"
},
"map_embed": {
"type": "string",
"description": "HTML embed code for map (e.g., Google Maps iframe)"
},
"show_form": {
"type": "boolean",
"description": "Show contact form",
"default": false
},
"form_action": {
"type": "string",
"description": "Form submission URL"
}
}
}
}
}
]
}
+1 -1
View File
@@ -37,7 +37,7 @@
<div class="text-center">
{{ with $block.content.title }}<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-6xl">{{ . | markdownify }}</h1>{{end}}
{{ with $block.content.text }}<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300">{{ . | $page.RenderString | emojify }}</p>{{end}}
{{ with $block.content.text }}<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">{{ . | $page.RenderString | emojify }}</p>{{end}}
{{ if $block.content.primary_action.url }}
{{ $link := $block.content.primary_action.url }}
@@ -79,7 +79,7 @@ export const HeroBlock = ({content, design, id, icon_svg}) => {
{/* Subtitle/Text */}
{content.text && (
<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300" dangerouslySetInnerHTML={{__html: renderText(content.text)}} />
<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto" dangerouslySetInnerHTML={{__html: renderText(content.text)}} />
)}
{/* Action Buttons */}
+220
View File
@@ -0,0 +1,220 @@
# Logos Block
Display partner, sponsor, or collaborator logos with modern interactive effects and multiple display modes. Perfect for showcasing affiliations, building trust, and highlighting collaborations.
## Features
- **Multiple Display Modes**: Grid, carousel, or marquee layouts
- **Interactive Effects**: Grayscale to color on hover, smooth scaling
- **Clickable Logos**: Link to partner websites
- **Hover Tooltips**: Show partner descriptions on hover
- **Responsive Design**: Adapts perfectly to all screen sizes
- **Smooth Animations**: Professional transitions and effects
- **Flexible Configuration**: Use structured data or folder-based logos
- **Performance Optimized**: Lazy loading and WebP format support
- **Dark Mode Support**: Seamless theme switching
## Display Modes
### Grid Mode (Default)
Classic responsive grid layout with hover effects:
- 2 columns on mobile
- 3-6 columns on larger screens
- Grayscale to color transition on hover
- Scale animation on hover
### Carousel Mode
Auto-scrolling horizontal carousel:
- Continuous smooth scrolling
- Pauses on hover
- Duplicate items for seamless loop
- Ideal for many logos
### Marquee Mode
Traditional marquee scroll effect:
- Continuous horizontal movement
- Lightweight and performant
- Good for header/footer placement
## Usage
### Basic Grid Layout
```yaml
sections:
- block: logos
content:
title: Our Partners & Collaborators
subtitle: Working with leading institutions worldwide
text: We collaborate with top universities and research centers
logos:
- name: MIT
image: partners/mit.png
url: https://mit.edu
description: Massachusetts Institute of Technology
- name: Stanford University
image: partners/stanford.svg
url: https://stanford.edu
description: Leading research university
- name: Google Research
image: partners/google.png
url: https://research.google
description: AI and ML research
- name: Microsoft Research
image: partners/microsoft.svg
url: https://www.microsoft.com/research
description: Computing research lab
design:
display_mode: grid
css_class: "bg-gray-50 dark:bg-gray-900"
```
### Carousel Display
```yaml
sections:
- block: logos
content:
title: Trusted By Industry Leaders
logos:
# Add 8-12 logos for best carousel effect
- name: Company 1
image: sponsors/company1.png
url: https://example.com
# ... more logos
design:
display_mode: carousel
```
### Legacy Folder Mode
For backward compatibility or quick setup:
```yaml
sections:
- block: logos
content:
title: Our Sponsors
logo_folder: sponsors # Loads all images from assets/media/sponsors/
design:
display_mode: grid
```
## Options
### Content Options
- **title**: Main heading for the section
- **subtitle**: Optional subtitle (shown in accent color)
- **text**: Description text below title
- **logos**: Array of logo configurations
- `name`: Partner/sponsor name (shown in tooltip)
- `image`: Path relative to `assets/media/`
- `url`: Website URL
- `external`: Open in new tab (default: true)
- `description`: Hover tooltip text
- **logo_folder**: Legacy option to load all images from a folder
- **cta**: Optional call-to-action button
- `text`: Button text
- `url`: Button link
- `icon`: Optional icon
### Design Options
- **display_mode**: Choose layout style
- `grid`: Responsive grid (default)
- `carousel`: Auto-scrolling carousel
- `marquee`: Classic marquee effect
- **show_pattern**: Add decorative background pattern (default: false)
## Image Guidelines
### Recommended Formats
- **SVG**: Best for logos (scalable, small file size)
- **PNG**: For logos with transparency
- **WebP**: Modern format with best compression
### Size Recommendations
- **Grid Mode**: 280x140px (will be auto-fitted)
- **Carousel Mode**: 280x140px minimum
- **Aspect Ratio**: 2:1 or square works best
### Optimization Tips
1. Use SVG when possible for crisp rendering
2. Optimize PNGs with tools like TinyPNG
3. Keep file sizes under 50KB per logo
4. Use consistent dimensions for visual harmony
## Styling
### Hover Effects
- Grayscale filter removed on hover
- Opacity increases from 60% to 100%
- Smooth scale transformation (110%)
- Background highlight on hover
### Custom CSS
Add custom styles to enhance the block:
```css
/* Example: Custom logo sizing */
.logos-block img {
max-height: 80px;
}
/* Example: Remove grayscale on specific logo */
.partner-featured img {
filter: none !important;
opacity: 1 !important;
}
```
## Best Practices
1. **Logo Quality**: Use high-resolution logos (2x display size)
2. **Consistency**: Keep logos similar in visual weight
3. **Accessibility**: Always provide alt text via `name` field
4. **Performance**: Optimize images before adding
5. **Responsive**: Test on mobile devices
6. **Links**: Verify all external links work
7. **Copyright**: Ensure you have permission to display logos
## Examples
### Research Lab Partners
```yaml
logos:
- name: National Science Foundation
image: partners/nsf.svg
url: https://nsf.gov
description: Federal funding agency
- name: NIH
image: partners/nih.png
url: https://nih.gov
description: National Institutes of Health
```
### Conference Sponsors
```yaml
logos:
- name: Gold Sponsor - Microsoft
image: sponsors/microsoft-gold.png
url: https://microsoft.com
description: Gold level sponsor
```
### Industry Collaborations
```yaml
logos:
- name: OpenAI
image: collaborators/openai.svg
url: https://openai.com
description: AI research partnership
```
## Animation Speed
To adjust animation speeds, modify the CSS:
```yaml
design:
css_style: |
.animate-marquee { animation-duration: 20s; }
.animate-scroll { animation-duration: 30s; }
```
+235
View File
@@ -0,0 +1,235 @@
{{/* Hugo Blox: Logos Block */}}
{{/* Documentation: https://hugoblox.com/blocks/ */}}
{{/* License: https://github.com/HugoBlox/hugo-blox-builder/blob/main/LICENSE.md */}}
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $display_mode := $block.design.display_mode | default "grid" }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
<div class="relative py-16 sm:py-20 lg:py-24 overflow-hidden">
{{/* Background Pattern */}}
{{ if $block.design.show_pattern }}
<div class="absolute inset-0 opacity-5">
<div class="absolute inset-0 bg-gradient-to-br from-primary-100 to-transparent dark:from-primary-900 dark:to-transparent"></div>
</div>
{{ end }}
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{/* Section Header */}}
<div class="text-center max-w-3xl mx-auto mb-12">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white tracking-tight mb-4">
{{ . }}
</h2>
{{ end }}
{{ with $subtitle }}
<p class="text-xl text-primary-600 dark:text-primary-400 font-medium mb-2">
{{ . }}
</p>
{{ end }}
{{ with $text }}
<p class="text-lg text-gray-600 dark:text-gray-400">
{{ . }}
</p>
{{ end }}
</div>
{{/* Grid Display Mode */}}
{{ if eq $display_mode "grid" }}
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-8 lg:gap-12 items-center">
{{/* Logo Items from Data */}}
{{ if $block.content.logos }}
{{ range $block.content.logos }}
<div class="group relative">
{{ $link := .url | default "#" }}
{{ $target := cond .external "_blank" "_self" }}
<a href="{{ $link }}" target="{{ $target }}" rel="{{ if .external }}noopener{{ end }}"
class="block p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-300"
{{ with .name }}title="{{ . }}"{{ end }}>
<div class="relative">
{{/* Logo Image */}}
{{ if .image }}
{{ $img := resources.Get (printf "media/%s" .image) }}
{{ if $img }}
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
{{ if not $isSVG }}
{{ $img = $img.Fit "280x140 webp" }}
{{ end }}
<img src="{{ $img.RelPermalink }}"
alt="{{ .name | default "Partner logo" }}"
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
class="h-12 sm:h-14 lg:h-16 w-auto mx-auto object-contain filter grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-300 group-hover:scale-110"
loading="lazy">
{{ end }}
{{ end }}
{{/* Tooltip on Hover */}}
{{ with .description }}
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none whitespace-nowrap z-10">
{{ . }}
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-l-transparent border-r-4 border-r-transparent border-t-4 border-t-gray-900 dark:border-t-gray-700"></div>
</div>
{{ end }}
</div>
</a>
</div>
{{ end }}
{{/* Logo Images from Folder (Legacy Support) */}}
{{ else if $block.content.logo_folder }}
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $block.content.logo_folder) }}
{{ range $images }}
<div class="group relative">
<div class="block p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-300">
{{ $image := . }}
{{ $isSVG := eq $image.MediaType.SubType "svg" }}
{{ if not $isSVG }}
{{ $image = $image.Fit "280x140 webp" }}
{{ end }}
<img src="{{ $image.RelPermalink }}"
alt="Partner logo"
{{ if not $isSVG }}width="{{ $image.Width }}" height="{{ $image.Height }}"{{ end }}
class="h-12 sm:h-14 lg:h-16 w-auto mx-auto object-contain filter grayscale opacity-60 hover:grayscale-0 hover:opacity-100 transition-all duration-300 hover:scale-110"
loading="lazy">
</div>
</div>
{{ end }}
{{ end }}
</div>
{{/* Carousel Display Mode */}}
{{ else if eq $display_mode "carousel" }}
<div class="relative overflow-hidden">
<div class="flex animate-scroll space-x-12 lg:space-x-16">
{{/* Duplicate items for continuous scroll */}}
{{ $items := slice }}
{{ if $block.content.logos }}
{{ $items = $block.content.logos }}
{{ else if $block.content.logo_folder }}
{{ $images := resources.Match (printf "media/%s/*.{svg,png,jpg,webp}" $block.content.logo_folder) }}
{{ range $images }}
{{ $items = $items | append (dict "image" .Name) }}
{{ end }}
{{ end }}
{{/* First set of logos */}}
{{ range $items }}
<div class="flex-shrink-0">
{{ if .image }}
{{ $img := resources.Get (printf "media/%s" .image) }}
{{ if $img }}
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
{{ if not $isSVG }}
{{ $img = $img.Fit "280x140 webp" }}
{{ end }}
<img src="{{ $img.RelPermalink }}"
alt="{{ .name | default "Partner logo" }}"
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
class="h-16 lg:h-20 w-auto object-contain opacity-70 hover:opacity-100 transition-opacity"
loading="lazy">
{{ end }}
{{ end }}
</div>
{{ end }}
{{/* Duplicate for seamless loop */}}
{{ range $items }}
<div class="flex-shrink-0">
{{ if .image }}
{{ $img := resources.Get (printf "media/%s" .image) }}
{{ if $img }}
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
{{ if not $isSVG }}
{{ $img = $img.Fit "280x140 webp" }}
{{ end }}
<img src="{{ $img.RelPermalink }}"
alt="{{ .name | default "Partner logo" }}"
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
class="h-16 lg:h-20 w-auto object-contain opacity-70 hover:opacity-100 transition-opacity"
loading="lazy">
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
</div>
{{/* Marquee Display Mode */}}
{{ else if eq $display_mode "marquee" }}
<div class="relative">
<div class="overflow-hidden">
<div class="flex animate-marquee whitespace-nowrap">
{{ if $block.content.logos }}
{{ range $block.content.logos }}
<div class="mx-8 inline-flex items-center">
{{ if .image }}
{{ $img := resources.Get (printf "media/%s" .image) }}
{{ if $img }}
{{ $isSVG := eq $img.MediaType.SubType "svg" }}
{{ if not $isSVG }}
{{ $img = $img.Fit "280x140 webp" }}
{{ end }}
<img src="{{ $img.RelPermalink }}"
alt="{{ .name | default "Partner logo" }}"
{{ if not $isSVG }}width="{{ $img.Width }}" height="{{ $img.Height }}"{{ end }}
class="h-14 lg:h-16 w-auto object-contain opacity-70"
loading="lazy">
{{ end }}
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
{{/* Call to Action */}}
{{ with $block.content.cta }}
<div class="mt-12 text-center">
<a href="{{ .url }}"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow hover:shadow-lg transition-all duration-300">
{{ .text }}
{{ with .icon }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
{{ end }}
</a>
</div>
{{ end }}
</div>
</div>
{{/* Custom CSS for Animations */}}
<style>
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@keyframes scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 30s linear infinite;
}
.animate-scroll {
animation: scroll 40s linear infinite;
}
.animate-marquee:hover,
.animate-scroll:hover {
animation-play-state: paused;
}
</style>
@@ -0,0 +1,13 @@
{
"id": "logos",
"name": "Logos",
"version": "1.0.0",
"license": "MIT",
"category": "content",
"tags": ["logos", "partners", "sponsors", "collaborators", "brands", "clients", "affiliations"],
"description": "Display partner, sponsor, or collaborator logos with interactive effects and multiple display modes",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/hugo-blox-builder",
"keywords": ["hugo", "static-site", "logos", "partners", "sponsors", "carousel", "marquee"]
}
@@ -0,0 +1,106 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://hugoblox.com/schemas/blocks/logos.json",
"title": "Logos Block Schema",
"description": "Schema for the Logos block - displays partner/sponsor logos with various display modes",
"allOf": [
{
"$ref": "../shared/schemas/base-block.json"
},
{
"type": "object",
"properties": {
"block": {
"const": "logos"
},
"content": {
"type": "object",
"description": "Content configuration for the logos block",
"properties": {
"title": {
"type": "string",
"description": "Main section title"
},
"subtitle": {
"type": "string",
"description": "Section subtitle"
},
"text": {
"type": "string",
"description": "Section description text"
},
"logos": {
"type": "array",
"description": "Array of logo items with details",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Partner/sponsor name"
},
"image": {
"type": "string",
"description": "Path to logo image relative to assets/media/"
},
"url": {
"type": "string",
"description": "Link to partner website"
},
"external": {
"type": "boolean",
"description": "Open link in new tab",
"default": true
},
"description": {
"type": "string",
"description": "Short description shown on hover"
}
}
}
},
"logo_folder": {
"type": "string",
"description": "Legacy: Folder containing logo images (deprecated, use logos array instead)"
},
"cta": {
"type": "object",
"description": "Call to action button",
"properties": {
"text": {
"type": "string",
"description": "Button text"
},
"url": {
"type": "string",
"description": "Button URL"
},
"icon": {
"type": "string",
"description": "Optional icon name"
}
}
}
}
},
"design": {
"type": "object",
"description": "Design options for the logos block",
"properties": {
"display_mode": {
"type": "string",
"enum": ["grid", "carousel", "marquee"],
"description": "How to display the logos",
"default": "grid"
},
"show_pattern": {
"type": "boolean",
"description": "Show decorative background pattern",
"default": false
}
}
}
}
}
]
}
@@ -0,0 +1,292 @@
# Research Areas Block
Display your research focus areas with stunning visuals, metrics, and interactive layouts. Perfect for research labs, academic groups, and R&D departments to showcase their areas of expertise.
## Features
- **Multiple Layout Options**: Cards, hexagon, or timeline layouts
- **Rich Visual Options**: Icons, emojis, images, or gradient backgrounds
- **Metrics Display**: Team size, publications, funding
- **Interactive Elements**: Hover effects, status badges, CTAs
- **Topic Keywords**: Highlight key research topics
- **Responsive Design**: Beautiful on all devices
- **Dark Mode Support**: Seamless theme switching
- **Performance Optimized**: Lazy loading and WebP support
## Layout Options
### Cards Layout (Default)
Modern card-based design with:
- Large visual header (image/icon/emoji)
- Rich content area
- Hover animations
- Status badges
- Metrics display
### Hexagon Layout
Unique hexagonal grid for visual impact:
- Geometric design
- Gradient backgrounds
- Compact information display
- Great for 3-6 areas
### Timeline Layout
Chronological or priority-based display:
- Vertical timeline
- Alternating left/right cards
- Visual progression
- Good for research evolution
## Usage Examples
### Basic Research Areas with Cards
```yaml
sections:
- block: research-areas
content:
title: Research Focus Areas
subtitle: Advancing Science Through Innovation
text: Our lab conducts cutting-edge research across multiple domains
items:
- name: Computational Biology
description: Developing algorithms for genomic analysis and protein structure prediction
emoji: 🧬
gradient: from-green-400 to-blue-500
status: active
topics:
- Genomics
- Proteomics
- Bioinformatics
- Systems Biology
team_size: 12 researchers
publications: 45 papers
funding: $2.5M
cta:
text: Learn More
url: /research/computational-biology
- name: Machine Learning
description: Advancing deep learning methods for scientific discovery
emoji: 🤖
gradient: from-purple-400 to-pink-500
status: active
topics:
- Deep Learning
- Neural Networks
- Computer Vision
- NLP
team_size: 8 researchers
publications: 32 papers
funding: $1.8M
- name: Materials Science
description: Designing novel materials through computational modeling
emoji: 🔬
gradient: from-orange-400 to-red-500
status: emerging
topics:
- Nanomaterials
- Quantum Materials
- Polymers
team_size: 6 researchers
publications: 28 papers
funding: $1.2M
design:
layout: cards
```
### Using Icons Instead of Emojis
```yaml
items:
- name: Artificial Intelligence
description: Building next-generation AI systems
icon: hero/cpu-chip
gradient: from-blue-500 to-indigo-600
# ... rest of configuration
```
### Using Images
```yaml
items:
- name: Climate Science
description: Understanding and mitigating climate change
image: research/climate-hero.jpg
status: active
# ... rest of configuration
```
### Hexagon Layout for Visual Impact
```yaml
sections:
- block: research-areas
content:
title: Core Research Pillars
items:
- name: AI & ML
description: Artificial intelligence research
icon: hero/cpu-chip
gradient: from-blue-400 to-purple-600
- name: Quantum
description: Quantum computing systems
icon: hero/sparkles
gradient: from-green-400 to-teal-600
# ... more items
design:
layout: hexagon
```
### Timeline Layout for Evolution
```yaml
sections:
- block: research-areas
content:
title: Research Evolution
subtitle: Our Journey of Discovery
items:
- name: Foundation (2015-2018)
description: Established core ML research
icon: hero/academic-cap
- name: Expansion (2018-2021)
description: Added biomedical applications
icon: hero/beaker
- name: Current Focus (2021-Present)
description: AI for scientific discovery
icon: hero/sparkles
design:
layout: timeline
```
## Configuration Options
### Content Options
- **title**: Main section heading
- **subtitle**: Section subtitle (accent color)
- **text**: Description paragraph
- **items**: Array of research areas
- `name`: Area name (required)
- `description`: Area description (required)
- `icon`: Icon identifier (e.g., 'hero/beaker')
- `emoji`: Emoji alternative to icon
- `image`: Image path relative to assets/media/
- `gradient`: Tailwind gradient classes
- `url`: Link to detailed page
- `status`: 'active', 'emerging', or 'planning'
- `topics`: Array of keywords/topics
- `team_size`: Team member count
- `publications`: Publication count
- `funding`: Funding amount
- `cta`: Individual area CTA with text and url
- **cta**: Global call-to-action button
### Design Options
- **layout**: Visual layout style
- `cards`: Modern card grid (default)
- `hexagon`: Hexagonal grid
- `timeline`: Vertical timeline
## Visual Guidelines
### Gradients
Use Tailwind gradient classes for visual appeal:
```yaml
gradient: from-blue-400 to-purple-600
gradient: from-green-500 to-teal-400
gradient: from-orange-400 to-red-500
```
### Icons vs Emojis vs Images
- **Icons**: Professional, consistent style
- **Emojis**: Friendly, universal, no loading
- **Images**: Rich visuals, specific imagery
### Status Badges
- `active`: Green - Currently active research
- `emerging`: Yellow - Growing area
- `planning`: Blue - Future direction
## Best Practices
1. **Consistency**: Use the same visual type (icons OR emojis OR images) across all areas
2. **Metrics**: Include quantifiable achievements
3. **Keywords**: 3-5 relevant topics per area
4. **Descriptions**: Keep concise (50-100 words)
5. **CTAs**: Link to detailed pages for each area
6. **Gradients**: Choose complementary colors
7. **Status**: Be transparent about research maturity
## Responsive Behavior
### Cards Layout
- Mobile: Single column
- Tablet: 2 columns
- Desktop: 3 columns
### Hexagon Layout
- Mobile: Stacked hexagons
- Desktop: Honeycomb grid
### Timeline Layout
- Mobile: Single column timeline
- Desktop: Alternating left/right
## Performance Tips
1. **Images**: Optimize to <100KB
2. **WebP Format**: Use for better compression
3. **Lazy Loading**: Automatic for images
4. **Icon Usage**: Prefer icons/emojis over images for speed
## Accessibility
- Semantic HTML structure
- Proper heading hierarchy
- Alt text for images
- Keyboard navigable
- Screen reader friendly
- High contrast for text
## Examples by Field
### AI/ML Lab
```yaml
items:
- name: Deep Learning
emoji: 🧠
gradient: from-purple-500 to-pink-500
- name: Computer Vision
emoji: 👁️
gradient: from-blue-500 to-cyan-500
- name: NLP
emoji: 💬
gradient: from-green-500 to-teal-500
```
### Biology Lab
```yaml
items:
- name: Genomics
emoji: 🧬
gradient: from-green-400 to-emerald-600
- name: Cell Biology
emoji: 🦠
gradient: from-purple-400 to-indigo-600
- name: Neuroscience
emoji: 🧠
gradient: from-pink-400 to-rose-600
```
### Physics Lab
```yaml
items:
- name: Quantum Computing
emoji: ⚛️
gradient: from-indigo-500 to-purple-600
- name: Particle Physics
emoji: 🌌
gradient: from-blue-500 to-indigo-600
- name: Condensed Matter
emoji: 💎
gradient: from-cyan-500 to-blue-600
```
@@ -0,0 +1,263 @@
{{/* Hugo Blox: Research Areas Block */}}
{{/* Documentation: https://hugoblox.com/blocks/ */}}
{{/* License: https://github.com/HugoBlox/hugo-blox-builder/blob/main/LICENSE.md */}}
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $layout := $block.design.layout | default "cards" }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
<div class="py-16 sm:py-20 lg:py-24 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
{{/* Section Header */}}
<div class="text-center max-w-3xl mx-auto mb-12 lg:mb-16">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white tracking-tight mb-4">
{{ . }}
</h2>
{{ end }}
{{ with $subtitle }}
<p class="text-xl text-primary-600 dark:text-primary-400 font-medium mb-3">
{{ . }}
</p>
{{ end }}
{{ with $text }}
<p class="text-lg text-gray-600 dark:text-gray-400">
{{ . }}
</p>
{{ end }}
</div>
{{/* Cards Layout */}}
{{ if eq $layout "cards" }}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
{{ range $idx, $item := $block.content.items }}
<div class="group relative">
<div class="h-full bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 overflow-hidden flex flex-col">
{{/* Card Image/Icon Header */}}
{{ $header_content := "" }}
{{ if $item.image }}
{{ $img := resources.Get (printf "media/%s" $item.image) }}
{{ if $img }}
{{ $img = $img.Fill "600x400 Center webp" }}
{{ $header_content = printf "<img src=\"%s\" alt=\"%s\" class=\"w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500\">" $img.RelPermalink $item.name }}
{{ end }}
{{ else if $item.icon }}
{{ $icon_html := partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-24 h-24 text-white/80 group-hover:scale-110 transition-transform duration-300\"") }}
{{ $header_content = printf "<div class=\"absolute inset-0 flex items-center justify-center\">%s</div>" $icon_html }}
{{ end }}
{{ if and $item.cta.url $header_content }}
<a href="{{ $item.cta.url }}" class="block relative h-48 bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} overflow-hidden">
{{ $header_content | safeHTML }}
</a>
{{ else }}
<div class="relative h-48 bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} overflow-hidden">
{{ $header_content | safeHTML }}
</div>
{{ end }}
{{/* Status Badge */}}
{{ with $item.status }}
<div class="absolute top-4 right-4 z-10">
<span class="px-3 py-1 bg-white/90 dark:bg-gray-900/90 backdrop-blur text-xs font-semibold rounded-full
{{ if eq . "active" }}text-green-600 dark:text-green-400
{{ else if eq . "emerging" }}text-yellow-600 dark:text-yellow-400
{{ else if eq . "planning" }}text-blue-600 dark:text-blue-400
{{ end }}">
{{ . | title }}
</span>
</div>
{{ end }}
{{/* Card Content - Flexible layout */}}
<div class="flex flex-col flex-1 p-6">
{{/* Main Content */}}
<div class="flex-1">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
{{ if $item.url }}
<a href="{{ $item.url }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ $item.name }}
{{ partial "functions/get_icon" (dict "name" "hero/arrow-top-right-on-square" "attributes" "class=\"inline-block w-4 h-4 ml-1\"") }}
</a>
{{ else }}
{{ $item.name }}
{{ end }}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
{{ $item.description }}
</p>
{{/* Key Topics/Keywords */}}
{{ with $item.topics }}
<div class="flex flex-wrap gap-2 mb-6">
{{ range first 3 . }}
<span class="inline-block px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded">
{{ . }}
</span>
{{ end }}
{{ if gt (len .) 3 }}
<span class="inline-block px-2 py-1 text-gray-500 dark:text-gray-400 text-xs">
+{{ sub (len .) 3 }} more
</span>
{{ end }}
</div>
{{ end }}
</div>
{{/* Footer Section - Always at bottom */}}
<div class="mt-auto">
{{/* Stats/Metrics */}}
{{ if or $item.team_size $item.publications $item.funding }}
<div class="grid grid-cols-3 gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 mb-4">
{{ with $item.team_size }}
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
{{ partial "functions/get_icon" (dict "name" "hero/user-group" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
<span class="text-xs leading-tight">{{ . | replaceRE " researchers" "" }}</span>
<span class="text-xs text-gray-400">team</span>
</div>
{{ end }}
{{ with $item.publications }}
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
{{ partial "functions/get_icon" (dict "name" "hero/document-text" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
<span class="text-xs leading-tight">{{ . | replaceRE " papers" "" }}</span>
<span class="text-xs text-gray-400">papers</span>
</div>
{{ end }}
{{ with $item.funding }}
<div class="flex flex-col items-center text-center text-gray-500 dark:text-gray-400">
{{ partial "functions/get_icon" (dict "name" "hero/currency-dollar" "attributes" "class=\"w-4 h-4 mx-auto mb-1\"") }}
<span class="text-xs leading-tight">{{ . }}</span>
<span class="text-xs text-gray-400">funding</span>
</div>
{{ end }}
</div>
{{ end }}
{{/* Call to Action */}}
{{ with $item.cta }}
<a href="{{ .url }}"
class="inline-flex items-center w-full justify-center px-4 py-2 bg-primary-50 dark:bg-primary-900/20 text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium text-sm rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-all duration-300">
{{ .text }}
{{ partial "functions/get_icon" (dict "name" "hero/arrow-right" "attributes" "class=\"ml-1 w-4 h-4\"") }}
</a>
{{ end }}
</div>
</div>
</div>
</div>
{{ end }}
</div>
{{/* Hexagon Layout */}}
{{ else if eq $layout "hexagon" }}
<div class="flex flex-wrap justify-center gap-8 lg:gap-4">
{{ range $idx, $item := $block.content.items }}
<div class="hexagon-container group">
<div class="hexagon bg-gradient-to-br {{ $item.gradient | default "from-primary-400 to-secondary-400" }} shadow-xl hover:shadow-2xl transition-all duration-300">
<div class="hexagon-content">
{{ if $item.icon }}
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-12 h-12 text-white mb-3\"") }}
{{ end }}
<h3 class="text-lg font-bold text-white mb-2">{{ $item.name }}</h3>
<p class="text-white/90 text-sm">{{ $item.description | truncate 60 }}</p>
</div>
</div>
</div>
{{ end }}
</div>
{{/* Timeline Layout */}}
{{ else if eq $layout "timeline" }}
<div class="relative">
{{/* Vertical Line */}}
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-gradient-to-b from-primary-400 to-secondary-400"></div>
<div class="space-y-12">
{{ range $idx, $item := $block.content.items }}
<div class="relative flex items-center {{ if eq (mod $idx 2) 0 }}justify-start{{ else }}justify-end{{ end }}">
{{/* Timeline dot */}}
<div class="absolute left-1/2 transform -translate-x-1/2 w-8 h-8 bg-white dark:bg-gray-800 border-4 border-primary-500 rounded-full z-10"></div>
<div class="w-5/12 {{ if eq (mod $idx 2) 1 }}mr-auto{{ end }}">
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow">
<div class="flex items-center gap-3 mb-3">
{{ if $item.icon }}
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-5 h-5 text-primary-600 dark:text-primary-400\"") }}
</div>
{{ end }}
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{{ $item.name }}</h3>
</div>
<p class="text-gray-600 dark:text-gray-400">{{ $item.description }}</p>
</div>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
{{/* Global Call to Action */}}
{{ with $block.content.cta }}
<div class="mt-12 text-center">
<a href="{{ .url }}"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow hover:shadow-lg transition-all duration-300">
{{ .text }}
{{ with .icon }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
{{ end }}
</a>
</div>
{{ end }}
</div>
</div>
{{/* Custom CSS for Hexagon Layout */}}
{{ if eq $layout "hexagon" }}
<style>
.hexagon-container {
position: relative;
width: 200px;
height: 200px;
margin: 30px;
}
.hexagon {
position: absolute;
width: 100%;
height: 100%;
clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.hexagon:hover {
transform: scale(1.05);
}
.hexagon-content {
text-align: center;
padding: 20px;
}
@media (max-width: 768px) {
.hexagon-container {
width: 150px;
height: 150px;
}
}
</style>
{{ end }}
@@ -0,0 +1,13 @@
{
"id": "research-areas",
"name": "Research Areas",
"version": "1.0.0",
"license": "MIT",
"category": "content",
"tags": ["research", "areas", "focus", "topics", "science", "lab", "academic", "showcase"],
"description": "Display research focus areas with rich visuals, metrics, and interactive layouts",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/hugo-blox-builder",
"keywords": ["hugo", "static-site", "research", "areas", "academic", "lab", "science"]
}
@@ -0,0 +1,139 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://hugoblox.com/schemas/blocks/research-areas.json",
"title": "Research Areas Block Schema",
"description": "Schema for the Research Areas block - displays research focus areas with rich visuals and metrics",
"allOf": [
{
"$ref": "../shared/schemas/base-block.json"
},
{
"type": "object",
"properties": {
"block": {
"const": "research-areas"
},
"content": {
"type": "object",
"description": "Content configuration for the research areas block",
"properties": {
"title": {
"type": "string",
"description": "Section title"
},
"subtitle": {
"type": "string",
"description": "Section subtitle"
},
"text": {
"type": "string",
"description": "Section description text"
},
"items": {
"type": "array",
"description": "Array of research area items",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Research area name",
"required": true
},
"description": {
"type": "string",
"description": "Research area description",
"required": true
},
"icon": {
"type": "string",
"description": "Icon identifier (e.g., 'hero/beaker', 'devicon/python')"
},
"emoji": {
"type": "string",
"description": "Emoji as alternative to icon"
},
"image": {
"type": "string",
"description": "Image path relative to assets/media/"
},
"gradient": {
"type": "string",
"description": "Tailwind gradient classes (e.g., 'from-blue-400 to-purple-400')"
},
"url": {
"type": "string",
"description": "Link to detailed page"
},
"status": {
"type": "string",
"enum": ["active", "emerging", "planning"],
"description": "Research area status"
},
"topics": {
"type": "array",
"description": "Key topics/keywords",
"items": {
"type": "string"
}
},
"team_size": {
"type": "string",
"description": "Number of team members (e.g., '12 researchers')"
},
"publications": {
"type": "string",
"description": "Number of publications (e.g., '45 papers')"
},
"funding": {
"type": "string",
"description": "Funding amount (e.g., '$2.5M')"
},
"cta": {
"type": "object",
"description": "Call to action for this area",
"properties": {
"text": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
},
"cta": {
"type": "object",
"description": "Global call to action",
"properties": {
"text": {
"type": "string"
},
"url": {
"type": "string"
},
"icon": {
"type": "string"
}
}
}
}
},
"design": {
"type": "object",
"description": "Design options for the research areas block",
"properties": {
"layout": {
"type": "string",
"enum": ["cards", "hexagon", "timeline"],
"description": "Layout style for research areas",
"default": "cards"
}
}
}
}
}
]
}
+207 -39
View File
@@ -1,59 +1,227 @@
# Stats Block
**Let your numbers tell your success story**
Display impressive statistics with modern animations, icons, and engaging visual effects. Perfect for showcasing lab achievements, research metrics, and organizational highlights.
Showcase your achievements, milestones, and key metrics with the Stats block - a powerful component that transforms dry numbers into compelling visual statements that build credibility and demonstrate your impact.
## Key Features
## ✨ Key Features
- **🎬 Counter Animations**: Numbers count up on scroll with easing
- **📊 Visual Icons**: Icons for each statistic enhance comprehension
- **🎨 Multiple Layouts**: Cards, compact, or minimal display modes
- **📱 Responsive Design**: Adapts beautifully to all screen sizes
- **🌙 Dark Mode Ready**: Seamless theme switching
- **⚡ Performance**: CSS-first animations with minimal JavaScript
- **🎯 Intersection Observer**: Animations trigger when scrolled into view
- **✨ Hover Effects**: Subtle interactions for engagement
- **Bold Typography**: Extra-large numbers (4xl to 6xl) that command attention
- **Three-Column Grid**: Perfect layout for highlighting your top 3 metrics
- **Visual Separators**: Subtle borders that organize information without clutter
- **Responsive Design**: Adapts beautifully from mobile to desktop
- **Markdown Support**: Rich formatting for both statistics and descriptions
- **Optional Header**: Add context with title and descriptive text
- **Professional Styling**: Clean, modern design that builds trust
## Layout Options
## 🎯 Perfect For
### Cards Layout (Default)
Modern card-based design featuring:
- Individual cards with shadows and hover effects
- Icons with background circles
- Gradient hover states
- Staggered animation entry
- Optimal for 2-4 statistics
- **Business Metrics**: Revenue, customers served, years in business
- **Product Statistics**: Downloads, users, installations, success rates
- **Company Achievements**: Awards won, projects completed, team size
- **Performance Data**: Uptime, response times, satisfaction scores
- **Social Proof**: Followers, testimonials, reviews, case studies
- **Impact Metrics**: Lives changed, money saved, time reduced
### Compact Layout
Single card with divided sections:
- Unified background
- Bordered divisions
- Space-efficient
- Good for 3-6 statistics
## 🚀 Why Choose Stats Block?
### Minimal Layout
Clean, icon-centric design:
- No backgrounds or borders
- Focus on typography
- Suitable for headers/footers
- Works well with many statistics
**Instant Credibility**: Large, prominent numbers immediately establish authority and success
## Usage Examples
**Social Proof**: Quantified achievements provide concrete evidence of your value
### Research Lab Metrics (Enhanced)
```yaml
sections:
- block: stats
content:
title: Impact by the Numbers
text: Driving scientific discovery through measurable achievements
items:
- statistic: "150+"
description: Publications in top-tier journals
sub_metric: Nature, Science, Cell, PNAS
icon: hero/document-text
- statistic: "25"
description: Brilliant researchers and scientists
sub_metric: From 12 countries worldwide
icon: hero/user-group
- statistic: "$12M"
description: Active research funding
sub_metric: NSF, NIH, DOE grants
icon: hero/currency-dollar
- statistic: "8"
description: Breakthrough discoveries
sub_metric: Patent applications filed
icon: hero/light-bulb
design:
layout: cards
css_class: "bg-gradient-to-b from-primary-50 to-white dark:from-primary-900/20 dark:to-gray-800"
```
**Visual Impact**: Bold typography and clean layout make statistics impossible to ignore
### University Department Stats
```yaml
content:
items:
- statistic: "2,500"
description: Students enrolled
icon: hero/academic-cap
- statistic: "95%"
description: Graduation rate
icon: hero/chart-bar
- statistic: "40+"
description: Countries represented
icon: hero/globe-alt
design:
layout: compact
```
**Trust Building**: Professional presentation of metrics builds confidence in your offering
### Startup Metrics
```yaml
content:
items:
- statistic: "$2.5M"
description: Funding raised
icon: hero/banknotes
- statistic: "10K+"
description: Active users
icon: hero/users
- statistic: "98.5%"
description: Uptime reliability
icon: hero/chart-line
design:
layout: minimal
```
## 📊 Psychology of Numbers
## Animation Features
- **Cognitive Ease**: Large numbers are processed faster by the human brain
- **Authority Signaling**: Impressive statistics position you as an industry leader
- **Decision Shortcuts**: Metrics help visitors make quick trust decisions
- **Competitive Advantage**: Quantified success differentiates you from competitors
### Counter Animation
- **Easing**: Smooth ease-out cubic function
- **Duration**: 2 seconds for optimal viewing
- **Trigger**: Intersection Observer when 50% visible
- **Number Extraction**: Automatically extracts numbers from text
## 💡 Best Practices
### Entrance Animation
- **Staggered Entry**: Items animate in sequence
- **Fade + Slide**: Combines opacity and translateY
- **Delay Timing**: 150ms between items
- **Threshold**: Triggers 100px before entering viewport
- Choose your three most impressive and relevant metrics
- Use round numbers when possible (1M+ instead of 987,432)
- Include context in descriptions ("Happy Customers", "Projects Delivered")
- Update regularly to maintain accuracy and freshness
- Order statistics by impact or importance
### Hover Effects
- **Scale Transform**: Icons scale 110% on hover
- **Color Transitions**: Text color changes
- **Background Glow**: Subtle gradient overlay
- **Shadow Enhancement**: Deeper shadows on hover
## 🎨 Visual Design
## Configuration Options
- **High Contrast**: Bold numbers against clean backgrounds
- **Generous Spacing**: Prevents overwhelming busy appearance
- **Consistent Sizing**: Uniform treatment creates professional cohesion
- **Mobile-First**: Stacks gracefully on smaller screens
### Content Fields
- **title**: Optional section heading
- **text**: Optional description
- **items**: Array of statistics
- `statistic`: Number or value (required)
- `description`: What the number represents (required)
- `icon`: Icon identifier (optional)
- `sub_metric`: Additional context (optional)
Perfect for SaaS companies, agencies, consultants, and any business that has measurable achievements worth celebrating. Turn your success metrics into powerful marketing assets that speak louder than words.
### Design Options
- **layout**: Visual layout
- `cards`: Individual cards (default)
- `compact`: Single divided card
- `minimal`: Clean typography focus
## Icon Guidelines
### Recommended Icons for Common Stats
**Publications/Research:**
- `hero/document-text` - Publications
- `hero/beaker` - Research projects
- `hero/light-bulb` - Discoveries/innovations
- `hero/trophy` - Awards/recognition
**People/Team:**
- `hero/user-group` - Team members
- `hero/academic-cap` - Students/graduates
- `hero/users` - Community/users
- `hero/heart` - Satisfaction ratings
**Financial/Business:**
- `hero/currency-dollar` - Funding/revenue
- `hero/banknotes` - Investments
- `hero/chart-bar` - Growth metrics
- `hero/building-office` - Locations
**Technology/Digital:**
- `hero/cpu-chip` - Computing power
- `hero/globe-alt` - Global reach
- `hero/chart-line` - Performance metrics
- `hero/server` - Infrastructure
## Performance Notes
### JavaScript Usage
Minimal JavaScript for:
- Intersection Observer (viewport detection)
- Counter animation (number progression)
- Modern browser APIs only
### CSS Animations
- Hardware accelerated transforms
- Optimized for 60fps
- Respects `prefers-reduced-motion`
- Fallbacks for older browsers
### Image Optimization
- SVG icons preferred (scalable, fast)
- WebP fallbacks for raster images
- Lazy loading for non-critical images
## Best Practices
1. **Number Selection**: Choose impactful, verifiable metrics
2. **Descriptions**: Keep concise but descriptive
3. **Icons**: Match icons to metric meaning
4. **Sub-metrics**: Add credibility with specific details
5. **Layout**: Choose based on number of items (2-4: cards, 5+: compact)
6. **Consistency**: Use similar metric types (all percentages, all counts)
7. **Context**: Include timeframes where relevant
## Advanced Customization
### Custom Animations
Override animation timing:
```css
.stats-item {
transition-duration: 1s;
transition-delay: 0.2s;
}
```
### Custom Counters
For complex number formatting:
```yaml
statistic: "99.9%" # Will animate 0 → 99
statistic: "$2.5M" # Will animate 0 → 2.5
statistic: "1st" # Will animate 0 → 1
```
### Accessibility
- Numbers are announced by screen readers
- Animations respect motion preferences
- High contrast maintained
- Keyboard navigation supported
The enhanced Stats block creates visual impact while maintaining professional credibility - essential for research lab websites targeting academic and industry audiences.
+252 -19
View File
@@ -7,31 +7,264 @@
{{ $block := .wcBlock }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
{{ $layout := $block.design.layout | default "cards" }}
<div class="px-4 py-16 mx-auto sm:max-w-xl md:max-w-full lg:max-w-screen-xl md:px-24 lg:px-8 lg:py-20">
{{ with $title }}
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
<div class="max-w-screen-md mb-8 lg:mb-16 flex flex-col justify-center mx-auto">
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white text-center">{{.}}</h2>
<div class="py-12 sm:py-16 lg:py-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{/* Section Header */}}
{{ if or $title $text }}
<div class="text-center mb-12 lg:mb-16">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white tracking-tight mb-4">
{{ . }}
</h2>
{{ end }}
{{ with $text }}
<p class="text-gray-500 sm:text-xl dark:text-gray-400 text-center">{{.}}</p>
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto">
{{ . }}
</p>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
<div class="grid grid-cols-3 row-gap-8">
{{ range $idx, $item := $block.content.items }}
<div class="text-center {{if ne (math.Add $idx 1) (len $block.content.items) }}md:border-r{{end}}">
<h6 class="text-4xl font-bold lg:text-5xl xl:text-6xl">
{{- .statistic | $page.RenderString -}}
</h6>
<p class="text-sm font-medium tracking-widest lg:text-base text-gray-800 dark:text-gray-300">
{{- .description | $page.RenderString -}}
</p>
{{/* Cards Layout */}}
{{ if eq $layout "cards" }}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{{ len $block.content.items }} gap-6 lg:gap-8">
{{ range $idx, $item := $block.content.items }}
<div class="group relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 overflow-hidden border border-gray-100 dark:border-gray-700 hover:border-primary-200 dark:hover:border-primary-800">
{{/* Background Gradient */}}
<div class="absolute inset-0 bg-gradient-to-br from-primary-50 to-transparent dark:from-primary-900/20 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative p-8 text-center">
{{/* Icon */}}
{{ if $item.icon }}
<div class="inline-flex items-center justify-center w-16 h-16 mb-6 bg-primary-100 dark:bg-primary-900/50 rounded-xl group-hover:scale-110 transition-transform duration-300">
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-8 h-8 text-primary-600 dark:text-primary-400\"") }}
</div>
{{ end }}
{{/* Statistic */}}
<div class="mb-3">
<h3 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
{{ $item.statistic | $page.RenderString }}
</h3>
</div>
{{/* Description */}}
<p class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-300 leading-relaxed group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors duration-300">
{{ $item.description | $page.RenderString }}
</p>
{{/* Optional Sub-metric */}}
{{ with $item.sub_metric }}
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ . }}
</p>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{/* Compact Layout */}}
{{ else if eq $layout "compact" }}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{{ len $block.content.items }} divide-y sm:divide-y-0 sm:divide-x divide-gray-200 dark:divide-gray-700">
{{ range $idx, $item := $block.content.items }}
<div class="group p-8 text-center hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-300">
{{ if $item.icon }}
<div class="inline-flex items-center justify-center w-12 h-12 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-lg group-hover:scale-110 transition-transform duration-300">
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-6 h-6 text-primary-600 dark:text-primary-400\"") }}
</div>
{{ end }}
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
{{ $item.statistic | $page.RenderString }}
</h3>
<p class="text-sm font-medium text-gray-600 dark:text-gray-300 leading-relaxed">
{{ $item.description | $page.RenderString }}
</p>
</div>
{{ end }}
</div>
</div>
{{/* Minimal Layout */}}
{{ else }}
<div class="grid grid-cols-2 lg:grid-cols-{{ len $block.content.items }} gap-8 lg:gap-12">
{{ range $item := $block.content.items }}
<div class="text-center group">
{{ if $item.icon }}
<div class="inline-flex items-center justify-center w-14 h-14 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-full group-hover:scale-110 transition-transform duration-300">
{{ partial "functions/get_icon" (dict "name" $item.icon "attributes" "class=\"w-7 h-7 text-primary-600 dark:text-primary-400\"") }}
</div>
{{ end }}
<h3 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter" data-target="{{ $item.statistic | replaceRE `[^0-9.]` "" }}">
{{ $item.statistic | $page.RenderString }}
</h3>
<p class="text-sm font-medium text-gray-600 dark:text-gray-300">
{{ $item.description | $page.RenderString }}
</p>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
{{/* Enhanced Animation CSS */}}
<style>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgb(59 130 246 / 0.4);
}
50% {
box-shadow: 0 0 0 10px rgb(59 130 246 / 0);
}
}
/* Intersection Observer Animation */
.stats-animate {
animation: fadeInUp 0.8s ease-out forwards;
}
.stats-item {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease-out;
}
.stats-item.animate {
opacity: 1;
transform: translateY(0);
}
/* Stagger animation delays */
.stats-item:nth-child(1) { transition-delay: 0ms; }
.stats-item:nth-child(2) { transition-delay: 150ms; }
.stats-item:nth-child(3) { transition-delay: 300ms; }
.stats-item:nth-child(4) { transition-delay: 450ms; }
.stats-item:nth-child(5) { transition-delay: 600ms; }
/* Number counting animation */
@keyframes countUp {
from { opacity: 0; }
to { opacity: 1; }
}
.counter-animate {
animation: countUp 0.6s ease-out;
}
/* Hover glow effect */
.stats-card-glow:hover {
animation: pulse-glow 2s infinite;
}
</style>
{{/* Animation JavaScript for Counter and Intersection Observer */}}
<script>
document.addEventListener('DOMContentLoaded', function () {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function parseTarget(counter) {
const dataTarget = counter.getAttribute('data-target');
const fallbackMatch = counter.textContent.match(/[0-9,.]+/);
const raw = (dataTarget ?? (fallbackMatch ? fallbackMatch[0] : '0')).replace(/,/g, '');
const num = Number(raw);
return isNaN(num) ? 0 : num;
}
function extractParts(text) {
const parts = text.match(/([^0-9]*)([0-9][0-9,.]*)(.*)/);
return {
prefix: parts ? parts[1] : '',
numberPart: parts ? parts[2] : '',
suffix: parts ? parts[3] : ''
};
}
function animateCountersFor(container) {
container.classList.add('animate');
const counters = container.querySelectorAll('.counter');
counters.forEach(counter => {
const target = parseTarget(counter);
if (target <= 0) return;
const { prefix, numberPart, suffix } = extractParts(counter.textContent);
const decimals = (numberPart.split('.')[1] || '').length;
const formatter = new Intl.NumberFormat(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
if (prefersReducedMotion) {
counter.textContent = `${prefix}${formatter.format(target)}${suffix}`;
return;
}
const duration = 2000; // 2 seconds
const start = Date.now();
function update() {
const now = Date.now();
const progress = Math.min((now - start) / duration, 1);
const easeOut = 1 - Math.pow(1 - progress, 3); // Ease-out cubic
const current = target * easeOut;
counter.textContent = `${prefix}${formatter.format(current)}${suffix}`;
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
});
}
function isInView(el) {
const rect = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
const vw = window.innerWidth || document.documentElement.clientWidth;
return rect.top < vh * 0.9 && rect.bottom > 0 && rect.left < vw && rect.right > 0;
}
// Intersection Observer for stats animation (with near-fold sensitivity)
const statsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCountersFor(entry.target);
statsObserver.unobserve(entry.target);
}
});
}, {
threshold: 0.2,
rootMargin: '0px 0px -10% 0px'
});
// Observe all stats containers (and trigger immediately if already visible)
const statsContainers = document.querySelectorAll('.stats-item, [class*="grid"] > div');
statsContainers.forEach(container => {
container.classList.add('stats-item');
if (isInView(container)) {
animateCountersFor(container);
} else {
statsObserver.observe(container);
}
});
});
</script>
@@ -1,11 +1,11 @@
{
"id": "stats",
"name": "Stats",
"version": "1.0.0",
"version": "2.0.0",
"license": "MIT",
"category": "content",
"tags": ["statistics", "numbers", "metrics", "achievements", "data", "showcase", "grid"],
"description": "Showcase impressive statistics and achievements in a clean, impactful grid layout",
"tags": ["statistics", "numbers", "metrics", "achievements", "data", "showcase", "grid", "animated", "counters", "icons"],
"description": "Showcase impressive statistics with modern animations, icons, and multiple layout options - enhanced for 2025/2026",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/hugo-blox-builder",
@@ -38,6 +38,14 @@
"description": {
"type": "string",
"description": "Description of the statistic (supports Markdown)"
},
"icon": {
"type": "string",
"description": "Icon identifier (e.g., 'hero/chart-bar', 'hero/user-group')"
},
"sub_metric": {
"type": "string",
"description": "Additional context or sub-metric (supports Markdown)"
}
},
"required": ["statistic", "description"]
@@ -45,6 +53,18 @@
}
},
"required": ["items"]
},
"design": {
"type": "object",
"description": "Design options for the stats block",
"properties": {
"layout": {
"type": "string",
"enum": ["cards", "compact", "minimal"],
"description": "Display layout style",
"default": "cards"
}
}
}
}
}
@@ -0,0 +1,109 @@
# Team Showcase Block
Display your team members with a beautiful, responsive grid layout featuring rich profiles, social links, and customizable grouping.
## Features
- **Responsive Grid Layout**: Automatically adjusts from 1-4 columns based on screen size
- **User Groups**: Organize team members by role (e.g., Principal Investigators, PhD Students)
- **Rich Profiles**: Display avatar, name, role, bio, interests, and social links
- **Customizable Display**: Toggle visibility of role, organization, interests, and social links
- **Sorting**: Sort team members by last name or custom field
- **Hover Effects**: Smooth image zoom and shadow effects on hover
- **Call to Action**: Optional button to recruitment or team page
## Usage
```yaml
sections:
- block: team-showcase
content:
title: Meet Our Team
subtitle: World-class researchers advancing science
text: Our diverse team brings together expertise from multiple disciplines.
user_groups:
- Principal Investigators
- Postdoctoral Researchers
- PhD Students
sort_by: 'Params.last_name'
sort_ascending: true
cta:
text: Join Our Team
url: /opportunities
icon: user-plus
design:
show_role: true
show_organizations: false
show_interests: true
show_social: true
# Section background color (CSS class)
css_class: "bg-gray-50 dark:bg-gray-900"
```
## Content Structure
Team members should be created in the `content/authors/` directory. Each team member needs:
```yaml
# content/authors/jane-doe/_index.md
---
title: Dr. Jane Doe
role: Postdoctoral Researcher
bio: Research interests include machine learning and computational biology.
interests:
- Machine Learning
- Computational Biology
- Data Science
organizations:
- name: University of Excellence
url: https://example.edu
social:
- icon: envelope
icon_pack: fas
link: 'mailto:jane@example.edu'
- icon: twitter
icon_pack: fab
link: https://twitter.com/janedoe
- icon: github
icon_pack: fab
link: https://github.com/janedoe
user_groups:
- Postdoctoral Researchers
---
Full biography content here...
```
## Options
### Content Options
- **title**: Main heading for the team section
- **subtitle**: Optional subtitle
- **text**: Optional description text (supports Markdown)
- **user_groups**: Array of group names to display
- **sort_by**: Field to sort by (default: "Params.last_name")
- **sort_ascending**: Sort direction (default: true)
- **cta**: Optional call-to-action button with text, url, and icon
### Design Options
- **show_role**: Display team member roles (default: true)
- **show_organizations**: Display affiliated organizations (default: false)
- **show_interests**: Display research interests (default: false)
- **show_social**: Display social media links (default: true)
## Styling
The block uses Tailwind CSS classes and supports:
- Dark mode
- Custom background colors via `css_class`
- Responsive breakpoints
- Smooth hover transitions
## Tips
1. **Optimize Images**: Use square avatar images (minimum 400x400px) for best results
2. **User Groups**: Create logical groupings that reflect your organization structure
3. **Bio Length**: Keep bios concise (2-3 lines) for the card view
4. **Social Links**: Include relevant professional networks (ORCID, Google Scholar, GitHub)
@@ -0,0 +1,177 @@
{{/* Hugo Blox: Team Showcase */}}
{{/* Documentation: https://hugoblox.com/blocks/ */}}
{{/* License: https://github.com/HugoBlox/hugo-blox-builder/blob/main/LICENSE.md */}}
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $show_social := $block.design.show_social | default true }}
{{ $show_interests := $block.design.show_interests | default false }}
{{ $show_role := $block.design.show_role | default true }}
{{ $show_organizations := $block.design.show_organizations | default false }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{/* Section Header */}}
<div class="flex flex-col items-center text-center mb-12">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white tracking-tight">
{{ . }}
</h2>
{{ end }}
{{ with $subtitle }}
<p class="mt-4 text-xl text-gray-600 dark:text-gray-400">
{{ . }}
</p>
{{ end }}
{{ with $text }}
<div class="mt-6 prose prose-lg lg:prose-xl dark:prose-invert max-w-3xl">
{{ . }}
</div>
{{ end }}
</div>
{{/* Team Grid */}}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ range $block.content.user_groups }}
{{ $group_name := . }}
{{ $query := where (where site.Pages "Section" "authors") ".Params.user_groups" "intersect" (slice .) }}
{{/* Sort */}}
{{ $sort_by := $block.content.sort_by | default "Params.last_name" }}
{{ $sort_by = partial "functions/get_sort_by_parameter" $sort_by }}
{{ $sort_ascending := $block.content.sort_ascending | default true }}
{{ $sort_order := cond $sort_ascending "asc" "desc" }}
{{ $query = sort $query $sort_by $sort_order }}
{{/* Group Header (if multiple groups) */}}
{{ if and $query (gt (len $block.content.user_groups) 1) }}
<div class="col-span-full">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 pb-2 border-b border-gray-200 dark:border-gray-700">
{{ $group_name | markdownify }}
</h3>
</div>
{{ end }}
{{/* Team Members */}}
{{ range $person := $query }}
{{ $avatar := (.Resources.ByType "image").GetMatch "*avatar*" }}
{{ $link := "" }}
{{ with site.GetPage (printf "/authors/%s" (path.Base .File.Dir)) }}
{{ $link = .RelPermalink }}
{{ end }}
<div class="group relative">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
{{/* Avatar */}}
{{ if $avatar }}
<a href="{{ $link }}" class="block relative h-64 overflow-hidden">
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ .Title }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
{{/* Gradient Overlay */}}
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
{{ end }}
{{/* Content */}}
<div class="p-6">
{{/* Name */}}
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-1">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ .Title }}
</a>
</h3>
{{/* Role & Organization */}}
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ with .Params.role }}
<p class="font-medium">{{ . }}</p>
{{ end }}
{{ if and $show_organizations .Params.organizations }}
{{ range first 1 .Params.organizations }}
<p class="text-sm mt-1">{{ .name }}</p>
{{ end }}
{{ end }}
</div>
{{ end }}
{{/* Bio */}}
{{ with .Params.bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{{ . }}
</p>
{{ end }}
{{/* Interests */}}
{{ if and $show_interests .Params.interests }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ range first 3 .Params.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">
{{ . }}
</span>
{{ end }}
</div>
</div>
{{ end }}
{{/* Social Links */}}
{{ if $show_social }}
<div class="flex gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
{{ range .Params.social }}
{{ $link := .url | default .link }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $link = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $link | safeURL }}"
{{ $target | safeHTMLAttr }}
aria-label="{{ .icon }}"
{{ with .label }} title="{{ . }}"{{ end }}
class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ end }}
</div>
{{/* Call to Action */}}
{{ with $block.content.cta }}
<div class="mt-12 text-center">
<a href="{{ .url }}" class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transition-all duration-300">
{{ .text }}
{{ with .icon }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
{{ end }}
</a>
</div>
{{ end }}
</div>
@@ -0,0 +1,13 @@
{
"id": "team-showcase",
"name": "Team Showcase",
"version": "1.0.0",
"license": "MIT",
"category": "content",
"tags": ["team", "people", "members", "staff", "lab", "research", "organization"],
"description": "Display team members with customizable grouping, filtering, and rich profiles",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/hugo-blox-builder",
"keywords": ["hugo", "static-site", "team", "people", "organization", "research-lab"]
}
@@ -0,0 +1,98 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://hugoblox.com/schemas/blocks/team-showcase.json",
"title": "Team Showcase Block Schema",
"description": "Schema for the Team Showcase block - displays team members with filtering and grouping",
"allOf": [
{
"$ref": "../shared/schemas/base-block.json"
},
{
"type": "object",
"properties": {
"block": {
"const": "team-showcase"
},
"content": {
"type": "object",
"description": "Content configuration for the team showcase block",
"properties": {
"title": {
"type": "string",
"description": "Section title (supports Markdown)"
},
"subtitle": {
"type": "string",
"description": "Section subtitle (supports Markdown)"
},
"text": {
"type": "string",
"description": "Section description (supports Markdown)"
},
"user_groups": {
"type": "array",
"description": "User groups to display (e.g., 'Principal Investigators', 'PhD Students')",
"items": {
"type": "string"
}
},
"sort_by": {
"type": "string",
"description": "Field to sort team members by",
"default": "Params.last_name"
},
"sort_ascending": {
"type": "boolean",
"description": "Sort in ascending order",
"default": true
},
"cta": {
"type": "object",
"description": "Call to action button",
"properties": {
"text": {
"type": "string",
"description": "Button text"
},
"url": {
"type": "string",
"description": "Button URL"
},
"icon": {
"type": "string",
"description": "Optional icon name"
}
}
}
}
},
"design": {
"type": "object",
"description": "Design options for the team showcase",
"properties": {
"show_role": {
"type": "boolean",
"description": "Show team member roles",
"default": true
},
"show_organizations": {
"type": "boolean",
"description": "Show team member organizations",
"default": false
},
"show_interests": {
"type": "boolean",
"description": "Show team member interests",
"default": false
},
"show_social": {
"type": "boolean",
"description": "Show social media links",
"default": true
}
}
}
}
}
]
}