Replace removed Choo files and deps
I realized I should be doing Choo for frontend and Fastify for API
This commit is contained in:
parent
0f287748c1
commit
4923bf3e83
18
package.json
18
package.json
|
@ -7,20 +7,28 @@
|
|||
"author": "Robbie Antenesse <dev@alamantus.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"postinstall": "npm run process-images && npm run compile-sass",
|
||||
"start": "node server.js",
|
||||
"start-production": "cross-env NODE_ENV=production npm start",
|
||||
"compile-sass": "node process-styles.js",
|
||||
"process-images": "node process-images.js"
|
||||
"dev": "npm run watch-js",
|
||||
"start": "npm run build && npm run serve",
|
||||
"watch-js": "parcel watch src/index.html --no-hmr",
|
||||
"serve": "node server.js",
|
||||
"build": "parcel build src/index.html --no-source-maps",
|
||||
"clear": "npm run clear-dist && npm run clear-cache",
|
||||
"clear-dist": "rimraf dist/{*,.*}",
|
||||
"clear-cache": "rimraf .cache/{*,.*}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^9.6.1",
|
||||
"choo-devtools": "^3.0.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"parcel-plugin-goodie-bag": "^2.0.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.23.0-module.beta.1",
|
||||
"sharp": "^0.23.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"choo": "^7.0.0",
|
||||
"fastify": "^2.8.0",
|
||||
"fastify-caching": "^5.0.0",
|
||||
"fastify-compress": "^0.11.0",
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>book-tracker</title>
|
||||
<meta name="description" content="An attempt at a viable alternative to Goodreads">
|
||||
<meta name="keywords" content="books, tracking, lists, bookshelves, bookshelf, rating, reviews, reading">
|
||||
|
||||
<link rel="stylesheet" href="index.scss">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
|
||||
<!-- Choo replaces the body tag with the app. -->
|
||||
<body></body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,45 @@
|
|||
import choo from 'choo';
|
||||
|
||||
import { viewManager } from './views/manager';
|
||||
|
||||
const app = choo();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Only runs in development and will be stripped from production build.
|
||||
app.use(require('choo-devtools')()); // Exposes `choo` to the console for debugging!
|
||||
}
|
||||
|
||||
// App state and emitters
|
||||
app.use((state, emitter) => {
|
||||
// Default state variables
|
||||
state.currentView = 'home';
|
||||
state.viewStates = {};
|
||||
|
||||
// Listeners
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
// Emitter listeners
|
||||
emitter.on('render', callback => {
|
||||
// This is a dirty hack to get the callback to call *after* re-rendering.
|
||||
if (callback && typeof callback === "function") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('changeView', newView => {
|
||||
// Change the view and call render. Makes it easier to call within views.
|
||||
state.currentView = newView;
|
||||
emitter.emit('render', () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// For the main screen, pass the viewManager function in viewManager.js,
|
||||
// which is given the app's state from above and the emitter.emit method that
|
||||
// triggers the app's emitter listeners.
|
||||
app.route('/', viewManager);
|
||||
app.route('/:page', viewManager);
|
||||
app.route('/404', viewManager);
|
||||
|
||||
app.mount('body'); // Overwrite the `<body>` tag with the content of the Choo app
|
|
@ -0,0 +1,12 @@
|
|||
export class ViewController {
|
||||
constructor(state, viewName, defaultState = {}) {
|
||||
// Store the global app state so it's accessible but out of the way.
|
||||
this.appState = state;
|
||||
|
||||
// Give this view its own state within the appState.
|
||||
if (!this.appState.viewStates.hasOwnProperty(viewName)) {
|
||||
this.appState.viewStates[viewName] = defaultState;
|
||||
}
|
||||
this.state = this.appState.viewStates[viewName];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ViewController } from '../controller';
|
||||
|
||||
export class HomeController extends ViewController {
|
||||
constructor(state) {
|
||||
// Super passes state, view name, and default state to ViewController,
|
||||
// which stores state in this.appState and the view controller's state to this.state
|
||||
super(state, 'home', {
|
||||
messages: [
|
||||
'hello',
|
||||
'test',
|
||||
'yay',
|
||||
],
|
||||
});
|
||||
|
||||
// If using controller methods in an input's onchange or onclick instance,
|
||||
// either bind the class's 'this' instance to the method first...
|
||||
// or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead.
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return [...this.state.messages];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
import './styles.scss'; // Creates a separate CSS file, but allows better code splitting.
|
||||
// We'll see if code splitting is worth it in the end or if we should combine everything into `src/index.scss`
|
||||
import { HomeController } from './controller'; // The controller for this view, where processing should happen.
|
||||
|
||||
// This is the view function that is exported and used in the view manager.
|
||||
export const homeView = (state, emit) => {
|
||||
const controller = new HomeController(state);
|
||||
|
||||
// Returning an array in a view allows non-shared parent HTML elements.
|
||||
// This one doesn't have the problem right now, but it's good to remember.
|
||||
return [
|
||||
html`<section>
|
||||
<h2 class="subtitle">An attempt at a viable alternative to Goodreads</h2>
|
||||
|
||||
<article class="flex two">
|
||||
<div class="half">
|
||||
<div class="card">
|
||||
<header>
|
||||
<p>Still gotta figure out a design.</p>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
<div class="half">
|
||||
<div class="card">
|
||||
<header>
|
||||
<p>It's early days, my friends!</p>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="test">
|
||||
${controller.messages.map(message => {
|
||||
return html`<p>${message}</p>`;
|
||||
})}
|
||||
</article>
|
||||
</section>`,
|
||||
];
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.test {
|
||||
background-color: #dddddd;
|
||||
padding: 10px;
|
||||
|
||||
p {
|
||||
border: 1px solid #444444;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
import { homeView } from './home';
|
||||
import { searchView } from './search';
|
||||
|
||||
export const viewManager = (state, emit) => {
|
||||
// In viewManager all we are doing is checking the app's state
|
||||
// and passing the state and emit to the relevant view.
|
||||
let htmlContent = html`<div>loading</div>`;
|
||||
switch (state.params.page) {
|
||||
case 'home':
|
||||
default: {
|
||||
htmlContent = homeView(state, emit);
|
||||
break;
|
||||
}
|
||||
case 'search': {
|
||||
htmlContent = searchView(state, emit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a wrapper for view content that includes global header/footer
|
||||
let view = html`<body>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="brand">
|
||||
<a href="./">
|
||||
<h1>Unnamed Book Tracker</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- responsive-->
|
||||
<input id="navMenu" type="checkbox" class="show">
|
||||
<label for="navMenu" class="burger pseudo button">≡</label>
|
||||
|
||||
<div class="menu">
|
||||
<label style="display: inline-block;">
|
||||
<input type="text" name="search" placeholder="Search" onchange=${e => {
|
||||
console.log(encodeURIComponent(e.target.value.trim()));
|
||||
emit('pushState', '/search?for=' + encodeURIComponent(e.target.value.trim()));
|
||||
}}>
|
||||
</label>
|
||||
<a href="https://gitlab.com/Alamantus/book-tracker" class="pseudo button">Repo</a>
|
||||
<a href="https://gitter.im/book-tracker/general" class="pseudo button">Chat</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
${htmlContent}
|
||||
</main>
|
||||
</body>`;
|
||||
|
||||
return view;
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
import { ViewController } from '../controller';
|
||||
|
||||
export class SearchController extends ViewController {
|
||||
constructor(state) {
|
||||
// Super passes state, view name, and default state to ViewController,
|
||||
// which stores state in this.appState and the view controller's state to this.state
|
||||
super(state, 'search', {
|
||||
done: false,
|
||||
results: [],
|
||||
});
|
||||
|
||||
// If using controller methods in an input's onchange or onclick instance,
|
||||
// either bind the class's 'this' instance to the method first...
|
||||
// or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead.
|
||||
}
|
||||
|
||||
get results() {
|
||||
return [...this.state.results];
|
||||
}
|
||||
|
||||
get hasQuery() {
|
||||
return this.appState.query.hasOwnProperty('for') && this.appState.query.for.trim() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
search(term) {
|
||||
const query = this.mediaWikiQuery('https://en.wikibooks.org/w/api.php', {
|
||||
action: 'query',
|
||||
list: 'search',
|
||||
// list: 'categorymembers',
|
||||
// cmtitle: 'Category:Subject:Books by subject/all books',
|
||||
srsearch: term,
|
||||
srprop: '',
|
||||
// pageids: 20308,
|
||||
// prop: 'categories|pageprops',
|
||||
});
|
||||
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')) {
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
// this.state.results = results;
|
||||
this.state.done = true;
|
||||
});
|
||||
|
||||
// return fetch(`https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${term}&format=json&callback=searchCallback`, {
|
||||
// method: 'GET',
|
||||
// mode: 'no-cors',
|
||||
// headers: new Headers(
|
||||
// {
|
||||
// "Accept": "text/plain"
|
||||
// }
|
||||
// ),
|
||||
// // body: JSON.stringify({
|
||||
// // action: 'opensearch',
|
||||
// // search: term,
|
||||
// // format: 'json',
|
||||
// // }),
|
||||
// })
|
||||
// .then(res => res.text())
|
||||
// .then(response => {
|
||||
// console.log(response);
|
||||
// // if (response.hasOwnProperty('docs')) {
|
||||
// // // 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;
|
||||
// // });
|
||||
|
||||
// // this.state.results = results;
|
||||
// this.state.done = true;
|
||||
// // }
|
||||
// });
|
||||
}
|
||||
|
||||
searchOpenLibrary(term) {
|
||||
this.state.done = false;
|
||||
return fetch('http://openlibrary.org/search.json?q=' + encodeURIComponent(term))
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
if (response.hasOwnProperty('docs')) {
|
||||
// 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;
|
||||
});
|
||||
|
||||
this.state.results = results;
|
||||
this.state.done = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
// We'll see if code splitting is worth it in the end or if we should combine everything into `src/index.scss`
|
||||
import { SearchController } from './controller'; // The controller for this view, where processing should happen.
|
||||
|
||||
// This is the view function that is exported and used in the view manager.
|
||||
export const searchView = (state, emit) => {
|
||||
const controller = new SearchController(state);
|
||||
|
||||
if (!controller.state.done && controller.hasQuery) {
|
||||
controller.searchOpenLibrary(state.query.for).then(() => {
|
||||
emit('render');
|
||||
});
|
||||
}
|
||||
|
||||
// Returning an array in a view allows non-shared parent HTML elements.
|
||||
// This one doesn't have the problem right now, but it's good to remember.
|
||||
return [
|
||||
html`<section>
|
||||
<h2 class="subtitle">An attempt at a viable alternative to Goodreads</h2>
|
||||
|
||||
<article>
|
||||
${controller.results.map(result => {
|
||||
return html`<div class="card">
|
||||
<header>
|
||||
${result.covers.map(cover => {
|
||||
return html`<img src=${cover} />`;
|
||||
})}
|
||||
<h1 class="title">${result.title}</h1>
|
||||
${result.authors.map(author => {
|
||||
return html`<h2 class="subtitle">${author}</h2>`;
|
||||
})}
|
||||
</header>
|
||||
</div>`;
|
||||
})}
|
||||
</article>
|
||||
</section>`,
|
||||
];
|
||||
}
|
Loading…
Reference in New Issue