1
0
Fork 0
mirror of https://github.com/YunoHost/apps.git synced 2024-09-03 20:06:07 +02:00

appstore: iterate on search/filters, cosmetics

This commit is contained in:
Alexandre Aubin 2023-08-29 16:57:32 +02:00
parent 21e968f0ba
commit e577bfef5d
5 changed files with 171 additions and 91 deletions

View file

@ -118,8 +118,8 @@ def index():
@app.route('/catalog') @app.route('/catalog')
def browse_catalog(category_filter=None): def browse_catalog():
return render_template("catalog.html", user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time())) return render_template("catalog.html", init_search=request.args.get("search"), init_category=request.args.get("category"), user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time()))
@app.route('/app/<app_id>') @app.route('/app/<app_id>')

View file

@ -10,7 +10,7 @@
<style type="text/tailwindcss"> <style type="text/tailwindcss">
@layer utilities { @layer utilities {
.btn { .btn {
@apply text-sm font-medium rounded-md px-4 py-2.5 transition; @apply text-sm font-medium rounded-md px-4 py-2 transition;
} }
.btn-sm { .btn-sm {
@apply text-xs font-medium rounded-md px-2 py-2 transition; @apply text-xs font-medium rounded-md px-2 py-2 transition;

View file

@ -1,6 +1,65 @@
{% set locale = 'en' %}
{% macro appCard(app, infos, timestamp_now, catalog) -%}
<div class="search-entry"
data-addedincatalog="{{ ((timestamp_now - infos['added_in_catalog']) / 3600 / 24) | int }}"
data-category="{%- if infos['category'] -%}{{ infos['category'] }}{%- endif -%}"
>
<a
href="{{ url_for('app_info', app_id=app) }}"
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200"
>
<div class="sm:flex sm:justify-between sm:gap-4">
<div class="sm:shrink-0">
<img
{% if infos['logo_hash'] %}
src="https://app.yunohost.org/default/v3/logos/{{ infos['logo_hash'] }}.png"
{% else %}
src="{{ url_for('static', filename='app_logo_placeholder.png') }}"
{% endif %}
loading="lazy"
class="h-16 w-16 rounded-lg object-cover shadow-sm mt-1"
/>
</div>
<div class="w-full">
<span class="flex">
<h3 class="grow text-md font-bold text-gray-900">
{{ infos['manifest']['name'] }}
</h3>
<span class="pt-1 pr-2 text-xs">
{% if infos['level'] == 0 %}
<i class="fa fa-exclamation-circle text-red-500 py-0.5" aria-hidden="true"></i>
{% elif infos['level']|int <= 4 %}
<i class="fa fa-exclamation-triangle text-orange-500 py-0.5" aria-hidden="true"></i>
{% elif infos['level'] == 8 %}
<i class="fa fa-diamond text-teal-500 py-0.5" aria-hidden="true"></i>
{% endif %}
<span class="group text-violet-500 hover:text-white hover:bg-violet-500 rounded-md px-1 py-0.5">
123 <i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
</span>
</span>
</span>
<p class="max-w-[40ch] text-xs text-gray-500">
{{ infos['manifest']['description']['en'] }}
</p>
<div class="hidden">
{{ infos["potential_alternative_to"]|join(', ') }}
</div>
{% if infos['category'] %}
<span class="rounded-full px-2.5 py-0.5 text-[10px] border text-{{ catalog['categories'][infos['category']]['color'] }}-500 border-{{ catalog['categories'][infos['category']]['color'] }}-400 ">
{{ catalog['categories'][infos['category']]['title'][locale].lower() }}
</span>
{% endif %}
</div>
</div>
</a>
</div>
{%- endmacro %}
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
{% set locale = 'en' %}
<div class="mt-5 text-center"> <div class="mt-5 text-center">
<h2 class="text-2xl font-bold text-gray-900"> <h2 class="text-2xl font-bold text-gray-900">
Application Catalog Application Catalog
@ -11,45 +70,49 @@
<div class="px-2 inline-block relative basis-2/3 text-gray-700"> <div class="px-2 inline-block relative basis-2/3 text-gray-700">
<label for="search" class="sr-only"> Search </label> <label for="search" class="sr-only"> Search </label>
<input <input
type="text" type="text"
id="search" id="search"
placeholder="Search for..." placeholder="Search for..."
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10" {% if init_search %}value="{{ init_search }}"{% endif %}
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2 pe-10"
/> />
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4"> <span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">
<i class="fa fa-search" aria-hidden="true"></i> <i class="fa fa-search" aria-hidden="true"></i>
</span> </span>
</div> </div>
<select <div class="basis-1/3">
name="selectcategory"
id="selectcategory" <select
class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 basis-1/3 " name="selectcategory"
> id="selectcategory"
<option value="">All apps</option> class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 py-1.5"
{% for id, category in catalog['categories'].items() %} >
{{ category['title'][locale] }} <option value="">All apps</option>
<option value="{{ id }}">{{ category['title'][locale] }}</option> {% for id, category in catalog['categories'].items() %}
{% endfor %} {{ category['title'][locale] }}
</select> <option {% if id == init_category %}selected{% endif %} value="{{ id }}" {{ id == init_category }} >{{ category['title'][locale] }}</option>
{% endfor %}
</select>
</div>
</div> </div>
<div class=" mx-auto pt-2 text-center"> <div class="flex flex-row justify-center items-center pt-2 text-center text-sm">
<div class="inline-block px-2"> <div class="inline-block px-2">
<label for="sortbynew" class="inline-block relative h-4 w-7 cursor-pointer"> Sort by
<input type="checkbox" id="sortbynew" class="peer sr-only" /> <select
name="selectsort"
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500"> id="selectsort"
</span> class="inline-block rounded-md border-gray-200 text-sm ml-1 pl-1 pr-7 h-8 py-0"
>
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3"> <option value="alpha">Alphabetical</option>
</span> <option value="newest">Newest</option>
</label> <option value="popularity">Popularity</option>
Sort by newest </select>
</div> </div>
<div class="inline-block px-2"> <div class="inline-block flex items-center px-2">
<label for="onlyfav" class="inline-block relative h-4 w-7 cursor-pointer"> <label for="onlyfav" class="inline-block relative mr-2 h-4 w-7 cursor-pointer">
<input type="checkbox" id="onlyfav" class="peer sr-only" /> <input type="checkbox" id="onlyfav" class="peer sr-only" />
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500"> <span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
@ -63,88 +126,99 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-10">
{% for app, infos in catalog['apps'].items() %}
{% if infos['level'] and infos['level'] > 4 %}
{{ appCard(app, infos, timestamp_now, catalog) }}
{% endif %}
{% endfor %}
</div>
<div id="noResultFound" class="text-center pt-5 hidden">
<p class="text-lg font-bold text-gray-900 mb-5">
No results found
</p>
<p class="text-md text-gray-900">
Not finding what you are looking for ?<br/>
Checkout the wishlist !
</p>
</div>
<div id="lowQualityAppTitle" class="text-center pt-10">
<h2 class="text-lg font-bold text-gray-900">
Applications currently broken or low-quality
</h2>
<p class="text-sm">
There are apps which failed our automatic tests.<br/>
This is usually a temporary situation which requires packagers to fix something in the app.
</p>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-10"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-10">
{% for app, infos in catalog['apps'].items() %} {% for app, infos in catalog['apps'].items() %}
<div class="search" data-addedincatalog="{{ ((timestamp_now - infos['added_in_catalog']) / 3600 / 24) | int }}"> {% if not infos['level'] or infos['level'] <= 4 %}
<a {{ appCard(app, infos, timestamp_now, catalog) }}
href="{{ url_for('app_info', app_id=app) }}" {% endif %}
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200"
>
<div class="sm:flex sm:justify-between sm:gap-4">
<div class="sm:shrink-0">
<img
{% if infos['logo_hash'] %}
src="https://app.yunohost.org/default/v3/logos/{{ infos['logo_hash'] }}.png"
{% else %}
src="{{ url_for('static', filename='app_logo_placeholder.png') }}"
{% endif %}
loading="lazy"
class="h-16 w-16 rounded-lg object-cover shadow-sm mt-1"
/>
</div>
<div class="w-full">
<span class="flex">
<h3 class="grow text-md font-bold text-gray-900">
{{ infos['manifest']['name'] }}
</h3>
<span class="pt-1 pr-2 text-xs">
{% if infos['level'] == 0 %}
<i class="fa fa-exclamation-circle text-red-500 py-0.5" aria-hidden="true"></i>
{% elif infos['level']|int <= 4 %}
<i class="fa fa-exclamation-triangle text-orange-500 py-0.5" aria-hidden="true"></i>
{% elif infos['level'] == 8 %}
<i class="fa fa-diamond text-teal-500 py-0.5" aria-hidden="true"></i>
{% endif %}
<span class="group text-violet-500 hover:text-white hover:bg-violet-500 rounded-md px-1 py-0.5">
123 <i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
</span>
</span>
</span>
<p class="max-w-[40ch] text-xs text-gray-500">
{{ infos['manifest']['description']['en'] }}
</p>
<div class="hidden">
{{ infos["potential_alternative_to"]|join(', ') }}
</div>
{% if infos['category'] %}
<span class="rounded-full px-2.5 py-0.5 text-[10px] border text-{{ catalog['categories'][infos['category']]['color'] }}-500 border-{{ catalog['categories'][infos['category']]['color'] }}-400 ">
{{ catalog['categories'][infos['category']]['title'][locale].lower() }}
</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %} {% endfor %}
</div> </div>
<script> <script>
// A little delay // A little delay
let typingTimer; let typingTimer;
let typeInterval = 500; // Half a second let typeInterval = 500; // Half a second
let searchInput = document.getElementById('search'); let searchInput = document.getElementById('search');
let selectCategory = document.getElementById('selectcategory');
function liveSearch() { function liveSearch() {
// Locate the card elements // Locate the card elements
let entries = document.querySelectorAll('.search') let entries = document.querySelectorAll('.search-entry')
// Locate the search input // Locate the search input
let search_query = searchInput.value; let search_query = searchInput.value.trim().toLowerCase();
let selectedCategory = selectCategory.value.trim();
let at_least_one_match = false;
// Loop through the entries // Loop through the entries
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
// If the text is within the card... // If the text is within the card and the text matches the search query
if(entries[i].textContent.toLowerCase() if ((entries[i].textContent.toLowerCase().includes(search_query))
// ...and the text matches the search query... && (! selectedCategory || (entries[i].dataset.category == selectedCategory)))
.includes(search_query.toLowerCase())) { {
// ...remove the `.is-hidden` class. // ...remove the `.is-hidden` class.
entries[i].classList.remove("hidden"); entries[i].classList.remove("hidden");
} else { at_least_one_match = true;
}
else
{
// Otherwise, add the class. // Otherwise, add the class.
entries[i].classList.add("hidden"); entries[i].classList.add("hidden");
} }
} }
if (at_least_one_match === false)
{
document.getElementById('lowQualityAppTitle').classList.add("hidden");
document.getElementById('noResultFound').classList.remove("hidden");
}
else
{
document.getElementById('lowQualityAppTitle').classList.remove("hidden");
document.getElementById('noResultFound').classList.add("hidden");
}
updateQueryArgsInURL()
}
function updateQueryArgsInURL() {
let search_query = searchInput.value.trim();
let category = selectCategory.value.trim();
if ('URLSearchParams' in window) {
var queryArgs = new URLSearchParams(window.location.search)
if (search_query) { queryArgs.set("search", search_query) } else { queryArgs.delete("search"); };
if (category) { queryArgs.set("category", category) } else { queryArgs.delete("category"); };
history.pushState(null, '', window.location.pathname + '?' + queryArgs.toString());
}
} }
searchInput.addEventListener('keyup', () => { searchInput.addEventListener('keyup', () => {
@ -152,6 +226,12 @@
typingTimer = setTimeout(liveSearch, typeInterval); typingTimer = setTimeout(liveSearch, typeInterval);
}); });
selectCategory.addEventListener('change', () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(liveSearch, typeInterval);
});
liveSearch();
</script> </script>
{% endblock %} {% endblock %}

View file

@ -23,7 +23,7 @@
{% for id, category in catalog['categories'].items() %} {% for id, category in catalog['categories'].items() %}
<div class="text-center border rounded-lg h-32"> <div class="text-center border rounded-lg h-32">
<a <a
href="{{ url_for('browse_catalog', category_filter=id) }}" href="{{ url_for('browse_catalog', category=id) }}"
class="h-full relative block overflow-hidden hover:bg-gray-200 pt-10" class="h-full relative block overflow-hidden hover:bg-gray-200 pt-10"
> >
<h3 class="text-md font-bold text-gray-900"> <h3 class="text-md font-bold text-gray-900">

View file

@ -15,7 +15,7 @@
type="text" type="text"
id="search" id="search"
placeholder="Search for..." placeholder="Search for..."
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10" class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2 pe-10"
/> />
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4"> <span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">