Files
e2c-how/layouts/_default/list.html
2025-07-21 21:25:24 -06:00

710 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ if .Description }}
<p class="lead">{{ .Description }}</p>
{{ end }}
{{ .Content }}
{{ if .Pages }}
<div class="filter-controls">
<div class="filter-row">
<input type="text" id="searchInput" placeholder="Search full text..." class="search-input">
<div class="tag-filter-container">
<div id="tagDropdown" class="tag-dropdown">
<div class="tag-dropdown-trigger">
<span class="dropdown-label">Select tags</span>
<div class="selected-tags" id="selectedTags"></div>
<svg class="dropdown-arrow" width="12" height="8" viewBox="0 0 12 8" fill="none">
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="tag-dropdown-menu" id="tagDropdownMenu">
<div class="tag-options" id="tagOptions">
<!-- Tags will be populated here -->
</div>
</div>
</div>
</div>
<div class="sort-container">
<div id="sortDropdown" class="sort-dropdown">
<div class="sort-dropdown-trigger">
<span class="sort-label">Sort by Title</span>
<svg class="dropdown-arrow" width="12" height="8" viewBox="0 0 12 8" fill="none">
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="sort-dropdown-menu" id="sortDropdownMenu">
<div class="sort-option" data-value="title">Sort by Title</div>
<div class="sort-option" data-value="date">Sort by Date</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid" id="cardGrid">
{{ range .Pages }}
<div class="card"
data-title="{{ .Title }}"
data-description="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | plainify | replaceRE "^### \\*\\*Summary\\*\\*\\s*" "" | truncate 200 }}{{ else }}{{ .Content | plainify | replaceRE "^### \\*\\*Summary\\*\\*\\s*" "" | truncate 200 }}{{ end }}"
data-content="{{ .Content | plainify | replaceRE "^### \\*\\*Summary\\*\\*\\s*" "" }}"
data-tags="{{ if .Params.tags }}{{ delimit .Params.tags "," }}{{ end }}"
data-date="{{ .Date.Format "2006-01-02" }}">
{{ if .Params.banner }}
<a href="{{ .RelPermalink }}" class="card-banner-link">
<div class="card-banner">
<img src="{{ .Params.banner }}" alt="{{ .Title }} banner" class="case-banner" />
{{ if .Params.image }}
<img src="{{ .Params.image }}" alt="{{ .Title }} logo" class="case-logo-overlay" />
{{ end }}
</div>
</a>
{{ else if .Params.image }}
<a href="{{ .RelPermalink }}" class="card-logo-link">
<img src="{{ .Params.image }}" alt="{{ .Title }} logo" class="case-logo" />
</a>
{{ end }}
<h3><a href="{{ .RelPermalink }}" class="card-title-link">{{ .Title }}</a></h3>
{{ if .Description }}
<p>{{ .Description }}</p>
{{ else if .Summary }}
<p>{{ .Summary | plainify | replaceRE "^### \\*\\*Summary\\*\\*\\s*" "" | truncate 200 }}</p>
{{ else }}
<p>{{ .Content | plainify | replaceRE "^### \\*\\*Summary\\*\\*\\s*" "" | truncate 200 }}</p>
{{ end }}
{{ if .Params.tags }}
<div class="tags">
{{ range .Params.tags }}
<a href="?tag={{ . | urlize }}" class="tag">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
<style>
.lead {
font-size: 1.125rem;
color: #666;
margin-bottom: 2rem;
}
.filter-controls {
margin-bottom: 48px;
padding: 24px;
background: var(--card-background);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 4px 12px var(--shadow);
}
.filter-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
}
.search-input {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
transition: all 0.2s ease;
color: var(--text-primary);
flex: 1;
min-width: 280px;
}
.search-input:focus {
outline: none;
border-color: var(--e2c-yellow);
background: var(--e2c-yellow);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.search-input::placeholder {
color: var(--text-primary);
font-weight: 400;
}
/* Custom Multi-Select Dropdown */
.tag-filter-container {
position: relative;
min-width: 200px;
}
.tag-dropdown {
position: relative;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tag-dropdown-trigger {
display: flex;
align-items: center;
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-primary);
font-size: 14px;
font-weight: 400;
min-height: 20px;
}
.tag-dropdown-trigger:hover, .tag-dropdown.open .tag-dropdown-trigger {
border-color: var(--e2c-yellow);
background: var(--e2c-yellow);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.dropdown-label {
flex: 1;
color: var(--text-primary);
font-weight: 400;
white-space: nowrap;
}
.tag-dropdown.has-selection .dropdown-label {
display: none;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
min-height: 20px;
}
.selected-tag {
background: var(--text-primary);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.selected-tag .remove {
cursor: pointer;
font-weight: bold;
font-size: 14px;
line-height: 1;
opacity: 0.8;
}
.selected-tag .remove:hover {
opacity: 1;
}
.dropdown-arrow {
margin-left: 8px;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.tag-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.tag-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--card-background);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 8px 24px var(--shadow);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: all 0.2s ease;
margin-top: 4px;
}
.tag-dropdown.open .tag-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.tag-option {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
transition: background-color 0.15s ease;
}
.tag-option:hover {
background: rgba(244, 208, 63, 0.1);
}
.tag-option input[type="checkbox"] {
margin-right: 8px;
accent-color: var(--e2c-yellow);
}
.tag-option.selected {
background: rgba(244, 208, 63, 0.15);
font-weight: 500;
}
/* Custom Sort Dropdown */
.sort-container {
position: relative;
min-width: 160px;
}
.sort-dropdown {
position: relative;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.sort-dropdown-trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-primary);
font-size: 14px;
font-weight: 400;
min-height: 20px;
}
.sort-dropdown-trigger:hover, .sort-dropdown.open .sort-dropdown-trigger {
border-color: var(--e2c-yellow);
background: var(--e2c-yellow);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sort-label {
flex: 1;
white-space: nowrap;
}
.sort-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.sort-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--card-background);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 8px 24px var(--shadow);
z-index: 1000;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: all 0.2s ease;
margin-top: 4px;
}
.sort-dropdown.open .sort-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.sort-option {
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
transition: background-color 0.15s ease;
}
.sort-option:hover {
background: rgba(244, 208, 63, 0.1);
}
.sort-option.selected {
background: rgba(244, 208, 63, 0.15);
font-weight: 500;
}
.tags {
margin-top: 0.5rem;
}
.tag {
display: inline-block;
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.5rem;
margin: 0.25rem 0.25rem 0 0;
border-radius: 0.25rem;
font-size: 0.875rem;
text-decoration: none;
transition: all 0.2s ease;
}
.tag:hover {
background: var(--e2c-yellow);
color: var(--text-primary);
transform: translateY(-1px);
}
.card-title-link {
display: inline-block;
padding: 8px 16px;
background: var(--e2c-yellow);
color: var(--text-primary) !important;
text-decoration: none !important;
border-radius: 12px;
margin: 8px 0;
font-weight: 600;
font-size: 1.25rem;
transition: all 0.2s ease;
border: 2px solid var(--e2c-yellow);
}
.card-title-link:hover {
background: var(--e2c-dark-yellow);
border-color: var(--e2c-dark-yellow);
color: var(--text-primary) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.card.hidden {
display: none;
}
.card-banner {
position: relative;
margin-bottom: 1rem;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16/9;
}
.case-banner {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.card-banner-link, .card-logo-link {
display: block;
text-decoration: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card-banner-link:hover, .card-logo-link:hover {
transform: translateY(-2px);
}
.card-banner-link:hover .card-banner {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.card-logo-link:hover .case-logo {
transform: scale(1.05);
}
@media (max-width: 768px) {
.filter-controls {
margin-bottom: 32px;
padding: 16px;
}
.filter-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.search-input {
width: 100%;
min-width: unset;
padding: 8px 14px;
font-size: 12px;
}
.tag-filter-container, .sort-container {
min-width: unset;
width: 100%;
}
.sort-dropdown-trigger {
padding: 8px 14px;
font-size: 12px;
}
.tag-dropdown-trigger {
padding: 8px 14px;
font-size: 12px;
}
.selected-tag {
font-size: 11px;
padding: 1px 6px;
}
.tag-dropdown-menu {
left: -4px;
right: -4px;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('searchInput');
const tagDropdown = document.getElementById('tagDropdown');
const tagDropdownTrigger = tagDropdown.querySelector('.tag-dropdown-trigger');
const tagDropdownMenu = document.getElementById('tagDropdownMenu');
const tagOptions = document.getElementById('tagOptions');
const selectedTagsContainer = document.getElementById('selectedTags');
const sortDropdown = document.getElementById('sortDropdown');
const sortDropdownTrigger = sortDropdown.querySelector('.sort-dropdown-trigger');
const sortDropdownMenu = document.getElementById('sortDropdownMenu');
const sortLabel = sortDropdown.querySelector('.sort-label');
const cardGrid = document.getElementById('cardGrid');
const cards = Array.from(cardGrid.querySelectorAll('.card'));
let selectedTags = new Set();
let currentSort = 'title';
// Check for URL parameter to pre-filter by tag
const urlParams = new URLSearchParams(window.location.search);
const preSelectedTag = urlParams.get('tag');
if (preSelectedTag) {
selectedTags.add(preSelectedTag);
}
// Collect all unique tags
const allTags = new Set();
cards.forEach(card => {
const tags = card.dataset.tags;
if (tags) {
tags.split(',').forEach(tag => allTags.add(tag.trim()));
}
});
// Populate tag dropdown
function populateTagOptions() {
tagOptions.innerHTML = '';
Array.from(allTags).sort().forEach(tag => {
const option = document.createElement('div');
option.className = 'tag-option';
option.innerHTML = `
<input type="checkbox" ${selectedTags.has(tag) ? 'checked' : ''}>
<span>${tag}</span>
`;
const checkbox = option.querySelector('input');
checkbox.addEventListener('change', function() {
if (checkbox.checked) {
selectedTags.add(tag);
} else {
selectedTags.delete(tag);
}
updateSelectedTagsDisplay();
filterAndSort();
updateURL();
});
tagOptions.appendChild(option);
});
}
// Update selected tags display
function updateSelectedTagsDisplay() {
selectedTagsContainer.innerHTML = '';
if (selectedTags.size > 0) {
tagDropdown.classList.add('has-selection');
selectedTags.forEach(tag => {
const tagElement = document.createElement('div');
tagElement.className = 'selected-tag';
tagElement.innerHTML = `
<span>${tag}</span>
<span class="remove">×</span>
`;
tagElement.querySelector('.remove').addEventListener('click', function(e) {
e.stopPropagation();
selectedTags.delete(tag);
updateSelectedTagsDisplay();
updateTagOptionStates();
filterAndSort();
updateURL();
});
selectedTagsContainer.appendChild(tagElement);
});
} else {
tagDropdown.classList.remove('has-selection');
}
}
// Update checkbox states in dropdown
function updateTagOptionStates() {
const checkboxes = tagOptions.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox, index) => {
const tag = Array.from(allTags).sort()[index];
checkbox.checked = selectedTags.has(tag);
checkbox.parentElement.classList.toggle('selected', selectedTags.has(tag));
});
}
// Toggle dropdown
tagDropdownTrigger.addEventListener('click', function(e) {
e.stopPropagation();
tagDropdown.classList.toggle('open');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!tagDropdown.contains(e.target)) {
tagDropdown.classList.remove('open');
}
if (!sortDropdown.contains(e.target)) {
sortDropdown.classList.remove('open');
}
});
// Sort dropdown functionality
sortDropdownTrigger.addEventListener('click', function(e) {
e.stopPropagation();
sortDropdown.classList.toggle('open');
tagDropdown.classList.remove('open'); // Close other dropdown
});
// Sort option selection
sortDropdownMenu.addEventListener('click', function(e) {
if (e.target.classList.contains('sort-option')) {
const value = e.target.dataset.value;
const text = e.target.textContent;
// Update current sort
currentSort = value;
sortLabel.textContent = text;
// Update selected state
sortDropdownMenu.querySelectorAll('.sort-option').forEach(option => {
option.classList.remove('selected');
});
e.target.classList.add('selected');
// Close dropdown and filter
sortDropdown.classList.remove('open');
filterAndSort();
}
});
// Update URL with selected tags
function updateURL() {
const url = new URL(window.location);
if (selectedTags.size > 0) {
url.searchParams.set('tag', Array.from(selectedTags)[0]); // For simplicity, just use first tag in URL
} else {
url.searchParams.delete('tag');
}
window.history.replaceState({}, '', url);
}
function filterAndSort() {
const searchTerm = searchInput.value.toLowerCase();
const sortCriteria = currentSort;
// Filter cards
const filteredCards = cards.filter(card => {
const title = card.dataset.title.toLowerCase();
const description = card.dataset.description.toLowerCase();
const content = card.dataset.content.toLowerCase();
const tags = card.dataset.tags;
// Full text search - search in title, description, and content
const matchesSearch = !searchTerm ||
title.includes(searchTerm) ||
description.includes(searchTerm) ||
content.includes(searchTerm);
// Tag filter - must match ALL selected tags
const matchesTags = selectedTags.size === 0 ||
(tags && Array.from(selectedTags).every(selectedTag =>
tags.split(',').map(t => t.trim()).includes(selectedTag)
));
return matchesSearch && matchesTags;
});
// Sort filtered cards
filteredCards.sort((a, b) => {
if (sortCriteria === 'title') {
return a.dataset.title.localeCompare(b.dataset.title);
} else if (sortCriteria === 'date') {
const dateA = new Date(a.dataset.date || '1970-01-01');
const dateB = new Date(b.dataset.date || '1970-01-01');
return dateB - dateA; // Most recent first
}
return 0;
});
// Hide all cards first
cards.forEach(card => card.classList.add('hidden'));
// Show and reorder filtered cards
filteredCards.forEach((card, index) => {
card.classList.remove('hidden');
card.style.order = index;
});
}
// Event listeners
searchInput.addEventListener('input', filterAndSort);
// Initialize
populateTagOptions();
updateSelectedTagsDisplay();
updateTagOptionStates();
// Initialize sort dropdown
sortDropdownMenu.querySelector('[data-value="title"]').classList.add('selected');
filterAndSort();
});
</script>
{{ end }}