feat: add new tag list page with interactive filtering and dynamic tag count display.

This commit is contained in:
Bruno Charest 2026-01-18 09:32:01 -05:00
parent c7e7f9ceee
commit a51b3b8eae

View File

@ -1,15 +1,98 @@
<!DOCTYPE html> <!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}> <html{if="$language !=='auto'"} lang=" {$language}"{/if}>
<head>
{$pageName="tag.list"}
{include="includes"}
</head>
<body>
{include="page.header"}
{include="tag.sort"} <head>
{$pageName="tag.list"}
{include="includes"}
<style>
.tag-filter-container {
position: relative;
margin-bottom: 1rem;
}
<div class="container"> .tag-filter-input {
width: 100%;
padding: 0.75rem 1rem;
padding-right: 2.5rem;
border: 1px solid var(--border-light, #3a3f4b);
border-radius: 8px;
background: var(--bg-secondary, #1e2128);
color: var(--text-primary, #e4e6eb);
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.tag-filter-input:focus {
outline: none;
border-color: var(--primary, #4a7dff);
box-shadow: 0 0 0 3px rgba(74, 125, 255, 0.15);
}
.tag-filter-input::placeholder {
color: var(--text-muted, #6c757d);
}
.tag-filter-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted, #6c757d);
cursor: pointer;
padding: 0.25rem;
display: none;
font-size: 1.2rem;
line-height: 1;
}
.tag-filter-clear:hover {
color: var(--text-primary, #e4e6eb);
}
.tag-filter-clear.visible {
display: block;
}
.tag-count-info {
font-size: 0.85rem;
color: var(--text-muted, #6c757d);
margin-top: 0.5rem;
}
.tag-count-info .count-visible {
color: var(--primary, #4a7dff);
font-weight: 600;
}
.tag-count-info .count-total {
color: var(--text-muted, #6c757d);
}
.no-tags-message {
padding: 2rem;
text-align: center;
color: var(--text-muted, #6c757d);
display: none;
}
.no-tags-message.visible {
display: block;
}
.list-group-item.hidden-by-filter {
display: none !important;
}
</style>
</head>
<body>
{include="page.header"}
{include="tag.sort"}
<div class="container">
{$countTags=count($tags)} {$countTags=count($tags)}
<div id="plugin_zone_start_tagcloud" class="plugin_zone"> <div id="plugin_zone_start_tagcloud" class="plugin_zone">
@ -21,33 +104,30 @@
<div id="taglist" class="card"> <div id="taglist" class="card">
<div class="card-header"> <div class="card-header">
<div class="pull-right" style="font-size: 0.9rem;"> <div class="pull-right" style="font-size: 0.9rem;">
<a href="{$base_path}/?searchtags={$search_tags|urlencode}" title="{'List all links with those tags'|t}">{$countTags} {'tags'|t}</a> <span class="tag-count-info">
<span class="count-visible" id="visibleTagCount">{$countTags}</span> / <span class="count-total" id="totalTagCount">{$countTags}</span> {'tags'|t}
</span>
</div> </div>
{'Tag list'|t} {'Tag list'|t}
</div> </div>
<div class="card-body" style="padding-bottom: 0;"> <div class="card-body" style="padding-bottom: 0;">
<form class="card-search" method="get"> <div class="tag-filter-container">
<input type="hidden" name="do" value="taglist"> <input type="text" id="tagFilterInput" class="tag-filter-input" placeholder="{'Filter by tag'|t}..." autocomplete="off">
<div class="form-group"> <button type="button" id="tagFilterClear" class="tag-filter-clear" title="Clear filter">
<input type="search" name="searchtags" class="form-control" placeholder="{'Filter by tag'|t}..." <i class="mdi mdi-close"></i>
{if="!empty($search_tags)"} </button>
value="{$search_tags}"
{/if}
autocomplete="off" data-multiple data-autofirst data-minChars="1"
data-list="{loop="$tags"}{$key}, {/loop}"
>
</div> </div>
</form> <div id="noTagsMessage" class="no-tags-message">
<i class="mdi mdi-tag-off-outline" style="font-size: 2rem; margin-bottom: 0.5rem;"></i>
<p>{'No tags match your filter'|t}</p>
</div> </div>
<div class="list-group list-group-flush"> </div>
<div class="list-group list-group-flush" id="tagListContainer">
{loop="tags"} {loop="tags"}
<div class="list-group-item"> <div class="list-group-item" data-tag-name="{$key|strtolower}">
<div class="list-group-item-content" style="display: flex; align-items: center; justify-content: space-between;"> <div class="list-group-item-content" style="display: flex; align-items: center; justify-content: space-between;">
<div style="flex: 1;"> <div style="flex: 1;">
<a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link" style="font-weight: 500; font-size: 1rem;">{$key}</a> <a href="{$base_path}/?searchtags={$key|urlencode}" class="tag-link" style="font-weight: 500; font-size: 1rem;">{$key}</a>
{loop="$value.tag_plugin"}
{$value}
{/loop}
</div> </div>
<div style="margin-right: 1rem;"> <div style="margin-right: 1rem;">
<span class="badge" style="background: var(--border-light); padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.8rem;">{$value}</span> <span class="badge" style="background: var(--border-light); padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.8rem;">{$value}</span>
@ -76,13 +156,83 @@
{$value} {$value}
{/loop} {/loop}
</div> </div>
</div> </div>
{if="$is_logged_in"} {if="$is_logged_in"}
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"> <input type="hidden" name="taglist" id="tagListData" value="">
{/if} {/if}
{include="page.footer"} {literal}
</body> <script>
</html> (function () {
var filterInput = document.getElementById('tagFilterInput');
var clearBtn = document.getElementById('tagFilterClear');
var tagListContainer = document.getElementById('tagListContainer');
var noTagsMessage = document.getElementById('noTagsMessage');
var visibleTagCount = document.getElementById('visibleTagCount');
if (!filterInput || !tagListContainer) return;
var tagItems = tagListContainer.querySelectorAll('.list-group-item');
var totalTags = tagItems.length;
function filterTags() {
var filterValue = filterInput.value.toLowerCase().trim();
var visibleCount = 0;
// Show/hide clear button
if (filterValue.length > 0) {
clearBtn.classList.add('visible');
} else {
clearBtn.classList.remove('visible');
}
for (var i = 0; i < tagItems.length; i++) {
var item = tagItems[i];
var tagName = item.getAttribute('data-tag-name') || '';
if (filterValue === '' || tagName.indexOf(filterValue) !== -1) {
item.classList.remove('hidden-by-filter');
visibleCount++;
} else {
item.classList.add('hidden-by-filter');
}
}
// Update count
if (visibleTagCount) {
visibleTagCount.textContent = visibleCount;
}
// Show/hide no results message
if (visibleCount === 0 && filterValue.length > 0) {
noTagsMessage.classList.add('visible');
} else {
noTagsMessage.classList.remove('visible');
}
}
filterInput.addEventListener('input', filterTags);
filterInput.addEventListener('keyup', filterTags);
clearBtn.addEventListener('click', function () {
filterInput.value = '';
filterTags();
filterInput.focus();
});
// Handle escape key
filterInput.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
filterInput.value = '';
filterTags();
}
});
})();
</script>
{/literal}
{include="page.footer"}
</body>
</html>