From f42f9ef98797caf233004de31e040e2e94e02e87 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Fri, 6 Sep 2019 17:20:27 -0600 Subject: [PATCH] Move search to handlebars format --- controllers/search.js | 148 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + routes/home.js | 9 --- routes/search.js | 13 ++++ server.js | 2 +- views/layout.hbs | 6 -- views/partials/header.hbs | 8 ++- views/search.hbs | 31 ++++++++ yarn.lock | 5 ++ 9 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 controllers/search.js create mode 100644 routes/search.js create mode 100644 views/search.hbs diff --git a/controllers/search.js b/controllers/search.js new file mode 100644 index 0000000..89808f0 --- /dev/null +++ b/controllers/search.js @@ -0,0 +1,148 @@ +const fetch = require('node-fetch'); + +class SearchController { + constructor(searchTerm) { + this.term = searchTerm; + } + + get hasQuery() { + return typeof this.term !== 'undefined' && this.term !== ''; + } + + /** + * Query a MediaWiki api.php instance with the given options + */ + mediaWikiQuery(endpoint, options) { + /** + * Create a uniquely-named callback that will process the JSONP results + */ + var createCallback = function (k) { + var i = 1; + var callbackName; + do { + callbackName = 'searchCallback' + i; + i = i + 1; + } while (window[callbackName]) + window[callbackName] = k; + return callbackName; + } + + /** + * Flatten an object into a URL query string. + * For example: { foo: 'bar', baz: 42 } becomes 'foo=bar&baz=42' + */ + var queryStr = function (options) { + var query = []; + for (var i in options) { + if (options.hasOwnProperty(i)) { + query.push(encodeURIComponent(i) + '=' + encodeURIComponent(options[i])); + } + } + return query.join('&'); + } + + /** + * Build a function that can be applied to a callback. The callback processes + * the JSON results of the API call. + */ + return function (k) { + options.format = 'json'; + options.callback = createCallback(k); + var script = document.createElement('script'); + script.id = 'searchResults'; + script.src = endpoint + '?' + queryStr(options); + var head = document.getElementsByTagName('head')[0]; + head.appendChild(script); + }; + + } + + async searchWikiBooks(term) { + if (!this.hasQuery) { + return []; + } + + const query = this.mediaWikiQuery('https://en.wikibooks.org/w/api.php', { + action: 'query', + list: 'search', + srsearch: this.term, + srprop: '', + }); + query(response => { + console.log(response); + const searchScript = document.getElementById('searchResults'); + searchScript.parentNode.removeChild(searchScript); + for (let property in window) { + if (property.includes('searchCallback')) { + delete window[property]; + } + } + + const bookResults = []; + const pageids = response.query.search.map(item => item.pageid); + const propsQuery = this.mediaWikiQuery('https://en.wikibooks.org/w/api.php', { + action: 'query', + pageids: pageids.join('|'), + prop: 'categories|pageprops', + }); + propsQuery(propsResponse => { + console.log(propsResponse); + for (let pageid in propsResponse.query.pages) { + if (propsResponse.query.pages[pageid].hasOwnProperty('categories')) { + + } + } + }); + return bookResults; + }); + } + + async searchOpenLibrary() { + if (!this.hasQuery) { + return []; + } + + return fetch('http://openlibrary.org/search.json?q=' + encodeURIComponent(this.term)) + .then(res => res.json()) + .then(response => { + if (!response.hasOwnProperty('docs')) { + return []; + } + // Format the response into usable objects + const docs = response.docs.map(doc => { + return { + title: doc.title_suggest.trim(), + authors: doc.hasOwnProperty('author_name') ? doc.author_name.map(name => name.trim()) : [], + cover: doc.hasOwnProperty('cover_i') ? `//covers.openlibrary.org/b/id/${doc.cover_i}-S.jpg` : false, + }; + }); + + // Filter out duplicate items with the same title and author + const results = docs.filter((doc, index, allDocs) => { + return typeof allDocs.find((filterResult, filterIndex) => { + return index !== filterIndex && filterResult.title === doc.title + && JSON.stringify(filterResult.authors) === JSON.stringify(doc.authors); + }) === 'undefined'; + }).map(result => { + // Find any duplicates in case they have different cover data + const duplicates = docs.filter(doc => { + return doc.title.toLowerCase() === result.title.toLowerCase() && JSON.stringify(doc.authors) === JSON.stringify(result.authors); + }); + result.covers = []; + duplicates.forEach(duplicate => { + if (duplicate.cover !== false) { + result.covers.push(duplicate.cover); + } + }); + return result; + }); + + return results; + }).catch(error => { + console.log(error); + return []; + }); + } +} + +module.exports = SearchController; \ No newline at end of file diff --git a/package.json b/package.json index 4509bce..7394c11 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "handlebars": "^4.2.0", "html-minifier": "^4.0.0", "make-promises-safe": "^5.0.0", + "node-fetch": "^2.6.0", "picnic": "^6.5.1", "point-of-view": "^3.5.0" } diff --git a/routes/home.js b/routes/home.js index b6f096c..70cd7a0 100644 --- a/routes/home.js +++ b/routes/home.js @@ -1,16 +1,7 @@ async function routes(fastify, options) { fastify.get('/', async (request, reply) => { - // return { hello: 'world' } reply.view('home.hbs', { text: 'test' }); }); - - // fastify.get('/search/:id', async function (request, reply) { - // const result = await collection.findOne({ id: request.params.id }) - // if (result.value === null) { - // throw new Error('Invalid value') - // } - // return result.value - // }) } module.exports = routes \ No newline at end of file diff --git a/routes/search.js b/routes/search.js new file mode 100644 index 0000000..3438a12 --- /dev/null +++ b/routes/search.js @@ -0,0 +1,13 @@ +const SearchController = require('../controllers/search'); + +async function routes(fastify, options) { + fastify.get('/search', async (request, reply) => { + const searchTerm = typeof request.query.for !== 'undefined' ? request.query.for.trim() : ''; + const search = new SearchController(searchTerm); + + const results = await search.searchOpenLibrary(); + reply.view('search.hbs', { results, searchTerm }); + }); +} + +module.exports = routes \ No newline at end of file diff --git a/server.js b/server.js index e5878b8..7688d73 100644 --- a/server.js +++ b/server.js @@ -24,7 +24,6 @@ fastify.register(require('point-of-view'), { removeCommentsFromCDATA: true, collapseWhitespace: true, collapseBooleanAttributes: true, - removeAttributeQuotes: true, removeEmptyAttributes: true }, partials: { @@ -38,6 +37,7 @@ fastify.register(require('point-of-view'), { // Routes fastify.register(require('./routes/resources')); fastify.register(require('./routes/home')); +fastify.register(require('./routes/search')); // Start the server fastify.listen(3000, function (err, address) { diff --git a/views/layout.hbs b/views/layout.hbs index 9fd667f..7b7c46c 100644 --- a/views/layout.hbs +++ b/views/layout.hbs @@ -10,7 +10,6 @@ {{!-- This is controlled by the resources router. --}} - @@ -29,11 +28,6 @@ {{/footer-block}} {{#> scripts-block }} - {{/scripts-block}} diff --git a/views/partials/header.hbs b/views/partials/header.hbs index 356184c..273421e 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -11,9 +11,11 @@ diff --git a/views/search.hbs b/views/search.hbs new file mode 100644 index 0000000..2b2603a --- /dev/null +++ b/views/search.hbs @@ -0,0 +1,31 @@ +{{#> layout }} + +{{#*inline "page-content-block"}} +
+

An attempt at a viable alternative to Goodreads

+ +
+ + {{#each results }} +
+
+ + {{#each covers }} + + {{/each}} + +

{{title}}

+ + {{#each authors }} +

{{this}}

+ {{/each}} + +
+
+ {{/each}} + +
+
+{{/inline}} + +{{/layout}} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 477e615..d24f671 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1335,6 +1335,11 @@ nocache@^2.0.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== +node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + node-releases@^1.1.29: version "1.1.29" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.29.tgz#86a57c6587a30ecd6726449e5d293466b0a0bb86"