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:
parent
21e968f0ba
commit
e577bfef5d
5 changed files with 171 additions and 91 deletions
|
@ -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>')
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,72 +1,10 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block main %}
|
|
||||||
{% set locale = 'en' %}
|
{% set locale = 'en' %}
|
||||||
<div class="mt-5 text-center">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-900">
|
|
||||||
Application Catalog
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-screen-md mx-auto mt-3 mb-3">
|
|
||||||
<div class="flex flex-row">
|
|
||||||
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
|
|
||||||
<label for="search" class="sr-only"> Search </label>
|
|
||||||
|
|
||||||
<input
|
{% macro appCard(app, infos, timestamp_now, catalog) -%}
|
||||||
type="text"
|
<div class="search-entry"
|
||||||
id="search"
|
data-addedincatalog="{{ ((timestamp_now - infos['added_in_catalog']) / 3600 / 24) | int }}"
|
||||||
placeholder="Search for..."
|
data-category="{%- if infos['category'] -%}{{ infos['category'] }}{%- endif -%}"
|
||||||
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
name="selectcategory"
|
|
||||||
id="selectcategory"
|
|
||||||
class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 basis-1/3 "
|
|
||||||
>
|
>
|
||||||
<option value="">All apps</option>
|
|
||||||
{% for id, category in catalog['categories'].items() %}
|
|
||||||
{{ category['title'][locale] }}
|
|
||||||
<option value="{{ id }}">{{ category['title'][locale] }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" mx-auto pt-2 text-center">
|
|
||||||
<div class="inline-block px-2">
|
|
||||||
<label for="sortbynew" class="inline-block relative h-4 w-7 cursor-pointer">
|
|
||||||
<input type="checkbox" id="sortbynew" class="peer sr-only" />
|
|
||||||
|
|
||||||
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
Sort by newest
|
|
||||||
</div>
|
|
||||||
<div class="inline-block px-2">
|
|
||||||
<label for="onlyfav" class="inline-block relative h-4 w-7 cursor-pointer">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
Show only your bookmarks
|
|
||||||
</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() %}
|
|
||||||
<div class="search" data-addedincatalog="{{ ((timestamp_now - infos['added_in_catalog']) / 3600 / 24) | int }}">
|
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('app_info', app_id=app) }}"
|
href="{{ url_for('app_info', app_id=app) }}"
|
||||||
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200"
|
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200"
|
||||||
|
@ -118,33 +56,169 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="mt-5 text-center">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">
|
||||||
|
Application Catalog
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-screen-md mx-auto mt-3 mb-3">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
|
||||||
|
<label for="search" class="sr-only"> Search </label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
|
placeholder="Search for..."
|
||||||
|
{% 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">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="basis-1/3">
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="selectcategory"
|
||||||
|
id="selectcategory"
|
||||||
|
class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<option value="">All apps</option>
|
||||||
|
{% for id, category in catalog['categories'].items() %}
|
||||||
|
{{ category['title'][locale] }}
|
||||||
|
<option {% if id == init_category %}selected{% endif %} value="{{ id }}" {{ id == init_category }} >{{ category['title'][locale] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center items-center pt-2 text-center text-sm">
|
||||||
|
<div class="inline-block px-2">
|
||||||
|
Sort by
|
||||||
|
<select
|
||||||
|
name="selectsort"
|
||||||
|
id="selectsort"
|
||||||
|
class="inline-block rounded-md border-gray-200 text-sm ml-1 pl-1 pr-7 h-8 py-0"
|
||||||
|
>
|
||||||
|
<option value="alpha">Alphabetical</option>
|
||||||
|
<option value="newest">Newest</option>
|
||||||
|
<option value="popularity">Popularity</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="inline-block flex items-center px-2">
|
||||||
|
<label for="onlyfav" class="inline-block relative mr-2 h-4 w-7 cursor-pointer">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
Show only your bookmarks
|
||||||
|
</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 %}
|
{% endfor %}
|
||||||
</div>
|
</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">
|
||||||
|
{% for app, infos in catalog['apps'].items() %}
|
||||||
|
{% if not infos['level'] or infos['level'] <= 4 %}
|
||||||
|
{{ appCard(app, infos, timestamp_now, catalog) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</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 %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Add table
Reference in a new issue