feat: implement responsive card layout with category filtering and consumption status

This commit is contained in:
Greg 2025-06-01 18:39:11 +02:00
parent 40ed683c5f
commit e80e9a54fe
4 changed files with 241 additions and 28 deletions

View File

@ -8,15 +8,162 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<style>
/* Sidebar Menu Styles */
.sidebar {
height: 100%;
width: 0;
position: fixed;
z-index: 100;
top: 0;
left: 0;
background-color: #fff;
overflow-x: hidden;
transition: 0.3s;
padding-top: 60px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.sidebar.open {
width: 250px;
}
.sidebar .close-btn {
position: absolute;
top: 10px;
right: 20px;
font-size: 30px;
cursor: pointer;
}
.sidebar-heading {
padding: 8px 16px 20px 16px;
font-size: 1.3rem;
font-weight: 500;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 10px;
}
.sidebar-menu {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-menu li {
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.sidebar-menu li:hover {
background-color: #f5f5f5;
}
.sidebar-menu li.active {
background-color: #e3f2fd;
color: #1976d2;
font-weight: 500;
}
.sidebar-section {
padding: 12px 16px 8px;
font-weight: 500;
color: #757575;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.overlay {
display: none;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.4);
z-index: 99;
}
.overlay.visible {
display: block;
}
</style>
</head>
<body>
<div id="sidebar" class="sidebar">
<span class="close-btn">&times;</span>
<div class="sidebar-heading">My Favorite Stuff</div>
<div class="sidebar-section">View Options</div>
<ul class="sidebar-menu" id="view-options">
<li data-filter="consumed" class="active">Consumed Items</li>
<li data-filter="not-consumed">Not Consumed Yet</li>
<li data-filter="all">Show All Items</li>
</ul>
</div>
<div id="overlay" class="overlay"></div>
<header>
<span style="font-size:1.7rem;cursor:pointer;margin-right:18px;">&#9776;</span>
<span id="menu-toggle" style="font-size:1.7rem;cursor:pointer;margin-right:18px;">&#9776;</span>
<h1 style="margin:0;font-size:2rem;font-weight:500;">My Favorite Stuff</h1>
</header>
<main>
{{ block "main" . }}{{ end }}
</main>
<div class="fab">+</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const menuToggle = document.getElementById('menu-toggle');
const sidebar = document.getElementById('sidebar');
const closeBtn = document.querySelector('.close-btn');
const overlay = document.getElementById('overlay');
const viewOptions = document.querySelectorAll('#view-options li');
// Set the initial filter based on the selected sidebar option
let currentConsumedFilter = 'consumed';
// Functions to open and close the sidebar
function openSidebar() {
sidebar.classList.add('open');
overlay.classList.add('visible');
}
function closeSidebar() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
}
// Toggle sidebar when hamburger is clicked
menuToggle.addEventListener('click', openSidebar);
// Close sidebar when X is clicked
closeBtn.addEventListener('click', closeSidebar);
// Close sidebar when clicking outside
overlay.addEventListener('click', closeSidebar);
// Handle view option clicks
viewOptions.forEach(option => {
option.addEventListener('click', function() {
// Update active class
viewOptions.forEach(opt => opt.classList.remove('active'));
this.classList.add('active');
// Set filter and apply
currentConsumedFilter = this.getAttribute('data-filter');
applyConsumedFilter(currentConsumedFilter);
closeSidebar();
});
});
// Function to apply the filter - will be called by list.html and index.html scripts
window.applyConsumedFilter = function(filterType) {
// Make this function available to other scripts
window.currentConsumedFilter = filterType;
// Dispatch a custom event that list.html and index.html can listen for
document.dispatchEvent(new CustomEvent('consumedFilterChanged', {
detail: { filterType: filterType }
}));
};
// Initialize with the default filter
window.currentConsumedFilter = 'consumed';
});
</script>
</body>
</html>

View File

@ -59,6 +59,10 @@ document.addEventListener('DOMContentLoaded', function() {
});
console.log('FUNC activateTabAndFilter (index.html): Active states updated for tabs.');
// Get the consumed filter status from window global
const consumedFilter = window.currentConsumedFilter || 'consumed';
console.log('FUNC activateTabAndFilter (index.html): Current consumed filter is:', consumedFilter);
let visibleCount = 0;
allCards.forEach(cardLinkElement => {
const cardDiv = cardLinkElement.querySelector('.card');
@ -68,23 +72,45 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// Check category visibility first
let categoryVisible = false;
if (selectedCategory === 'all') {
cardLinkElement.style.display = '';
visibleCount++;
categoryVisible = true;
} else {
const cardCategoryAttr = cardDiv.getAttribute('data-category');
const sCat = selectedCategory ? selectedCategory : 'null_or_empty';
const cCat = cardCategoryAttr ? cardCategoryAttr : 'null_or_empty';
console.log(
`FUNC activateTabAndFilter (index.html): Comparing: cardCategory='${cCat}' (length ${cCat.length})` +
` vs selectedCategory='${sCat}' (length ${sCat.length})` +
` for card titled: '${cardDiv.querySelector('.card-title') ? cardDiv.querySelector('.card-title').textContent.trim() : 'N/A'}'`
);
const isVisible = (cardCategoryAttr === selectedCategory);
categoryVisible = (cardCategoryAttr === selectedCategory);
}
// Now check consumed status
const cardData = cardLinkElement.querySelector('meta[name="card-data"]');
let consumedStatus;
if (cardData) {
try {
const cardDataObj = JSON.parse(cardData.getAttribute('content'));
consumedStatus = cardDataObj.consumed;
} catch (e) {
console.warn('FUNC activateTabAndFilter (index.html): Error parsing card data:', e);
}
}
// Determine visibility based on consumed filter
let consumedVisible = true; // Default to true if no consumed status is specified
if (consumedFilter === 'consumed') {
// Show only consumed=true or consumed not specified
consumedVisible = (consumedStatus !== false);
} else if (consumedFilter === 'not-consumed') {
// Show only consumed=false
consumedVisible = (consumedStatus === false);
}
// If consumedFilter is 'all', then consumedVisible remains true
// Final visibility is the combination of both filters
const isVisible = categoryVisible && consumedVisible;
cardLinkElement.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
}
});
console.log('FUNC activateTabAndFilter (index.html): Visible cards after filtering:', visibleCount);
}
@ -114,6 +140,14 @@ document.addEventListener('DOMContentLoaded', function() {
activateTabAndFilter(initialCategoryToDisplay);
console.log('SCRIPT (index.html): Initial page load filtering complete.');
// Listen for consumed filter changes from the sidebar
document.addEventListener('consumedFilterChanged', function(event) {
console.log('EVENT (index.html): consumedFilterChanged received with filterType:', event.detail.filterType);
// Re-apply the current category filter when the consumed filter changes
const currentCategory = document.querySelector('#tabs .tab.active').getAttribute('data-tab');
activateTabAndFilter(currentCategory);
});
});
</script>
{{ end }}

