Skip to content

Commit

Permalink
💻 Make the public adventures infinite scroll
Browse files Browse the repository at this point in the history
The public adventures page had a pagination mechanism, with "next
page"/"prev page" buttons.

A more modern idiom these days is to use "infinite scroll": once the
user scrolls to the bottom of the container, we fetch the next bit
of results, and so on.
  • Loading branch information
rix0rrr committed Jan 22, 2025
1 parent 2b8c98d commit a3291d3
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 84 deletions.
Binary file added static/images/spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions templates/public-adventures/incl-adventure-list-elements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{##
# List the adventure elements, with infinite scroll
#
#
#}
{% for adventure in adventures %}
<div
class="adventure-item text-gray-800 flex flex-col cursor-pointer p-2 border-b border-gray-300"
tabindex="0"
_="on click remove .selected from .adventure-item then add .selected to me end on keyup[key is 'Enter'] send click to me"
hx-get="/public-adventures/preview/{{ adventure.id }}" hx-target="#preview-div"
>
<div class="flex-1">
<span class="text-xl min-h-28">{{ adventure.name }}</span>
</div>
{#
<div alt="This adventure has been cloned {{ adventure.cloned_times }} times">
{{ adventure.cloned_stars }}
{{ adventure.cloned_times }}
{% for x in range(adventure.cloned_stars) %}
<span class="fa fa-star text-green-500"></span>
{% endfor %}
</div>
#}
<div class="flex-none text-xs">
<div class="flex">
<div class="flex-1 text-gray-500">{{ adventure.creator }}</div>
<div>
<span class="flex-1 text-gray-500" title="{{ adventure.date|jsts_to_unix|datetimeformat }}">
{{ adventure.date|jsts_to_unix|format_date_rel }}
</span>
</div>
</div>
<div class="flex flex-row">
<div class="flex-1 text-gray-500">{{_('level')}} {{ adventure.levels|join(', ') }}</div>
<div class="flex-none">
{% if adventure.solution_example %}
<span class="fa fa-book text-gray-500" title="{{_('this_adventure_has_an_example_solution')}}"></span>
{% endif %}
</div>
</div>
{% if adventure.tags %}
<div>
{% for tag in adventure.tags %}
<span class="inline-block bg-pink-200 rounded-full px-2 text-xs text-gray-700 mr-1 mb-1">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}

{# Infinite scroll token #}
{% if next_page_token %}
<div
hx-get="/public-adventures/more"
hx-trigger="intersect once"
hx-swap="outerHTML"
hx-include="this">

<input type="hidden" name="selected_lang" value="{{selected_lang}}">
<input type="hidden" name="selected_level" value="{{selected_level}}">
<input type="hidden" name="q" value="{{q}}">
<input type="hidden" name="selected_tag" value="{{selected_tag}}">
<input type="hidden" name="page" value="{{next_page_token}}">

<img src="{{static('/images/spinner.gif')}}" width="50" class="m-auto py-4">

</div>
{% endif %}
85 changes: 4 additions & 81 deletions templates/public-adventures/incl-adventure-list.html
Original file line number Diff line number Diff line change
@@ -1,92 +1,15 @@
<div class="flex gap-4">
<div class="flex-none w-64 relative" style="max-height: 40em;">
<div class="overflow-auto bg-white shadow-md htmx-resetscroll pt-8 pb-16 h-full"
<div class="overflow-auto bg-white shadow-md htmx-resetscroll h-full"
data-cy="search-results">

{% for adventure in adventures %}
<div
class="adventure-item text-gray-800 flex flex-col cursor-pointer p-2 border-b border-gray-300"
tabindex="0"
_="on click remove .selected from .adventure-item then add .selected to me end on keyup[key is 'Enter'] send click to me"
hx-get="/public-adventures/preview/{{ adventure.id }}" hx-target="#preview-div"
>
<div class="flex-1">
<span class="text-xl min-h-28">{{ adventure.name }}</span>
</div>
{#
<div alt="This adventure has been cloned {{ adventure.cloned_times }} times">
{{ adventure.cloned_stars }}
{{ adventure.cloned_times }}
{% for x in range(adventure.cloned_stars) %}
<span class="fa fa-star text-green-500"></span>
{% endfor %}
</div>
#}
<div class="flex-none text-xs">
<div class="flex">
<div class="flex-1 text-gray-500">{{ adventure.creator }}</div>
<div>
<span class="flex-1 text-gray-500" title="{{ adventure.date|jsts_to_unix|datetimeformat }}">
{{ adventure.date|jsts_to_unix|format_date_rel }}
</span>
</div>
</div>
<div class="flex flex-row">
<div class="flex-1 text-gray-500">{{_('level')}} {{ adventure.levels|join(', ') }}</div>
<div class="flex-none">
{% if adventure.solution_example %}
<span class="fa fa-book text-gray-500" title="{{_('this_adventure_has_an_example_solution')}}"></span>
{% endif %}
</div>
</div>
{% if adventure.tags %}
<div>
{% for tag in adventure.tags %}
<span class="inline-block bg-pink-200 rounded-full px-2 text-xs text-gray-700 mr-1 mb-1">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}

{# Pagination buttons #}
<div class="flex gap-2 w-full items-stretch mt-4 p-2">
<div class="flex-1">
{% if prev_page_token %}
<form hx-get="/public-adventures/" hx-target="#public_adventures_page_div">
<input type="hidden" name="selected_lang" value="{{selected_lang}}">
<input type="hidden" name="selected_level" value="{{selected_level}}">
<input type="hidden" name="q" value="{{q}}">
<input type="hidden" name="selected_tag" value="{{selected_tag}}">
<input type="hidden" name="page" value="{{prev_page_token}}">
<button type="submit"
id="prev_button"
class="green-btn px-2 w-full h-full">
{{_('previous_page')}}<br>«</button>
</form>
{% endif %}
</div>
<div class="flex-1">
{% if next_page_token %}
<form hx-get="/public-adventures/" hx-target="#public_adventures_page_div">
<input type="hidden" name="selected_lang" value="{{selected_lang}}">
<input type="hidden" name="selected_level" value="{{selected_level}}">
<input type="hidden" name="q" value="{{q}}">
<input type="hidden" name="selected_tag" value="{{selected_tag}}">
<input type="hidden" name="page" value="{{next_page_token}}">
<button
id="next_button"
class="green-btn px-2 w-full h-full">
{{_('next_page')}}<br>»</button>
</form>
{% endif %}
</div>
</div>
{% include "public-adventures/incl-adventure-list-elements.html" %}
</div>

<!--
<div class="inset-x-0 h-12 absolute top-0 bg-gradient-to-b from-white"></div>
<div class="inset-x-0 h-12 absolute bottom-0 bg-gradient-to-t from-white"></div>
-->
</div>
{# min-w-0 is necessary to prevent overflowing content #}
<div class="flex-1 min-w-0">
Expand Down
23 changes: 20 additions & 3 deletions website/public_adventures.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def _all_tags(self, _ttl_hash):
return list(sorted(self.db.get_public_adventures_tags()))

@route("/", methods=["GET"])
@route("/more", methods=["GET"])
@requires_teacher
def search(self, user):
"""Render the search page including the form."""
Expand All @@ -43,7 +44,7 @@ def search(self, user):
selected_level = request.args.get("selected_level", '')
selected_lang = request.args.get("selected_lang", g.lang)
q = request.args.get("q", "")
selected_tag = request.args.get("selected_tag")
selected_tag = request.args.get("selected_tag", '')
page = request.args.get("page", '')

# Dropbox options
Expand All @@ -58,9 +59,26 @@ def search(self, user):
q or None,
pagination_token=page if page else None)
next_page_token = adventures.next_page_token
prev_page_token = adventures.prev_page_token

adventures = [self.enhance_adventure_for_list(a) for a in adventures]
print(request.path)

# The '/more' endpoint is used only to load elements into the infinite scroll
# container.
if request.path.endswith('/more'):
return render_template(f"public-adventures/incl-adventure-list-elements.html",
adventures=adventures,
selected_level=selected_level,
selected_lang=selected_lang,
selected_tag=selected_tag,
q=q,
next_page_token=next_page_token)

# Otherwise, we return either the full page with the search control for
# a browser request, or the result chrome with the initial set of results
# for an HTMX request.

# (The HTMX call structure can probably be simplified a little here)

template = "body" if is_hx_request() else "index"

Expand All @@ -76,7 +94,6 @@ def search(self, user):

adventures=adventures,
next_page_token=next_page_token,
prev_page_token=prev_page_token,

user=user,
current_page="public-adventures",
Expand Down

0 comments on commit a3291d3

Please sign in to comment.