Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💻 Replace public adventures pagination with infinite scroll #6124

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
71 changes: 71 additions & 0 deletions templates/public-adventures/incl-adventure-list-elements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{##
# List the adventure elements, with infinite scroll
#
# If there is a 'next_page_token', add a div that will fetch
# the next block of results from the /public-adventures/more endpoint.
#}
{% 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 %}
86 changes: 2 additions & 84 deletions templates/public-adventures/incl-adventure-list.html
Original file line number Diff line number Diff line change
@@ -1,92 +1,10 @@
<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
34 changes: 25 additions & 9 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,25 +59,40 @@ 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]

# Dictionary with the query & pagination args
query_args = dict(
selected_level=selected_level,
selected_lang=selected_lang,
selected_tag=selected_tag,
q=q,
next_page_token=next_page_token
)

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

# 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"

return render_template(f"public-adventures/{template}.html",
available_languages=available_languages,
available_levels=available_levels,
available_tags=available_tags,
selected_level=selected_level,
selected_lang=selected_lang,
selected_tag=selected_tag,
q=q,
page=page,

adventures=adventures,
next_page_token=next_page_token,
prev_page_token=prev_page_token,
**query_args,

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