Site

My personal site, which you are currently browsing, is made using Jekyll. Jekyll is one of many site generator (SG): it takes a bunch of files, in this case markdown files, and then converts them to a HTML page. The resulting HTML page can then be hosted virtually anywhere.

The reason why I am using a SG is because creating content in markdown is easy. It takes very little effort to make a site, and little maintenance. To read more about the philosophy of Jekyll see its page on the jamstack site.

Creating the site was fairly enjoyable. While most things are supported by the theme I use, minimal mistakes, the grid of posts on the notes page is a bunch of custom stuff.

From top to bottom, first are some buttons to filter on categories. These buttons are generated using Liquid templating. All buttons are part of a main container, button-group, so I can use flexbox for layouting. Each category gets its own button, which is by default active, and has a data-filter attribute that is used for filtering.

<!-- Buttons to filter -->
<div class="button-group filter-button-group justify-content-center">
    {% for category in site.categories %}
        <a class="btn btn-sm btn-primary active" data-filter="{{ category[0] }}">{{ category[0] }}</a>
    {% endfor %}
</div>

Below the buttons is the grid, which is also generated using Liquid: for each post I create a post-item that is shown by default (hence post-shown). At the bottom of the grid is a piece of text, identified by #emptygrid, that is hidden by default. This shows up if you filter out all posts.

<!-- Grid -->
<div class="grid flex">
    {% for post in site.posts %}
        <div class="post-item post-shown {{ post.category }}">
            <a class="post-link" href="{{ post.url }}">
                <h4>{{ post.title }}</h4>
            </a>
            <p> {{ post.excerpt | markdownify }}</p>
        </div>
    {% endfor %}
  <p id="emptygrid" style="display:none">You have filtered out all posts. It is empty now.</p>
</div>

Filtering of these items is achieved through some JS. First, grab the values that we can filter on.

// Create a list of values that we filter on.
let filterValues = []
let elems = document.querySelectorAll('.filter-button-group a.active');
for (const elem of elems)
    // This grabs the data-filter attribute of the buttons made above.
    filterValues.push(elem.getAttribute("data-filter"))

Then, add an event listener to all buttons. This event listener is the code that runs once you click on the button.

// Loop over all the buttons
const buttons = document.querySelectorAll('.button-group a.btn')
for (const item of buttons) {
    // Define what happens when they are clicked
    item.addEventListener('click', (evt) => {
        // First, grab its data-filter attribute
        const value = item.getAttribute("data-filter")
        // Update its visibility.
        // If it was visible, hide it. If it was hidden, show it.
        updateVisibilityFor(value)

        // If the value to filter on was included in the list of values
        if (filterValues.includes(value)) {
            // Then we remove it from the values to filter on
            filterValues.splice( filterValues.indexOf(value), 1 )
            // And we toggle the active and inactive classes
            item.classList.toggle('active');
            item.classList.toggle('inactive');
        } else {
            // Otherwise, we add it back into the list of values to filter on
            filterValues.push(value)
            // And we toggle the active and inactive classes
            item.classList.toggle('active');
            item.classList.toggle('inactive');
        }

        // After all work is done, check if the grid has become empty.
        checkEmptyGrid()
    })
}

There are two helper functions. First, a helper function to update visibility of items. Notice that the setTimeout() is a must, and that its duration must be identical to the CSS animation duration. This is because of a peculiarity when animating display properties.

function updateVisibilityFor(value) {
    // Select all DOM elements to toggle visibility for.
    const toggleMe = document.querySelectorAll(`.grid > div.${value}`)
    toggleMe.forEach((item) => {
        // If it is currently shown, then:
        if (item.classList.contains('post-shown')) {
            // SET A TIMEOUT to hide it. 
            // Note: The duration must be identical to the CSS animation duration
            setTimeout(() => {item.style.display = 'none'}, 200)
        }
        // If it is hidden, then:
        if (item.classList.contains('post-hidden')) {
            // show it (immediately).
            item.style.display = 'block'
        }

        // Always toggle the two classes indicating if it is shown or hidden
        item.classList.toggle('post-shown')
        item.classList.toggle('post-hidden')
    })
}

The second helper function is relatively simple: it checks if all items are hidden, and if so, it shows the piece of text indicated by emptygrid.

function checkEmptyGrid() {
    // Grab all items in the grid
    const items = document.querySelectorAll(`.grid > div`)
    // Grab the piece of text you see when the grid is empty
    const text = document.querySelector('#emptygrid') 

    // Loop through all items in the grid
    let found = false;
    for (const item of items) {
        // If at least one of them is shown
        if (item.classList.contains('post-shown')) {
            // Then we don't have to do anything
            found = true
            break
        }
    }

    // Set the display style of the text accordingly
    text.style.display = !found ? 'unset' : 'none'
}

There are CSS animations on hiding and showing the items. This is achieved using following classes (CSS truncated to only relevant items). Note that the duration of all the animations are 200 ms – this must be identical to the delay in setTimeout() when toggling visibility.

.post-item {
    transition: all .2s ease;
}

.post-hidden {
    animation: hide .2s;
    animation-fill-mode: forwards;
    display: block;
}

.post-shown {
    animation: show .2s;
    display: block;
}

The related keyframes are shown below. Note that for the hide animation there is a keyframe at 99% - this is again because of the peculiarity when animating display properties.

@keyframes show {
    0% {
        opacity: 0;
        transform: scale(0);
    }

    100% {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes hide {
    0% {
        display: block;
        opacity: 1;
        transform: scale(1);
    }
    99% {
        display: block;
        opacity: 0;
        transform: scale(0);
    }
    100% {
        display: none;
        opacity: 0;
        transform: scale(0);
    }
}

The source code is available on my github, in case you want to see.