added filter functionality

This commit is contained in:
Manan Jethwani 2021-05-18 17:57:22 +05:30 committed by Emmanuel Engelhart
parent 3a4e8303a0
commit bb92f26b60
10 changed files with 316 additions and 30 deletions

View File

@ -304,7 +304,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request) std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{ {
return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8"); return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8", true);
} }
std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request) std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)

View File

@ -106,7 +106,7 @@ class InternalServer {
std::string m_server_id; std::string m_server_id;
friend std::unique_ptr<Response> Response::build(const InternalServer& server); friend std::unique_ptr<Response> Response::build(const InternalServer& server);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype); friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item); friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg); friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg);

View File

@ -349,21 +349,21 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, bool wit
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType); add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
} }
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype) std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage)
{ {
return std::unique_ptr<ContentResponse>(new ContentResponse( return std::unique_ptr<ContentResponse>(new ContentResponse(
server.m_root, server.m_root,
server.m_verbose.load(), server.m_verbose.load(),
server.m_withTaskbar, server.m_withTaskbar && !isHomePage,
server.m_withLibraryButton, server.m_withLibraryButton,
server.m_blockExternalLinks, server.m_blockExternalLinks,
content, content,
mimetype)); mimetype));
} }
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype) { std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage) {
auto content = render_template(template_str, data); auto content = render_template(template_str, data);
return ContentResponse::build(server, content, mimetype); return ContentResponse::build(server, content, mimetype, isHomePage);
} }
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) : ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :

View File

@ -79,8 +79,8 @@ class Response {
class ContentResponse : public Response { class ContentResponse : public Response {
public: public:
ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype); ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& content, const std::string& mimetype); static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage = false);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype); static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage = false);
void set_taskbar(const std::string& bookName, const std::string& bookTitle); void set_taskbar(const std::string& bookName, const std::string& bookTitle);

View File

@ -19,6 +19,9 @@ skin/jquery-ui/jquery-ui.theme.min.css
skin/jquery-ui/jquery-ui.min.css skin/jquery-ui/jquery-ui.min.css
skin/caret.png skin/caret.png
skin/taskbar.js skin/taskbar.js
skin/langList.json
skin/categoryList.json
skin/isotope.pkgd.min.js
skin/index.js skin/index.js
skin/taskbar.css skin/taskbar.css
skin/block_external.js skin/block_external.js

View File

@ -0,0 +1,18 @@
{
"other": "Other",
"gutenberg": "Gutenberg",
"mooc": "Mooc",
"phet": "Phet",
"psiram": "Psiram",
"stack_exchange": "Stack Exchange",
"ted": "Ted",
"vikidia": "Vikidia",
"wikibooks": "Wikibooks",
"wikinews": "Wikinews",
"wikipedia": "Wikipedia",
"wikiquote": "Wikiquote",
"wikisource": "Wikisource",
"wikiversity": "Wikiversity",
"wikivoyage": "Wikivoyage",
"wiktionary": "Wiktionary"
}

View File