View File

@ -89,6 +89,10 @@ document.addEventListener('DOMContentLoaded', function() {
});
console.log('FUNC activateTabAndFilter: Active states updated for tabs.');
// Get the consumed filter status from window global
const consumedFilter = window.currentConsumedFilter || 'consumed';
console.log('FUNC activateTabAndFilter: Current consumed filter is:', consumedFilter);
let visibleCount = 0;
allCards.forEach(cardLinkElement => {
const cardDiv = cardLinkElement.querySelector('.card');
@ -98,23 +102,45 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// Check category visibility first
let categoryVisible = false;
if (selectedCategory === 'all') {
cardLinkElement.style.display = '';
visibleCount++;
categoryVisible = true;
} else {
const cardCategoryAttr = cardDiv.getAttribute('data-category');
const sCat = selectedCategory ? selectedCategory : 'null_or_empty';
const cCat = cardCategoryAttr ? cardCategoryAttr : 'null_or_empty';
console.log(
`FUNC activateTabAndFilter: Comparing: cardCategory='${cCat}' (length ${cCat.length})` +
` vs selectedCategory='${sCat}' (length ${sCat.length})` +
` for card titled: '${cardDiv.querySelector('.card-title') ? cardDiv.querySelector('.card-title').textContent.trim() : 'N/A'}'`
);
const isVisible = (cardCategoryAttr === selectedCategory);
categoryVisible = (cardCategoryAttr === selectedCategory);
}
// Now check consumed status
const cardData = cardLinkElement.querySelector('meta[name="card-data"]');
let consumedStatus;
if (cardData) {
try {
const cardDataObj = JSON.parse(cardData.getAttribute('content'));
consumedStatus = cardDataObj.consumed;
} catch (e) {
console.warn('FUNC activateTabAndFilter: Error parsing card data:', e);
}
}
// Determine visibility based on consumed filter
let consumedVisible = true; // Default to true if no consumed status is specified
if (consumedFilter === 'consumed') {
// Show only consumed=true or consumed not specified
consumedVisible = (consumedStatus !== false);
} else if (consumedFilter === 'not-consumed') {
// Show only consumed=false
consumedVisible = (consumedStatus === false);
}
// If consumedFilter is 'all', then consumedVisible remains true
// Final visibility is the combination of both filters
const isVisible = categoryVisible && consumedVisible;
cardLinkElement.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
}
});
console.log('FUNC activateTabAndFilter: Visible cards after filtering:', visibleCount);
}
@ -146,6 +172,14 @@ document.addEventListener('DOMContentLoaded', function() {
activateTabAndFilter(initialCategoryToDisplay);
console.log('SCRIPT: Initial page load filtering complete.');
// Listen for consumed filter changes from the sidebar
document.addEventListener('consumedFilterChanged', function(event) {
console.log('EVENT: consumedFilterChanged received in list.html with filterType:', event.detail.filterType);
// Re-apply the current category filter when the consumed filter changes
const currentCategory = document.querySelector('#tabs .tab.active').getAttribute('data-tab');
activateTabAndFilter(currentCategory);
});
});
</script>
{{ end }}

View File

@ -1,13 +1,11 @@
<a class="card-link-outer" href="{{ .RelPermalink }}">
<meta name="card-data" content='{{ jsonify (dict "consumed" .Params.consumed) }}'>
<div class="card" data-category="{{ .Params.category }}">
{{ with .Params.image }}
<img src="{{ . }}" alt="{{ $.Title }} cover">
{{ else }}
<img src="/img/no-cover-available-yet.png" alt="{{ $.Title }} - No cover available">
{{ end }}
{{ if eq .Params.consumed false }}
<div class="not-consumed-badge">Not {{ if eq .Params.category "Books" }}Read{{ else if eq .Params.category "Movies" }}Watched{{ else if eq .Params.category "TV Series" }}Watched{{ else if eq .Params.category "Restaurants" }}Visited{{ else if eq .Params.category "Recipes" }}Made{{ else if eq .Params.category "Concerts" }}Attended{{ else }}Experienced{{ end }} Yet</div>
{{ end }}
<div class="card-title">{{ .Title }}</div>
{{ with .Params.rating }}<div class="card-rating">{{ partial "stars.html" (dict "rating" .) }} {{ . }}</div>{{ end }}
{{ with .Params.likes }}<div class="card-likes">&#128077; {{ . }}</div>{{ end }}