2 version of filtering and persist filter settings.

This commit is contained in:
Krzysztof Płaczek
2025-07-29 15:24:52 +02:00
parent e836daa2b1
commit ce08be27a7
2 changed files with 149 additions and 84 deletions

View File

@@ -13,8 +13,8 @@
<body> <body>
<div x-data="bookmeterList"> <div x-data="bookmeterList">
<div x-data="{ advancedSearch: $persist(0).as('advanced-search')}" x-cloak> <div x-cloak>
<form class="flex items-center m-4"> <form class="flex items-center m-2" x-show="!advancedSearch">
<div class="relative w-full d-block max-w-md "> <div class="relative w-full d-block max-w-md ">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"> <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 21"> <svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 21">
@@ -30,18 +30,50 @@
</button> </button>
</div> </div>
<div class="relative inline-block m-4 text-blue-400 inline cursor-pointer"> <div class="relative inline-block m-4 text-blue-400 inline cursor-pointer">
<div @click="advancedSearch = !advancedSearch">advanced search toggle</div> <div @click="advancedSearch = true">zaawansowane filtrowanie</div>
</div> </div>
</form> </form>
<div x-show="advancedSearch" class="flex m-4"> <div x-show="advancedSearch" class="flex m-4">
<input x-model="username" placeholder="użytkownik" type="text" class="mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" /> <div class="relative w-full d-block max-w-xs mr-2">
<input x-model="title" placeholder="tytuł" type="text" class="mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" /> <input x-model="username" placeholder="użytkownik" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" />
<input x-model="author" placeholder="autor" type="text" class="mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" /> <button x-show="username.length" @click="username=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
<input x-model="publisher" placeholder="wydawca" type="text" class="mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" /> <svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div> </div>
<div class="relative w-full d-block max-w-xs mr-2">
<input x-model="title" placeholder="tytuł" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" />
<button x-show="title.length" @click="title=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="relative w-full d-block max-w-xs mr-2">
<input x-model="author" placeholder="autor" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" />
<button x-show="author.length" @click="author=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="relative w-full d-block max-w-xs mr-2">
<input x-model="publisher" placeholder="wydawca" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4" />
<button x-show="publisher.length" @click="author=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="inline-block m-2 text-blue-400 inline cursor-pointer">
<div @click="advancedSearch = false">proste filtrowanie</div>
</div>
</div>
</div> </div>
<table class="table table-fixed w-full"> <table class="table table-fixed w-full text-sm">
<thead> <thead>
<tr> <tr>
<th class="text-right pl-1 w-[4rem]">#</th> <th class="text-right pl-1 w-[4rem]">#</th>
@@ -49,26 +81,54 @@
<th class="text-left pl-1">Tytuł</th> <th class="text-left pl-1">Tytuł</th>
<th class="text-left pl-1">Autor</th> <th class="text-left pl-1">Autor</th>
<th class="text-left pl-1">Wydawca</th> <th class="text-left pl-1">Wydawca</th>
<th class="text-right pr-4 w-[5rem]">Stron</th> <th class="text-right pr-4 w-[7rem]">Stron</th>
<th class="text-left pl-1">Format</th> <th class="text-left pl-1 w-[12rem]">Format</th>
<th class="text-right pr-4">Ocena</th> <th class="text-right w-[5rem]">Ocena</th>
<th class="text-right pr-4"></th> <th class="text-right pr-4 w-[5rem]"></th>
<th class="text-left pl-1">Link</th> <th class="text-left pl-1 w-[5rem]">Link</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template x-for="(item, key) in filteredItems" :key="item.index"> <template x-for="(item, key) in filteredItems" :key="item.index">
<tr class="text-base border-b border-slate-100 m-3 w-auto hover:bg-gray-100"> <tr class="border-b border-slate-100 m-3 w-auto hover:bg-gray-100">
<td class="pl-1 text-right" x-text="filteredItems.length-key"></td> <td class="pl-1 text-right" x-text="filteredItems.length-key"></td>
<td class="pl-1 w-3 py-2"><a :href="'?filter=@'+item.username" x-text="item.username"></a></td> <td class="pl-1 w-3 py-2">
<td class="pl-1 w-12"><a :href="'?filter='+item.book.title" x-text="item.book.title"></a></td> <template x-if="!advancedSearch">
<td class="pl-1 w-8"><a :href="'?filter='+item.book.author" x-text="item.book.author"></a></td> <a @click="nameFilter='@'+item.username" class="cursor-pointer" x-text="item.username"></a>
<td class="pl-1 w-8"><a :href="'?filter='+item.book.publisher" x-text="item.book.publisher"></a></td> </template>
<template x-if="advancedSearch">
<a @click="username=item.username" class="cursor-pointer" x-text="item.username"></a>
</template>
</td>
<td class="pl-1 w-12">
<template x-if="!advancedSearch">
<a @click="nameFilter=item.book.title" class="cursor-pointer" x-text="item.book.title"></a>
</template>
<template x-if="advancedSearch">
<a @click="title=item.book.title" class="cursor-pointer" x-text="item.book.title"></a>
</template>
</td>
<td class="pl-1 w-8">
<template x-if="!advancedSearch">
<a @click="nameFilter=item.book.author" class="cursor-pointer" x-text="item.book.author"></a>
</template>
<template x-if="advancedSearch">
<a @click="author=item.book.author" class="cursor-pointer" x-text="item.book.author"></a>
</template>
</td>
<td class="pl-1 w-8">
<template x-if="!advancedSearch">
<a @click="nameFilter=item.book.publisher" class="cursor-pointer" x-text="item.book.publisher"></a>
</template>
<template x-if="advancedSearch">
<a @click="publisher=item.book.publisher" class="cursor-pointer" x-text="item.book.publisher"></a>
</template>
</td>
<td class="pr-4 text-right w-[5rem] w-[5rem]"><a :href="item.book.pages" x-text="item.book.pages"></a></td> <td class="pr-4 text-right w-[5rem] w-[5rem]"><a :href="item.book.pages" x-text="item.book.pages"></a></td>
<td class="pl-1 w-1"><a :href="item.book.format" x-text="item.book.format"></a></td> <td class="pl-1"><a :href="item.book.format" x-text="item.book.format"></a></td>
<td class="pr-4 w-1 text-right"><a :href="item.book.rating" x-text="item.book.rating"></a></td> <td class="text-right"><a :href="item.book.rating" x-text="item.book.rating"></a></td>
<td class="pr-4 w-1 text-right"><a :href="item.book.likes" x-text="item.likes"></a></td> <td class="pr-4 text-right"><a :href="item.book.likes" x-text="item.likes"></a></td>
<td class="pl-1 w-1 py-2"> <td class="pl-1 py-2">
<a :href="'https://www.hejto.pl/wpis/'+item.slug"> <a :href="'https://www.hejto.pl/wpis/'+item.slug">
link link
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-gray-500 inline"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-gray-500 inline">
@@ -82,8 +142,8 @@
</tbody> </tbody>
<tfoot> <tfoot>
<tr x-show="filteredItems.length"> <tr x-show="filteredItems.length">
<td colspan="5"></td> <td colspan="4"></td>
<td class="align-top"> <td class="align-top" colspan="2">
<p class="text-right pr-4" x-text="'strony: '+stats().pages_total"></p> <p class="text-right pr-4" x-text="'strony: '+stats().pages_total"></p>
<p class="text-right pr-4" x-text="'strony śr: '+stats().pages_avg"></p> <p class="text-right pr-4" x-text="'strony śr: '+stats().pages_avg"></p>
</td> </td>
@@ -93,11 +153,11 @@
</template> </template>
</td> </td>
<td class="align-top"> <td class="align-top">
<p class="text-right pr-4" x-text="'ocena śr: '+stats().rating_avg"></p> <p class="text-right" x-text="'ocena śr: '+stats().rating_avg"></p>
</td> </td>
<td> <td>
<p class="text-right pr-4" x-text="'⚡: '+stats().likes_total"></p> <p class="text-right" x-text="'⚡: '+stats().likes_total"></p>
<p class="text-right pr-4" x-text="'⚡ śr: '+stats().likes_avg"></p> <p class="text-right" x-text="'⚡ śr: '+stats().likes_avg"></p>
</td> </td>
<td></td> <td></td>
</tr> </tr>