@ -1,16 +1,19 @@
(function() { (function() {
const root = $(`link[type='root']`).attr('href'); const root = $(`link[type='root']`).attr('href');
let isFetching = false;
let iso;
let bookMap = {};
const incrementalLoadingParams = { const incrementalLoadingParams = {
start: 0, start: 0,
count: viewPortToCount() count: viewPortToCount()
}; };
let isFetching = false; let params = new URLSearchParams(window.location.search);
let timer; const filterTypes = ['lang', 'category', 'q'];
function queryUrlBuilder() { function queryUrlBuilder() {
let url = `${root}/catalog/search?`; let url = `${root}/catalog/search?`;
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&"); url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
return url; return (url + (params.toString() ? `&${params.toString()}` : ''));
} }
function viewPortToCount(){ function viewPortToCount(){
@ -21,41 +24,109 @@
return node.querySelector(query).innerHTML; return node.querySelector(query).innerHTML;
} }
function generateBookHtml(book) { function generateBookHtml(book, sort = false) {
const link = book.querySelector('link').getAttribute('href'); const link = book.querySelector('link').getAttribute('href');
const title = getInnerHtml(book, 'title'); const title = getInnerHtml(book, 'title');
const description = getInnerHtml(book, 'summary'); const description = getInnerHtml(book, 'summary');
const linkTag = document.createElement('a');
const id = getInnerHtml(book, 'id'); const id = getInnerHtml(book, 'id');
const iconUrl = getInnerHtml(book, 'icon'); const iconUrl = getInnerHtml(book, 'icon');
const articleCount = getInnerHtml(book, 'articleCount'); const articleCount = getInnerHtml(book, 'articleCount');
const mediaCount = getInnerHtml(book, 'mediaCount'); const mediaCount = getInnerHtml(book, 'mediaCount');
linkTag.setAttribute('class', 'book');
linkTag.setAttribute('data-id', id);
linkTag.setAttribute('href', link);
if (sort) {
linkTag.setAttribute('data-idx', bookMap[id]);
}
return `<a href='${link}' data-id='${id}'><div class='book'> linkTag.innerHTML = `<div class='book__background' style="background-image: url('${iconUrl}');">
<div class='book__background' style="background-image: url('${iconUrl}');">
<div class='book__title' title='${title}'>${title}</div> <div class='book__title' title='${title}'>${title}</div>
<div class='book__description' title='${description}'>${description}</div> <div class='book__description' title='${description}'>${description}</div>
<div class='book__info'>${articleCount} articles, ${mediaCount} medias</div> <div class='book__info'>${articleCount} articles, ${mediaCount} medias</div>
</div> </div>`;
</div></a>`; return linkTag;
} }
async function loadAndDisplayBooks() { async function loadBooks() {
if (isFetching) return; return await fetch(queryUrlBuilder()).then(async (resp) => {
isFetching = true;
fetch(queryUrlBuilder()).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml'); const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
const books = data.querySelectorAll('entry'); const books = data.querySelectorAll('entry');
let bookHtml = ''; books.forEach((book, idx) => {
books.forEach((book) => {bookHtml += generateBookHtml(book)}); bookMap[getInnerHtml(book, 'id')] = idx;
document.querySelector('.book__list').innerHTML += bookHtml; });
incrementalLoadingParams.start += books.length; incrementalLoadingParams.start += books.length;
if (books.length < incrementalLoadingParams.count) { if (books.length < incrementalLoadingParams.count) {
incrementalLoadingParams.count = 0; incrementalLoadingParams.count = 0;
} }
isFetching = false; return books;
}); });
} }
async function loadAndDisplayOptions(nodeQuery, query) {
// currently taking an array in place of query, will replace it with query while fetching data from backend later on.
await fetch(query)
.then(async (resp) => {
const data = await resp.json();
Object.keys(data).forEach((option) => {
document.querySelector(nodeQuery).innerHTML += `<option value='${option}'>${data[option]}</option>`;
});
});
}
async function loadAndDisplayBooks(sort = false) {
if (isFetching) return;
isFetching = true;
let books = await loadBooks();
const booksToFilter = new Set();
const booksToDelete = new Set();
iso.arrange({
filter: function (idx, elem) {
const id = elem.getAttribute('data-id');
const retVal = bookMap.hasOwnProperty(id);
if (retVal) {
booksToFilter.add(id);
if (sort) {
elem.setAttribute('data-idx', bookMap[id]);
iso.updateSortData(elem);
}
} else {
booksToDelete.add(elem);
}
return retVal;
}
});
books = [...books].filter((book) => {return !booksToFilter.has(getInnerHtml(book, 'id'))});
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {iso.insert(generateBookHtml(book, sort))});
isFetching = false;
}
async function filterBooks(filterType, filterValue) {
isFetching = false;
incrementalLoadingParams.start = 0;
incrementalLoadingParams.count = viewPortToCount();
bookMap = {};
params = new URLSearchParams(window.location.search);
if (!filterValue) {
params.delete(filterType);
} else {
params.set(filterType, filterValue);
}
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
await loadAndDisplayBooks(true);
}
window.addEventListener('popstate', async () => {
bookMap = {};
isFetching = false;
incrementalLoadingParams.start = 0;
incrementalLoadingParams.count = viewPortToCount();
params = new URLSearchParams(window.location.search);
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
loadAndDisplayBooks(true);
});
async function loadSubset() { async function loadSubset() {
if (incrementalLoadingParams.count && window.innerHeight + window.scrollY >= document.body.offsetHeight) { if (incrementalLoadingParams.count && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
loadAndDisplayBooks(); loadAndDisplayBooks();
@ -73,6 +144,26 @@
window.addEventListener('scroll', loadSubset); window.addEventListener('scroll', loadSubset);
window.onload = async () => { window.onload = async () => {
iso = new Isotope( '.book__list', {
itemSelector: '.book',
getSortData:{
weight: function( itemElem ) {
const index = itemElem.getAttribute('data-idx');
return index ? parseInt(index) : Infinity;
}
},
sortBy: 'weight'
});
loadAndDisplayBooks(); loadAndDisplayBooks();
loadAndDisplayOptions('#languageFilter', `${root}/skin/langList.json`);
loadAndDisplayOptions('#categoryFilter', `${root}/skin/categoryList.json`);
for (const key of params.keys()) {
document.getElementsByName(key)[0].value = params.get(key);
}
filterTypes.forEach((filter) => {
const filterTag = document.getElementsByName(filter)[0];
filterTag.addEventListener('change', () => {filterBooks(filterTag.name, filterTag.value)});
});
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()}
} }
})(); })();

12
static/skin/isotope.pkgd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

124
static/skin/langList.json Normal file
View File

