Compare commits
No commits in common. "596721fb2d5ce6c97fe39680039303ff89cefebf" and "8047c83edeb2f949d7c8d4614161f03d5ccaa01e" have entirely different histories.
596721fb2d
...
8047c83ede
30
app/index.js
30
app/index.js
|
@ -31,42 +31,22 @@ app.use((state, emitter) => {
|
||||||
savedSettings = {};
|
savedSettings = {};
|
||||||
}
|
}
|
||||||
savedSettings[settingsKey] = value;
|
savedSettings[settingsKey] = value;
|
||||||
return window.localStorage.setItem('settings', JSON.stringify(savedSettings));
|
return window.localStorage.setItem(JSON.stringify(savedSettings));
|
||||||
}
|
|
||||||
app.getSessionState = () => {
|
|
||||||
let sessionState = window.sessionStorage.getItem('sessionState');
|
|
||||||
if (sessionState) {
|
|
||||||
return JSON.parse(sessionState);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
app.setSessionState = () => {
|
|
||||||
return window.sessionStorage.setItem('sessionState', JSON.stringify(app.state));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// App state and emitters
|
// App state and emitters
|
||||||
app.use((state, emitter) => {
|
app.use((state, emitter) => {
|
||||||
const sessionState = app.getSessionState();
|
// Default state variables
|
||||||
if (sessionState) {
|
state.currentView = 'home';
|
||||||
Object.keys(sessionState).forEach(key => {
|
state.language = app.getSettingsItem('lang') ? app.getSettingsItem('lang') : (navigator.language || navigator.userLanguage).split('-')[0];
|
||||||
if (typeof state[key] === 'undefined') {
|
state.viewStates = {};
|
||||||
state[key] = sessionState[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Default state variables
|
|
||||||
state.currentView = 'home';
|
|
||||||
state.language = app.getSettingsItem('lang') ? app.getSettingsItem('lang') : (navigator.language || navigator.userLanguage).split('-')[0];
|
|
||||||
state.viewStates = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
document.title = config.siteName;
|
document.title = config.siteName;
|
||||||
// Emitter listeners
|
// Emitter listeners
|
||||||
emitter.on('render', callback => {
|
emitter.on('render', callback => {
|
||||||
app.setSessionState();
|
|
||||||
// This is a dirty hack to get the callback to call *after* re-rendering.
|
// This is a dirty hack to get the callback to call *after* re-rendering.
|
||||||
if (callback && typeof callback === "function") {
|
if (callback && typeof callback === "function") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -35,11 +35,10 @@
|
||||||
@import '../../node_modules/picnic/src/plugins/modal/plugin';
|
@import '../../node_modules/picnic/src/plugins/modal/plugin';
|
||||||
|
|
||||||
// @import '../../node_modules/picnic/src/plugins/dropimage/plugin';
|
// @import '../../node_modules/picnic/src/plugins/dropimage/plugin';
|
||||||
@import '../../node_modules/picnic/src/plugins/tabs/plugin';
|
// @import '../../node_modules/picnic/src/plugins/tabs/plugin';
|
||||||
@import '../../node_modules/picnic/src/plugins/tooltip/plugin';
|
@import '../../node_modules/picnic/src/plugins/tooltip/plugin';
|
||||||
|
|
||||||
// Custom global styling
|
// Custom global styling
|
||||||
@import './picnic-customizations/tabs';
|
|
||||||
@import './picnic-customizations/custom';
|
@import './picnic-customizations/custom';
|
||||||
|
|
||||||
// View styling
|
// View styling
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
$tabs: five, six, seven, eight, nine, ten, eleven, twelve, thirteen, fourteen, fifteen, sixteen, seventeen, eighteen, nineteen, twenty;
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
&.one {
|
|
||||||
> .row {
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> input:nth-of-type(1):checked ~ .row {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> label img {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is probably really stupid, but whatever. I need more than the 4 built-in tabs that Picnic provides
|
|
||||||
@for $tab-index from 1 through length($tabs) {
|
|
||||||
$number: $tab-index + 4;
|
|
||||||
$tab: nth($tabs, $tab-index);
|
|
||||||
|
|
||||||
&.#{$tab} {
|
|
||||||
> .row {
|
|
||||||
width: 100% * $number;
|
|
||||||
left: -100% * ($number - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $item from 1 through ($number - 1) {
|
|
||||||
> input:nth-of-type(#{$item}):checked ~ .row {
|
|
||||||
margin-left: (100% * ($number - 1)) - (100% * ($item - 1));// 400%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> label img {
|
|
||||||
width: floor(100% / $number) - 2%;
|
|
||||||
margin: 4% 0 4% 4%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,8 +10,6 @@ export const modal = (modalId, controller, contentHTML, options = {}) => {
|
||||||
* headerText <string>: Displayed in an `<h3>` if no header is specified
|
* headerText <string>: Displayed in an `<h3>` if no header is specified
|
||||||
* noFooter <bool>: Set to `true` and exclude footerHTML to not include a modal footer
|
* noFooter <bool>: Set to `true` and exclude footerHTML to not include a modal footer
|
||||||
* footerHTML <choo/html>: Displayed in place of the default footer; Recommended to use `<footer>` tag
|
* footerHTML <choo/html>: Displayed in place of the default footer; Recommended to use `<footer>` tag
|
||||||
* onShow <function>: Runs when the modal opens.
|
|
||||||
* onHide <function>: Runs when the modal closes.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isOpen = controller.openModal === modalId;
|
const isOpen = controller.openModal === modalId;
|
||||||
|
@ -25,17 +23,9 @@ export const modal = (modalId, controller, contentHTML, options = {}) => {
|
||||||
|
|
||||||
// Modals in Picnic CSS uses pure CSS with clever usage of invisible checkboxes and labels
|
// Modals in Picnic CSS uses pure CSS with clever usage of invisible checkboxes and labels
|
||||||
html`<div class="modal">
|
html`<div class="modal">
|
||||||
<input id=${modalId} type="checkbox" ${isOpen ? 'checked' : null} onchange=${event => {
|
<input id=${modalId} type="checkbox" ${!isOpen ? null : 'checked'}
|
||||||
controller.openModal = !isOpen ? modalId : null; // If it's not already open, set it to the open one
|
onchange=${() => controller.openModal = isOpen ? modalId : null }/>
|
||||||
if (typeof options.onShow !== 'undefined' && event.target.checked) {
|
|
||||||
options.onShow();
|
|
||||||
}
|
|
||||||
if (typeof options.onHide !== 'undefined' && !event.target.checked) {
|
|
||||||
options.onHide();
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<label for=${modalId} class="overlay"></label>
|
<label for=${modalId} class="overlay"></label>
|
||||||
|
|
||||||
<article style=${typeof options.styles !== 'undefined' ? options.styles : null}>
|
<article style=${typeof options.styles !== 'undefined' ? options.styles : null}>
|
||||||
|
|
||||||
${typeof options.headerHTML === 'undefined'
|
${typeof options.headerHTML === 'undefined'
|
||||||
|
|
|
@ -56,21 +56,4 @@ export class SearchController extends ViewController {
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCovers(inventaireURI) {
|
|
||||||
// This should only be callable after results are displayed.
|
|
||||||
const workIndex = this.results.works.findIndex(work => work.uri === inventaireURI);
|
|
||||||
if (workIndex > -1) { // This should never be false, but just in case...
|
|
||||||
if (typeof this.state.results.works[workIndex].covers === 'undefined') {
|
|
||||||
// Only fetch covers if not already fetched.
|
|
||||||
return fetch(`/api/books/covers?uri=${inventaireURI}&lang=${this.appState.language}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(responseJSON => {
|
|
||||||
this.state.results.works[workIndex].covers = responseJSON;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -35,7 +35,7 @@ export const searchView = (state, emit) => {
|
||||||
${result.description ? html`<h4 class="subtitle">${result.description}</h4>` : null}
|
${result.description ? html`<h4 class="subtitle">${result.description}</h4>` : null}
|
||||||
</div>
|
</div>
|
||||||
<div class="third-800 half-500">
|
<div class="third-800 half-500">
|
||||||
${resultDetails(controller, result, emit)}
|
${resultDetails(controller, result)}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -3,59 +3,28 @@ import html from 'choo/html';
|
||||||
import { starRating } from '../partials/starRating';
|
import { starRating } from '../partials/starRating';
|
||||||
import { modal } from '../partials/modal';
|
import { modal } from '../partials/modal';
|
||||||
|
|
||||||
export const resultDetails = (searchController, result, emit = () => {}) => {
|
export const resultDetails = (searchController, result) => {
|
||||||
const { i18n } = searchController;
|
const { i18n } = searchController;
|
||||||
const modalId = `result_${result.uri}`;
|
const modalId = `result_${result.uri}`;
|
||||||
|
|
||||||
const buttonHTML = html`<label for=${modalId} class="pseudo button">
|
const buttonHTML = html`<label for=${modalId} class="pseudo button">
|
||||||
<span data-tooltip="${i18n.__('interaction.average_rating')}: ${result.averageRating}">
|
<span data-tooltip="${i18n.__('interaction.average_rating')}: ${result.rating}">
|
||||||
${starRating(result.averageRating)}
|
${starRating(result.rating)}
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left:10px;" data-tooltip=${i18n.__('interaction.reviews_written')}>
|
<span style="margin-left:10px;" data-tooltip=${i18n.__('interaction.reviews_written')}>
|
||||||
<span style="margin-right:8px;"><i class="icon-chat"></i></span>
|
<span style="margin-right:8px;"><i class="icon-chat"></i></span>
|
||||||
<span>${result.numberOfReviews}</span>
|
<span>${result.reviewCount}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>`;
|
</label>`;
|
||||||
|
|
||||||
const tabNames = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty'];
|
|
||||||
|
|
||||||
const modalContent = html`<article class="flex">
|
const modalContent = html`<article class="flex">
|
||||||
<div class="sixth-700" style="text-align:center;">
|
<div class="sixth-700" style="text-align:center;">
|
||||||
<h4>Covers</h4>
|
<h4>Covers</h4>
|
||||||
${typeof result.covers === 'undefined'
|
<span style="font-size:3em;"><i class="icon-loading animate-spin"></i></span>
|
||||||
? html`<span style="font-size:3em;"><i class="icon-loading animate-spin"></i></span>`
|
|
||||||
: html`<div class="tabs ${typeof tabNames[result.covers.length - 1] !== 'undefined' ? tabNames[result.covers.length - 1] : null}">
|
|
||||||
${result.covers.map((cover, index) => {
|
|
||||||
return [
|
|
||||||
html`<input id="cover_${cover.uri}" type="radio" name="${modalId}_covers" ${index === 0 ? 'checked' : null} />`,
|
|
||||||
// html`<label class="small pseudo button toggle" for="cover_${cover.uri}">•</label>`,
|
|
||||||
];
|
|
||||||
})}
|
|
||||||
<div class="row">
|
|
||||||
${result.covers.map((cover, index, allCovers) => {
|
|
||||||
return html`<div>
|
|
||||||
<img src=${cover.url} alt="${cover.uri.replace(':', ' ').toUpperCase()}, Published: ${cover.publishDate}">
|
|
||||||
${typeof allCovers[index - 1] === 'undefined'
|
|
||||||
? null
|
|
||||||
: html`<label class="button" for="cover_${allCovers[index - 1].uri}" style="margin-right:8px;">
|
|
||||||
${'<'}
|
|
||||||
</label>`
|
|
||||||
}
|
|
||||||
${typeof allCovers[index + 1] === 'undefined'
|
|
||||||
? null
|
|
||||||
: html`<label class="button" for="cover_${allCovers[index + 1].uri}">
|
|
||||||
${'>'}
|
|
||||||
</label>`
|
|
||||||
}
|
|
||||||
</div>`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="two-third-700">
|
<div class="two-third-700">
|
||||||
<h4>${i18n.__('interaction.average_rating')}</h4>
|
<h4>${i18n.__('interaction.average_rating')}</h4>
|
||||||
<span data-tooltip="${result.averageRating}">${starRating(result.averageRating)}</span>
|
<span data-tooltip="${result.rating}">${starRating(result.rating)}</span>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div>
|
<div>
|
||||||
|
@ -64,34 +33,42 @@ export const resultDetails = (searchController, result, emit = () => {}) => {
|
||||||
<div>
|
<div>
|
||||||
<a href="/book/${result.uri}" class="small button">
|
<a href="/book/${result.uri}" class="small button">
|
||||||
<span style="margin-right:8px;"><i class="icon-chat"></i></span>
|
<span style="margin-right:8px;"><i class="icon-chat"></i></span>
|
||||||
<span>${result.numberOfReviews}</span>
|
<span>${result.reviewCount}</span>
|
||||||
<span>${i18n.__('search.see_interaction_details')}</span>
|
<span>${i18n.__('search.see_interaction_details')}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${result.reviews.map(review => {
|
<article class="card">
|
||||||
return html`<article class="card">
|
<header>
|
||||||
<header style="font-weight:normal;">
|
{{USERNAME}} ${starRating(Math.ceil(result.rating))}
|
||||||
<strong>${review.reviewer.name}</strong> <em>${review.reviewer.handle}</em><br>
|
</header>
|
||||||
${review.date} ${starRating(Math.ceil(review.rating))}
|
<footer>
|
||||||
</header>
|
<div class="content">
|
||||||
<footer>
|
<p>
|
||||||
<div class="content">
|
The only thing worse than yellow snow is green snow. Let's put a touch more of the magic here.
|
||||||
<p>
|
With practice comes confidence. You have to allow the paint to break to make it beautiful.
|
||||||
${review.review}
|
</p>
|
||||||
</p>
|
<p>
|
||||||
</div>
|
Let all these little things happen. Don't fight them. Learn to use them. Imagination is the key
|
||||||
<span class="tooltip-top" data-tooltip=${i18n.__('interaction.heart')}>
|
to painting. You can't make a mistake. Anything that happens you can learn to use - and make
|
||||||
<button class="pseudo">
|
something beautiful out of it. This is a fantastic little painting. In painting, you have unlimited
|
||||||
<i class="icon-heart-outline"></i>
|
power. You have the ability to move mountains. We don't have anything but happy trees here.
|
||||||
</button>
|
</p>
|
||||||
</span>
|
<p>
|
||||||
<span>
|
If what you're doing doesn't make you happy - you're doing the wrong thing. A fan brush can be
|
||||||
${review.hearts}
|
your best friend. Mountains are so simple, they're hard.
|
||||||
</span>
|
</p>
|
||||||
</footer>
|
</div>
|
||||||
</article>`;
|
<span class="tooltip-top" data-tooltip=${i18n.__('interaction.heart')}>
|
||||||
})}
|
<button class="pseudo">
|
||||||
|
<i class="icon-heart-outline"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
${result.reviewCount}
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div class="sixth-700">
|
<div class="sixth-700">
|
||||||
<p>
|
<p>
|
||||||
|
@ -115,10 +92,5 @@ export const resultDetails = (searchController, result, emit = () => {}) => {
|
||||||
styles: "width:90%;",
|
styles: "width:90%;",
|
||||||
buttonHTML, // This should be replaced with buttonHTML containing the ratings and number of reviews etc.
|
buttonHTML, // This should be replaced with buttonHTML containing the ratings and number of reviews etc.
|
||||||
headerText: result.name,
|
headerText: result.name,
|
||||||
onShow: () => {
|
|
||||||
if (typeof result.covers === 'undefined') {
|
|
||||||
searchController.getCovers(result.uri).then(() => emit('render'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -21,7 +21,6 @@
|
||||||
"concurrently": "^4.1.2",
|
"concurrently": "^4.1.2",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"faker": "^4.1.0",
|
|
||||||
"parcel-bundler": "^1.12.3",
|
"parcel-bundler": "^1.12.3",
|
||||||
"parcel-plugin-goodie-bag": "^2.0.0",
|
"parcel-plugin-goodie-bag": "^2.0.0",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
class BooksController {
|
|
||||||
constructor(inventaireDomain, bookURI, language) {
|
|
||||||
this.inventaire = inventaireDomain;
|
|
||||||
this.uri = bookURI;
|
|
||||||
this.lang = language;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBookData() {
|
|
||||||
const bookData = this.getBookDataFromInventaire();
|
|
||||||
const communityData = this.getCommunityData();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...bookData,
|
|
||||||
...communityData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommunityData(maxReviews) {
|
|
||||||
if (process.NODE_ENV !== 'production') {
|
|
||||||
return this.getFakeData(maxReviews);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getFakeData(maxReviews) {
|
|
||||||
const faker = require('faker');
|
|
||||||
const numberOfReviews = Math.floor(Math.random() * 100);
|
|
||||||
const reviews = [];
|
|
||||||
for (let i = 0; i < numberOfReviews; i++) {
|
|
||||||
const reviewerName = Math.random() < 0.5
|
|
||||||
? faker.fake('{{name.firstName}} {{name.lastName}}')
|
|
||||||
: faker.fake('{{hacker.adjective}}{{hacker.noun}}');
|
|
||||||
reviews.push({
|
|
||||||
reviewer: {
|
|
||||||
name: reviewerName,
|
|
||||||
handle: faker.fake('@{{internet.userName}}@{{internet.domainName}}'),
|
|
||||||
},
|
|
||||||
date: faker.date.past(),
|
|
||||||
rating: parseFloat((Math.random() * 5).toFixed(1)),
|
|
||||||
review: faker.lorem.paragraph(),
|
|
||||||
hearts: Math.floor(Math.random() * 1000),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const averageRating = parseFloat((reviews.reduce((total, review) => {
|
|
||||||
return total + review.rating;
|
|
||||||
}, 0) / numberOfReviews).toFixed(1));
|
|
||||||
|
|
||||||
reviews.sort((a, b) => {
|
|
||||||
if (a.hearts === b.hearts) return 0;
|
|
||||||
return a.hearts > b.hearts ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
averageRating,
|
|
||||||
numberOfReviews,
|
|
||||||
reviews: typeof maxReviews !== 'undefined' ? reviews.slice(0, maxReviews - 1) : reviews,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInventaireEntity(entityObject) {
|
|
||||||
const hasLabels = typeof entityObject.labels !== 'undefined';
|
|
||||||
const hasDescriptions = typeof entityObject.descriptions !== 'undefined';
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: (
|
|
||||||
hasLabels && typeof entityObject.labels[this.lang] !== 'undefined'
|
|
||||||
? entityObject.labels[this.lang]
|
|
||||||
: (
|
|
||||||
hasLabels && Object.keys(entityObject.labels).length > 0
|
|
||||||
? entityObject.labels[Object.keys(entityObject.labels)[0]]
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
),
|
|
||||||
description: (
|
|
||||||
hasDescriptions && typeof entityObject.descriptions[this.lang] !== 'undefined'
|
|
||||||
? entityObject.descriptions[this.lang]
|
|
||||||
: (
|
|
||||||
hasDescriptions && Object.keys(entityObject.descriptions).length > 0
|
|
||||||
? entityObject.descriptions[Object.keys(entityObject.descriptions)[0]]
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
),
|
|
||||||
link: (
|
|
||||||
typeof entityObject.uri !== 'undefined'
|
|
||||||
? `${this.inventaire}/entity/${entityObject.uri}`
|
|
||||||
: null
|
|
||||||
),
|
|
||||||
uri: (
|
|
||||||
typeof entityObject.uri !== 'undefined'
|
|
||||||
? entityObject.uri
|
|
||||||
: null
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBookDataFromInventaire() {
|
|
||||||
if (this.uri) {
|
|
||||||
const request = fetch(`${this.inventaire}/api/entities?action=by-uris&uris=${encodeURIComponent(this.uri)}`)
|
|
||||||
request.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: 'An error occurred when trying to reach the Inventaire API.',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const json = request.then(response => response.json());
|
|
||||||
json.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookData = await json;
|
|
||||||
|
|
||||||
if (typeof bookData.entities !== 'undefined' && typeof bookData.entities[this.uri] !== 'undefined') {
|
|
||||||
const bookData = this.handleInventaireEntity(bookData.entities[this.uri], this.lang);
|
|
||||||
bookData['covers'] = await this.getInventaireCovers();
|
|
||||||
|
|
||||||
return bookData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'No URI provided',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInventaireCovers() {
|
|
||||||
if (!this.uri) {
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: property `wdt:P629` is a given entity (uri)'s list of editions (ISBNs).
|
|
||||||
const editionsRequest = fetch(`${this.inventaire}/api/entities?action=reverse-claims&uri=${encodeURIComponent(this.uri)}&property=wdt:P629`)
|
|
||||||
editionsRequest.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: `An error occurred when trying to reach the Inventaire API for URI ${this.uri}.`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const editionsJson = editionsRequest.then(response => response.json());
|
|
||||||
editionsJson.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const editions = await editionsJson;
|
|
||||||
const editionURIs = typeof editions.uris !== 'undefined' ? editions.uris.join('|') : false;
|
|
||||||
|
|
||||||
if (editionURIs === false) {
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isbnsRequest = fetch(`${this.inventaire}/api/entities?action=by-uris&uris=${encodeURIComponent(editionURIs)}`);
|
|
||||||
isbnsRequest.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: `An error occurred when trying to reach the Inventaire API for URI ${this.uri}.`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isbnsJson = isbnsRequest.then(response => response.json());
|
|
||||||
isbnsJson.catch(exception => {
|
|
||||||
console.error(exception);
|
|
||||||
return {
|
|
||||||
error: exception,
|
|
||||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return isbnsJson.then(responseJSON => {
|
|
||||||
if (typeof responseJSON.entities === 'undefined') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const covers = Object.keys(responseJSON.entities).filter(key => {
|
|
||||||
const entity = responseJSON.entities[key];
|
|
||||||
return entity.originalLang === this.lang && typeof entity.claims !== undefined && typeof entity.claims['invp:P2'] !== undefined;
|
|
||||||
}).map(key => {
|
|
||||||
const entity = responseJSON.entities[key];
|
|
||||||
return {
|
|
||||||
uri: entity.uri,
|
|
||||||
url: typeof entity.claims['invp:P2'] !== 'undefined' ? `${this.inventaire}/img/entities/${entity.claims['invp:P2'][0]}` : null,
|
|
||||||
publishDate: typeof entity.claims['wdt:P577'] !== 'undefined' ? entity.claims['wdt:P577'][0] : null,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
covers.sort((a, b) => {
|
|
||||||
if (a.publishDate === b.publishDate) return 0;
|
|
||||||
if (!a.publishDate && !!b.publishDate) return 1;
|
|
||||||
if (!!a.publishDate && !b.publishDate) return 1;
|
|
||||||
return a.publishDate < b.publishDate ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return covers;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = BooksController;
|
|
|
@ -1,7 +1,5 @@
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
const BooksController = require('./books');
|
|
||||||
|
|
||||||
class SearchController {
|
class SearchController {
|
||||||
constructor(inventaireDomain, searchTerm, language = 'en') {
|
constructor(inventaireDomain, searchTerm, language = 'en') {
|
||||||
this.inventaire = inventaireDomain;
|
this.inventaire = inventaireDomain;
|
||||||
|
@ -99,14 +97,41 @@ class SearchController {
|
||||||
});
|
});
|
||||||
|
|
||||||
const works = responseJSON.works.map(work => {
|
const works = responseJSON.works.map(work => {
|
||||||
const booksController = new BooksController(this.inventaire, work.uri, this.lang);
|
const hasLabels = typeof work.labels !== 'undefined';
|
||||||
const bookData = booksController.handleInventaireEntity(work);
|
const hasDescriptions = typeof work.descriptions !== 'undefined';
|
||||||
const communityData = booksController.getCommunityData(5);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...bookData,
|
name: (
|
||||||
...communityData,
|
hasLabels && typeof work.labels[this.lang] !== 'undefined'
|
||||||
}
|
? work.labels[this.lang]
|
||||||
|
: (
|
||||||
|
hasLabels && Object.keys(work.labels).length > 0
|
||||||
|
? work.labels[Object.keys(work.labels)[0]]
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
description: (
|
||||||
|
hasDescriptions && typeof work.descriptions[this.lang] !== 'undefined'
|
||||||
|
? work.descriptions[this.lang]
|
||||||
|
: (
|
||||||
|
hasDescriptions && Object.keys(work.descriptions).length > 0
|
||||||
|
? work.descriptions[Object.keys(work.descriptions)[0]]
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
link: (
|
||||||
|
typeof work.uri !== 'undefined'
|
||||||
|
? `${this.inventaire}/entity/${work.uri}`
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
uri: (
|
||||||
|
typeof work.uri !== 'undefined'
|
||||||
|
? work.uri
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
rating: (Math.random() * 5).toFixed(1),
|
||||||
|
reviewCount: Math.floor(Math.random() * 100),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -118,6 +143,73 @@ class SearchController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getInventaireCovers(inventaireURI) {
|
||||||
|
if (!inventaireURI) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: property `wdt:P629` is a given entity (uri)'s list of editions (ISBNs).
|
||||||
|
const editionsRequest = fetch(`${this.inventaire}/api/entities?action=reverse-claims&uri=${encodeURIComponent(inventaireURI)}&property=wdt:P629`)
|
||||||
|
editionsRequest.catch(exception => {
|
||||||
|
console.error(exception);
|
||||||
|
return {
|
||||||
|
error: exception,
|
||||||
|
message: `An error occurred when trying to reach the Inventaire API for URI ${inventaireURI}.`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const editionsJson = editionsRequest.then(response => response.json());
|
||||||
|
editionsJson.catch(exception => {
|
||||||
|
console.error(exception);
|
||||||
|
return {
|
||||||
|
error: exception,
|
||||||
|
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const editions = await editionsJson;
|
||||||
|
const editionURIs = typeof editions.uris !== 'undefined' ? editions.uris.join('|') : false;
|
||||||
|
|
||||||
|
if (editionURIs === false) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isbnsRequest = fetch(`${this.inventaire}/api/entities?action=by-uris&uris=${encodeURIComponent(editionURIs)}`);
|
||||||
|
isbnsRequest.catch(exception => {
|
||||||
|
console.error(exception);
|
||||||
|
return {
|
||||||
|
error: exception,
|
||||||
|
message: `An error occurred when trying to reach the Inventaire API for URI ${inventaireURI}.`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isbnsJson = isbnsRequest.then(response => response.json());
|
||||||
|
isbnsJson.catch(exception => {
|
||||||
|
console.error(exception);
|
||||||
|
return {
|
||||||
|
error: exception,
|
||||||
|
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isbnsJson.then(responseJSON => {
|
||||||
|
if (typeof responseJSON.entities === 'undefined') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(responseJSON.entities).filter(key => {
|
||||||
|
const entity = responseJSON.entities[key];
|
||||||
|
return entity.originalLang === this.lang && typeof entity.claims !== undefined && typeof entity.claims['invp:P2'] !== undefined ;
|
||||||
|
}).map(key => {
|
||||||
|
const entity = responseJSON.entities[key];
|
||||||
|
return {
|
||||||
|
uri: entity.uri,
|
||||||
|
url: `${this.inventaire}/img/entities/${entity.claims['invp:P2'][0]}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query a MediaWiki api.php instance with the given options
|
* Query a MediaWiki api.php instance with the given options
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -34,7 +34,7 @@ fastify.addHook('onRequest', (request, reply, done) => {
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
fastify.register(require('./routes/public'));
|
fastify.register(require('./routes/public'));
|
||||||
fastify.register(require('./routes/books'));
|
// fastify.register(require('./routes/home'));
|
||||||
// fastify.register(require('./routes/account'));
|
// fastify.register(require('./routes/account'));
|
||||||
fastify.register(require('./routes/search'));
|
fastify.register(require('./routes/search'));
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
const BooksController = require('../controllers/books');
|
|
||||||
|
|
||||||
async function routes(fastify, options) {
|
|
||||||
fastify.get('/api/books', async (request, reply) => {
|
|
||||||
const bookURI = typeof request.query.uri !== 'undefined' ? request.query.uri.trim() : '';
|
|
||||||
const language = typeof request.query.lang !== 'undefined' ? request.query.lang.trim().split('-')[0] : undefined; // Get base language in cases like 'en-US'
|
|
||||||
const books = new BooksController(fastify.siteConfig.inventaireDomain, bookURI, language);
|
|
||||||
|
|
||||||
return books.getBookData();
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/api/books/covers', async (request, reply) => {
|
|
||||||
const bookURI = typeof request.query.uri !== 'undefined' ? request.query.uri.trim() : '';
|
|
||||||
const language = typeof request.query.lang !== 'undefined' ? request.query.lang.trim().split('-')[0] : undefined; // Get base language in cases like 'en-US'
|
|
||||||
const books = new BooksController(fastify.siteConfig.inventaireDomain, bookURI, language);
|
|
||||||
|
|
||||||
return await books.getInventaireCovers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = routes
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
async function routes(fastify, options) {
|
||||||
|
fastify.get('/', async (request, reply) => {
|
||||||
|
const viewData = {};
|
||||||
|
if (typeof request.query.loggedOut !== 'undefined') {
|
||||||
|
viewData.message = 'You have been logged out';
|
||||||
|
} else {
|
||||||
|
viewData.message = request.isLoggedInUser ? JSON.stringify(fastify.jwt.decode(request.cookies.token)) : 'you are NOT logged in';
|
||||||
|
}
|
||||||
|
if (request.isLoggedInUser) {
|
||||||
|
viewData.loggedIn = true;
|
||||||
|
viewData.statuses = [{ title: 'books' }, { title: 'fun' }];
|
||||||
|
}
|
||||||
|
reply.view('home.hbs', viewData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routes
|
|
@ -2605,11 +2605,6 @@ extsprintf@^1.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||||
|
|
||||||
faker@^4.1.0:
|
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
|
|
||||||
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=
|
|
||||||
|
|
||||||
falafel@^2.1.0:
|
falafel@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c"
|
resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c"
|
||||||
|
|
Loading…
Reference in New Issue