View File

@@ -6,66 +6,71 @@ import persist from '@alpinejs/persist'
window.Alpine = Alpine; window.Alpine = Alpine;
Alpine.plugin(persist) Alpine.plugin(persist)
Alpine.data('bookmeterList', () => ({ Alpine.data('bookmeterList', function () {
items: [], return {
filteredItems: [], items: [],
username: '', filteredItems: [],
publisher: '', username: '',
title: '', publisher: '',
author: '', title: '',
nameFilter: '', author: '',
async init() { nameFilter: '',
this.$watch('nameFilter', () => this.updateFilter()); advancedSearch: this.$persist(0).as('advanced-search'),
this.$watch('username', () => this.updateFilter()); async init() {
this.$watch('title', () => this.updateFilter()); this.$watch('nameFilter', () => this.updateFilter());
this.$watch('author', () => this.updateFilter()); this.$watch('username', () => this.updateFilter());
this.$watch('publisher', () => this.updateFilter()); this.$watch('title', () => this.updateFilter());
this.$watch('author', () => this.updateFilter());
this.$watch('publisher', () => this.updateFilter());
let qp = new URLSearchParams(window.location.search); let qp = new URLSearchParams(window.location.search);
if(qp.get('filter')) this.nameFilter = qp.get('filter'); if(qp.get('filter')) this.nameFilter = qp.get('filter');
const res = await fetch('/posts.json') const res = await fetch('/posts.json')
this.items = await res.json(); this.items = await res.json();
this.updateFilter(); this.updateFilter();
}, },
stats(){ stats(){
return { return {
'pages_total': this.filteredItems.reduce((a, b) => a + b.book.pages, 0) || '-', 'pages_total': this.filteredItems.reduce((a, b) => a + b.book.pages, 0) || '-',
'likes_total': this.filteredItems.reduce((a, b) => a + b.likes, 0) || '-', 'likes_total': this.filteredItems.reduce((a, b) => a + b.likes, 0) || '-',
'pages_avg': Math.round(this.filteredItems.reduce((a, b) => a + b.book.pages, 0)/this.filteredItems.length) || '-', 'pages_avg': Math.round(this.filteredItems.reduce((a, b) => a + b.book.pages, 0)/this.filteredItems.length) || '-',
'likes_avg': Math.round(this.filteredItems.reduce((a, b) => a + b.likes, 0)/this.filteredItems.length) || '-', 'likes_avg': Math.round(this.filteredItems.reduce((a, b) => a + b.likes, 0)/this.filteredItems.length) || '-',
'rating_avg': (this.filteredItems.reduce((a, b) => a + b.book.rating, 0)/this.filteredItems.length).toPrecision(2) || '-', 'rating_avg': (this.filteredItems.reduce((a, b) => a + b.book.rating, 0)/this.filteredItems.length).toPrecision(2) || '-',
'formats': this.filteredItems.reduce((acc, item) => { 'formats': this.filteredItems.reduce((acc, item) => {
let val = item.book.format.toLowerCase() || 'brak danych'; let val = item.book.format.toLowerCase() || 'brak danych';
acc[val] = acc[val] === undefined ? 1 : acc[val] += 1; acc[val] = acc[val] === undefined ? 1 : acc[val] += 1;
return acc; return acc;
}, {}) }, {})
}; };
}, },
updateFilter(){ updateFilter(){
let filter = this.nameFilter.toLowerCase(); let filter = this.nameFilter.toLowerCase();
this.filteredItems = this.items.filter(i => { this.filteredItems = this.items.filter(i => {
if(this.advancedSearch) {
return (
(!this.username || i.username.toLowerCase().includes(this.username.toLowerCase())) &&
(!this.title || i.book.title.toLowerCase().includes(this.title.toLowerCase())) &&
(!this.author || i.book.author.toLowerCase().includes(this.author.toLowerCase())) &&
(!this.publisher || i.book.publisher.toLowerCase().includes(this.publisher.toLowerCase()))
);
} else {
if(filter.startsWith('@') && i.username.toLowerCase().includes(filter.substring(1))) return true;
if (i.username.toLowerCase().includes(filter) ||
i.book.title.toLowerCase().includes(filter) || i.book.author.toLowerCase().includes(filter) || i.book.publisher.toLowerCase().includes(filter)) return true;
return false;
}
});
return ( this.updateURL();
(!this.username || i.username.toLowerCase().includes(this.username.toLowerCase())) && },
(!this.title || i.book.title.toLowerCase().includes(this.title.toLowerCase())) && updateURL() {
(!this.author || i.book.author.toLowerCase().includes(this.author.toLowerCase())) && let qp = new URLSearchParams();
(!this.publisher || i.book.publisher.toLowerCase().includes(this.publisher.toLowerCase())) if(this.nameFilter !== '') qp.set('filter', this.nameFilter);
);
if(filter.startsWith('@') && i.username.toLowerCase().includes(filter.substring(1))) return true;
if (i.username.toLowerCase().includes(filter) ||
i.book.title.toLowerCase().includes(filter) || i.book.author.toLowerCase().includes(filter) || i.book.publisher.toLowerCase().includes(filter)) return true;
return false;
});
this.updateURL(); history.replaceState(null, null, "?"+qp.toString());
}, }
updateURL() {
let qp = new URLSearchParams();
if(this.nameFilter !== '') qp.set('filter', this.nameFilter);
history.replaceState(null, null, "?"+qp.toString());
} }
})) })
Alpine.start(); Alpine.start();