@ -0,0 +1,124 @@
{
"aar": "Afar",
"afr": "Afrikaans",
"aka": "Akan",
"amh": "Amharic",
"ara": "Arabic",
"asm": "Assamese",
"aze": "Azerbaijani",
"bak": "Bashkir",
"bel": "Belarusian",
"bul": "Bulgarian",
"bam": "Bambara",
"ben": "Bengali",
"bod": "Tibetan",
"bre": "Breton",
"bos": "Bosnian",
"cat": "Catalan",
"che": "Chechen",
"cos": "Corsican",
"ces": "Czech",
"chv": "Chuvash",
"cym": "Welsh",
"dan": "Danish",
"deu": "German",
"dzo": "Dzongkha",
"ewe": "Ewe",
"eng": "English",
"spa": "Spanish",
"est": "Estonian",
"eus": "Basque",
"fas": "Persian",
"ful": "Fulah",
"fin": "Finnish",
"fao": "Faroese",
"fra": "French",
"gle": "Irish",
"glg": "Galician",
"grn": "Guarani",
"guj": "Gujarati",
"glv": "Manx",
"hau": "Hausa",
"heb": "Hebrew",
"hin": "Hindi",
"hrv": "Croatian",
"hun": "Hungarian",
"hye": "Armenian",
"ind": "Indonesian",
"ibo": "Igbo",
"isl": "Icelandic",
"ita": "Italian",
"iku": "Inuktitut",
"jpn": "Japanese",
"jav": "Javanese",
"kat": "Georgian",
"kik": "Kikuyu",
"kaz": "Kazakh",
"khm": "Khmer",
"kan": "Kannada",
"kor": "Korean",
"kas": "Kashmiri",
"kur": "Kurdish",
"cor": "Cornish",
"kir": "Kirghiz",
"ltz": "Luxembourgish",
"lug": "Ganda",
"lin": "Lingala",
"lao": "Lao",
"lit": "Lithuanian",
"lav": "Latvian",
"mlg": "Malagasy",
"mri": "Maori",
"mkd": "Macedonian",
"mal": "Malayalam",
"mon": "Mongolian",
"mar": "Marathi",
"mlt": "Maltese",
"mya": "Burmese",
"nld": "Dutch",
"nya": "Nyanja",
"orm": "Oromo",
"pol": "Polish",
"por": "Portuguese",
"que": "Quechua",
"roh": "Romansh",
"run": "Rundi",
"ron": "Romanian",
"rus": "Russian",
"kin": "Kinyarwanda",
"san": "Sanskrit",
"snd": "Sindhi",
"sag": "Sango",
"sin": "Sinhala",
"slk": "Slovak",
"slv": "Slovenian",
"sna": "Shona",
"som": "Somali",
"sqi": "Albanian",
"srp": "Serbian",
"ssw": "Swati",
"swe": "Swedish",
"tam": "Tamil",
"tel": "Telugu",
"tgk": "Tajik",
"tha": "Thai",
"tir": "Tigrinya",
"tuk": "Turkmen",
"fil": "Filipino",
"tsn": "Tswana",
"tur": "Turkish",
"tso": "Tsonga",
"tat": "Tatar",
"uig": "Uighur",
"ukr": "Ukrainian",
"urd": "Urdu",
"uzb": "Uzbek",
"ven": "Venda",
"vie": "Vietnamese",
"wln": "Walloon",
"wol": "Wolof",
"xho": "Xhosa",
"yor": "Yoruba",
"zho": "Chinese",
"zul": "Zulu"
}

View File

@ -49,7 +49,27 @@
font-family: sans-serif; font-family: sans-serif;
font-size: 13px; font-size: 13px;
background-color: #f1f1f1; background-color: #f1f1f1;
box-shadow: 2px 2px 5px 0px #ccc; box-shadow: 2px 2px 5px 0 #ccc;
}
#kiwixfooter {
text-align: center;
margin-top: 1em;
}
.kiwixHomeNavbar {
display: flex;
justify-content: center;
}
.kiwixFilter {
margin: 8px 10px;
}
.kiwixSearchForm {
margin: 8px 10px;
float: right;
}
@media (max-width: 1100px) {
.kiwixHomeBody {
padding: 0 125px;
}
} }
.book:hover { .book:hover {
background-color: #f9f9f9; background-color: #f9f9f9;
@ -70,7 +90,7 @@
line-height: 1em; line-height: 1em;
} }
.book__description { .book__description {
padding: 5px 55px 5px 0px; padding: 5px 55px 5px 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -83,14 +103,32 @@
font-size: 13px; font-size: 13px;
line-height: 1em; line-height: 1em;
} }
a:link {
text-decoration: none;
}
a:visited {
text-decoration: none;
}
</style> </style>
<script type="text/javascript" src="{{root}}/skin/index.js" async></script> <script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
</head> </head>
<body class="kiwix"> <body class="kiwix">
<div class="kiwix"> <div class='kiwixHomeNavbar'>
<select name="lang" id="languageFilter" class='kiwixFilter'>
<option value="" selected>All languages</option>
</select>
<select name="category" id="categoryFilter" class='kiwixFilter'>
<option value="" selected>ALl categories</option>
</select>
<form id='kiwixSearchForm' class='kiwixSearchForm'>
<input type="text" name="q" id="searchFilter" class='kiwixSearch'>
<input type="submit" value="Submit"/>
</form>
</div>
<div class="kiwixHomeBody">
<div class="book__list"></div> <div class="book__list"></div>
</div> </div>
<div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div> <div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
</body> </body>
</html> </html>