@@ -9,14 +9,37 @@
{{ if .Pages }}
< div class = "filter-controls" >
< div class = "filter-row" >
< input type = "text" id = "searchInput" placeholder = "Search titles and descriptions ..." class = "search-input" >
< select id = "tagFilter" class = "tag-filter" >
< option value = "" > All tags< / option >
< / select >
< select id = "sortBy" class = "sort-select" >
< option value = "title" > Sort by Title< / option >
< option value = "date" > Sort by Date< / option >
< / select >
< 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" >
@@ -24,6 +47,7 @@
< 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 }}
@@ -83,9 +107,9 @@
align-items : center ;
}
. search-input , . tag-filter , . sort-select {
. search-input {
font-family : 'Space Grotesk' , - apple-system , BlinkMacSystemFont , 'Segoe UI' , Roboto , sans-serif ;
font-weight : 5 00;
font-weight : 4 00;
font-size : 14 px ;
padding : 10 px 16 px ;
border : 1 px solid var ( - - border ) ;
@@ -94,14 +118,11 @@
backdrop-filter : blur ( 8 px ) ;
transition : all 0.2 s ease ;
color : var ( - - text - primary ) ;
}
. search-input {
flex : 1 ;
min-width : 280 px ;
}
. search-input : focus , . tag-filter : focus , . sort-select : focus {
. search-input : focus {
outline : none ;
border-color : var ( - - e2c - yellow ) ;
background : var ( - - e2c - yellow ) ;
@@ -110,14 +131,230 @@
}
. search-input :: placeholder {
color : var ( - - text - second ary) ;
color : var ( - - text - prim ary) ;
font-weight : 400 ;
}
. tag-filter , . sort-select {
/* Custom Multi-Select Dropdown */
. tag-filter-container {
position : relative ;
min-width : 200 px ;
}
. 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 : 10 px 16 px ;
border : 1 px solid var ( - - border ) ;
border-radius : 24 px ;
background : rgba ( 255 , 255 , 255 , 0.9 ) ;
backdrop-filter : blur ( 8 px ) ;
cursor : pointer ;
transition : all 0.2 s ease ;
color : var ( - - text - primary ) ;
font-size : 14 px ;
font-weight : 400 ;
min-height : 20 px ;
}
. tag-dropdown-trigger : hover , . tag-dropdown . open . tag-dropdown-trigger {
border-color : var ( - - e2c - yellow ) ;
background : var ( - - e2c - yellow ) ;
transform : translateY ( -1 px ) ;
box-shadow : 0 4 px 12 px 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 : 4 px ;
flex : 1 ;
min-height : 20 px ;
}
. selected-tag {
background : var ( - - text - primary ) ;
color : white ;
padding : 2 px 8 px ;
border-radius : 12 px ;
font-size : 12 px ;
font-weight : 500 ;
display : flex ;
align-items : center ;
gap : 4 px ;
}
. selected-tag . remove {
cursor : pointer ;
font-weight : bold ;
font-size : 14 px ;
line-height : 1 ;
opacity : 0.8 ;
}
. selected-tag . remove : hover {
opacity : 1 ;
}
. dropdown-arrow {
margin-left : 8 px ;
transition : transform 0.2 s ease ;
flex-shrink : 0 ;
}
. tag-dropdown . open . dropdown-arrow {
transform : rotate ( 180 deg ) ;
}
. tag-dropdown-menu {
position : absolute ;
top : 100 % ;
left : 0 ;
right : 0 ;
background : var ( - - card - background ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 12 px ;
box-shadow : 0 8 px 24 px var ( - - shadow ) ;
z-index : 1000 ;
max-height : 200 px ;
overflow-y : auto ;
opacity : 0 ;
visibility : hidden ;
transform : translateY ( -4 px ) ;
transition : all 0.2 s ease ;
margin-top : 4 px ;
}
. tag-dropdown . open . tag-dropdown-menu {
opacity : 1 ;
visibility : visible ;
transform : translateY ( 0 ) ;
}
. tag-option {
display : flex ;
align-items : center ;
padding : 8 px 12 px ;
cursor : pointer ;
font-size : 14 px ;
color : var ( - - text - primary ) ;
transition : background-color 0.15 s ease ;
}
. tag-option : hover {
background : rgba ( 244 , 208 , 63 , 0.1 ) ;
}
. tag-option input [ type = "checkbox" ] {
margin-right : 8 px ;
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 : 160 px ;
}
. 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 : 10 px 16 px ;
border : 1 px solid var ( - - border ) ;
border-radius : 24 px ;
background : rgba ( 255 , 255 , 255 , 0.9 ) ;
backdrop-filter : blur ( 8 px ) ;
cursor : pointer ;
transition : all 0.2 s ease ;
color : var ( - - text - primary ) ;
font-size : 14 px ;
font-weight : 400 ;
min-height : 20 px ;
}
. sort-dropdown-trigger : hover , . sort-dropdown . open . sort-dropdown-trigger {
border-color : var ( - - e2c - yellow ) ;
background : var ( - - e2c - yellow ) ;
transform : translateY ( -1 px ) ;
box-shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.15 ) ;
}
. sort-label {
flex : 1 ;
white-space : nowrap ;
}
. sort-dropdown . open . dropdown-arrow {
transform : rotate ( 180 deg ) ;
}
. sort-dropdown-menu {
position : absolute ;
top : 100 % ;
left : 0 ;
right : 0 ;
background : var ( - - card - background ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 12 px ;
box-shadow : 0 8 px 24 px var ( - - shadow ) ;
z-index : 1000 ;
opacity : 0 ;
visibility : hidden ;
transform : translateY ( -4 px ) ;
transition : all 0.2 s ease ;
margin-top : 4 px ;
}
. sort-dropdown . open . sort-dropdown-menu {
opacity : 1 ;
visibility : visible ;
transform : translateY ( 0 ) ;
}
. sort-option {
padding : 8 px 12 px ;
cursor : pointer ;
font-size : 14 px ;
color : var ( - - text - primary ) ;
transition : background-color 0.15 s 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.5 rem ;
}
@@ -216,26 +453,64 @@
gap : 12 px ;
}
. search-input , . tag-filter , . sort-select {
. search-input {
width : 100 % ;
min-width : unset ;
padding : 8 px 14 px ;
font-size : 12 px ;
}
. tag-filter-container , . sort-container {
min-width : unset ;
width : 100 % ;
}
. sort-dropdown-trigger {
padding : 8 px 14 px ;
font-size : 12 px ;
}
. tag-dropdown-trigger {
padding : 8 px 14 px ;
font-size : 12 px ;
}
. selected-tag {
font-size : 11 px ;
padding : 1 px 6 px ;
}
. tag-dropdown-menu {
left : -4 px ;
right : -4 px ;
}
}
< / style >
< script >
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
const searchInput = document . getElementById ( 'searchInput' ) ;
const tagFilter = document . getElementById ( 'tagFilter ' ) ;
const sortBy = document . getElementById ( 'sortBy ') ;
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 ( ) ;
@@ -246,69 +521,154 @@ document.addEventListener('DOMContentLoaded', function() {
}
} ) ;
// Populate tag filter dropdown
// Populate tag dropdown
function populateTagOptions ( ) {
tagOptions . innerHTML = '' ;
Array . from ( allTags ) . sort ( ) . forEach ( tag => {
const option = document . createElement ( 'option ' ) ;
option . valu e = tag;
option . textContent = tag ;
if ( preSelectedTag && tag === preSel ect edTag ) {
option . selected = true ;
const option = document . createElement ( 'div ' ) ;
option . classNam e = ' tag-option' ;
option . innerHTML = `
<input type="checkbox" ${ selectedTags . has ( tag ) ? 'ch eck ed' : '' } >
<span> ${ tag } </span>
` ;
const checkbox = option . querySelector ( 'input' ) ;
checkbox . addEventListener ( 'change' , function ( ) {
if ( checkbox . checked ) {
selectedTags . add ( tag ) ;
} else {
selectedTags . delete ( tag ) ;
}
tagFilter . appendChild ( option ) ;
updateSelectedTagsDisplay ( ) ;
filterAndSort ( ) ;
updateURL ( ) ;
} ) ;
function updateTagFilter ( ) {
// Store currently selected value
const currentSelection = tagFilter . value ;
// Clear existing options except "All tags"
tagFilter . innerHTML = '<option value="">All tags</option>' ;
// Collect tags from currently visible cards
const visibleTags = new Set ( ) ;
cards . forEach ( card => {
if ( ! card . classList . contains ( 'hidden' ) ) {
const tags = card . dataset . tags ;
if ( tags ) {
tags . split ( ',' ) . forEach ( tag => visibleTags . add ( tag . trim ( ) ) ) ;
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' ) ;
}
} ) ;
// Populate tag filter dropdown with current tags
Array . from ( visibleTags ) . sort ( ) . forEach ( tag => {
const option = document . createElement ( 'option' ) ;
option . value = tag ;
option . textContent = tag ;
// Maintain selection if this tag was previously selected
if ( tag === currentSelection ) {
option . selected = true ;
}
tagFilter . appendChild ( option ) ;
// 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 selectedTag = tagFilter . value ;
const sortCriteria = sortBy . value ;
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 ;
// T ext search
// Full t ext search - search in title, description, and content
const matchesSearch = ! searchTerm ||
title . includes ( searchTerm ) ||
description . includes ( searchTerm ) ;
description . includes ( searchTerm ) ||
content . includes ( searchTerm ) ;
// Tag filter
const matchesTag = ! selectedTag ||
( tags && tags . split ( ',' ) . map ( t => t . trim ( ) ) . includes ( selectedTag ) ) ;
// 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 && matchesTag ;
return matchesSearch && matchesTags ;
} ) ;
// Sort filtered cards
@@ -331,28 +691,19 @@ document.addEventListener('DOMContentLoaded', function() {
card . classList . remove ( 'hidden' ) ;
card . style . order = index ;
} ) ;
// Update tag filter to show only current tags
updateTagFilter ( ) ;
}
// Event listeners
searchInput . addEventListener ( 'input' , filterAndSort ) ;
tagFilter . addEventListener ( 'change' , function ( ) {
// Update URL when tag filter changes
const selectedTag = tagFilter . value ;
const url = new URL ( window . location ) ;
if ( selectedTag ) {
url . searchParams . set ( 'tag' , selectedTag ) ;
} else {
url . searchParams . delete ( 'tag' ) ;
}
window . history . replaceState ( { } , '' , url ) ;
filterAndSort ( ) ;
} ) ;
sortBy . addEventListener ( 'change' , filterAndSort ) ;
// Initial sort by titl e
// Initializ e
populateTagOptions ( ) ;
updateSelectedTagsDisplay ( ) ;
updateTagOptionStates ( ) ;
// Initialize sort dropdown
sortDropdownMenu . querySelector ( '[data-value="title"]' ) . classList . add ( 'selected' ) ;
filterAndSort ( ) ;
} ) ;
< / script >