Compare commits
19 Commits
49ab635660
...
3f9ca67918
Author | SHA1 | Date |
---|---|---|
Robbie Antenesse | 3f9ca67918 | |
Robbie Antenesse | bf33ff8e3e | |
Robbie Antenesse | c5e73242d0 | |
Robbie Antenesse | ebc5c9b06d | |
Robbie Antenesse | 1062b9b2fd | |
Robbie Antenesse | 40084c4072 | |
Robbie Antenesse | e39fe52e65 | |
Robbie Antenesse | abc1e10b3f | |
Robbie Antenesse | 691f0c350b | |
Robbie Antenesse | 69563eb74e | |
Robbie Antenesse | 9823ae8290 | |
Robbie Antenesse | fe7305164b | |
Robbie Antenesse | 63a25283af | |
Robbie Antenesse | c22f632b53 | |
Robbie Antenesse | eab60fe159 | |
Robbie Antenesse | 8e9f74a901 | |
Robbie Antenesse | dd131550dc | |
Robbie Antenesse | 192a78188c | |
Robbie Antenesse | b093595f1d |
|
@ -0,0 +1,22 @@
|
|||
export const appListeners = (app, state, emitter) => {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.title = app.siteConfig.siteName;
|
||||
// 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('set-language', newLanguage => {
|
||||
app.setSettingsItem('lang', newLanguage);
|
||||
state.language = newLanguage;
|
||||
emitter.emit('render', () => { });
|
||||
});
|
||||
|
||||
emitter.emit('render'); // This should hopefully only run once after the DOM is loaded. It prevents routing issues where 'render' hasn't been defined yet
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { globalView } from './views/global';
|
||||
import { homeView } from './views/home';
|
||||
import { loginView } from './views/login';
|
||||
import { searchView } from './views/search';
|
||||
import { errorView } from './views/404';
|
||||
|
||||
export const appRoutes = (app) => {
|
||||
app.route('/', (state, emit) => globalView(state, emit, homeView));
|
||||
|
||||
app.route('/login', (state, emit) => globalView(state, emit, loginView));
|
||||
|
||||
app.route('/search', (state, emit) => globalView(state, emit, searchView));
|
||||
|
||||
app.route('/404', (state, emit) => globalView(state, emit, errorView));
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { I18n } from "./i18n";
|
||||
|
||||
export const appState = (app, state, emitter) => {
|
||||
state.language = app.getSettingsItem('lang') ? app.getSettingsItem('lang') : (navigator.language || navigator.userLanguage).split('-')[0];
|
||||
state.viewStates = {};
|
||||
state.isLoggedIn = false;
|
||||
state.i18n = new I18n(state); // Global I18n class passed to all views
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
export const appUtilities = (app) => {
|
||||
app.getSettingsItem = settingsKey => {
|
||||
let savedSettings = window.localStorage.getItem('settings');
|
||||
if (savedSettings) {
|
||||
savedSettings = JSON.parse(savedSettings);
|
||||
if (typeof savedSettings[settingsKey] !== 'undefined') {
|
||||
return savedSettings[settingsKey];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
app.setSettingsItem = (settingsKey, value) => {
|
||||
let savedSettings = window.localStorage.getItem('settings');
|
||||
if (savedSettings) {
|
||||
savedSettings = JSON.parse(savedSettings);
|
||||
} else {
|
||||
savedSettings = {};
|
||||
}
|
||||
savedSettings[settingsKey] = value;
|
||||
return window.localStorage.setItem('settings', JSON.stringify(savedSettings));
|
||||
}
|
||||
}
|
|
@ -19,6 +19,15 @@ Font license info
|
|||
Homepage: http://typicons.com/
|
||||
|
||||
|
||||
## Iconic
|
||||
|
||||
Copyright (C) 2012 by P.J. Onori
|
||||
|
||||
Author: P.J. Onori
|
||||
License: SIL (http://scripts.sil.org/OFL)
|
||||
Homepage: http://somerandomdude.com/work/iconic/
|
||||
|
||||
|
||||
## Fontelico
|
||||
|
||||
Copyright (C) 2012 by Fontello project
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
.icon-heart-outline:before { content: '\e803'; } /* '' */
|
||||
.icon-heart-filled:before { content: '\e804'; } /* '' */
|
||||
.icon-comment:before { content: '\e805'; } /* '' */
|
||||
.icon-reload:before { content: '\e806'; } /* '' */
|
||||
.icon-check:before { content: '\e807'; } /* '' */
|
||||
.icon-plus:before { content: '\e808'; } /* '' */
|
||||
.icon-search:before { content: '\e809'; } /* '' */
|
||||
.icon-loading:before { content: '\e839'; } /* '' */
|
||||
.icon-external:before { content: '\f08e'; } /* '' */
|
||||
.icon-star-half:before { content: '\f123'; } /* '' */
|
File diff suppressed because one or more lines are too long
|
@ -5,8 +5,10 @@
|
|||
.icon-heart-outline { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-heart-filled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-reload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-loading { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-external { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-star-half { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
|
@ -16,8 +16,10 @@
|
|||
.icon-heart-outline { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-heart-filled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-reload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-loading { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-external { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-star-half { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'icons';
|
||||
src: url('../font/icons.eot?59778868');
|
||||
src: url('../font/icons.eot?59778868#iefix') format('embedded-opentype'),
|
||||
url('../font/icons.woff2?59778868') format('woff2'),
|
||||
url('../font/icons.woff?59778868') format('woff'),
|
||||
url('../font/icons.ttf?59778868') format('truetype'),
|
||||
url('../font/icons.svg?59778868#icons') format('svg');
|
||||
src: url('../font/icons.eot?3055787');
|
||||
src: url('../font/icons.eot?3055787#iefix') format('embedded-opentype'),
|
||||
url('../font/icons.woff2?3055787') format('woff2'),
|
||||
url('../font/icons.woff?3055787') format('woff'),
|
||||
url('../font/icons.ttf?3055787') format('truetype'),
|
||||
url('../font/icons.svg?3055787#icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'icons';
|
||||
src: url('../font/icons.svg?59778868#icons') format('svg');
|
||||
src: url('../font/icons.svg?3055787#icons') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -61,8 +61,10 @@
|
|||
.icon-heart-outline:before { content: '\e803'; } /* '' */
|
||||
.icon-heart-filled:before { content: '\e804'; } /* '' */
|
||||
.icon-comment:before { content: '\e805'; } /* '' */
|
||||
.icon-reload:before { content: '\e806'; } /* '' */
|
||||
.icon-check:before { content: '\e807'; } /* '' */
|
||||
.icon-plus:before { content: '\e808'; } /* '' */
|
||||
.icon-search:before { content: '\e809'; } /* '' */
|
||||
.icon-loading:before { content: '\e839'; } /* '' */
|
||||
.icon-external:before { content: '\f08e'; } /* '' */
|
||||
.icon-star-half:before { content: '\f123'; } /* '' */
|
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'icons';
|
||||
src: url('./font/icons.eot?54567039');
|
||||
src: url('./font/icons.eot?54567039#iefix') format('embedded-opentype'),
|
||||
url('./font/icons.woff?54567039') format('woff'),
|
||||
url('./font/icons.ttf?54567039') format('truetype'),
|
||||
url('./font/icons.svg?54567039#icons') format('svg');
|
||||
src: url('./font/icons.eot?21491342');
|
||||
src: url('./font/icons.eot?21491342#iefix') format('embedded-opentype'),
|
||||
url('./font/icons.woff?21491342') format('woff'),
|
||||
url('./font/icons.ttf?21491342') format('truetype'),
|
||||
url('./font/icons.svg?21491342#icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -306,12 +306,16 @@ body {
|
|||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-heart-filled"></i> <span class="i-name">icon-heart-filled</span><span class="i-code">0xe804</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-comment"></i> <span class="i-name">icon-comment</span><span class="i-code">0xe805</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-reload"></i> <span class="i-name">icon-reload</span><span class="i-code">0xe806</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-check"></i> <span class="i-name">icon-check</span><span class="i-code">0xe807</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-plus"></i> <span class="i-name">icon-plus</span><span class="i-code">0xe808</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-plus"></i> <span class="i-name">icon-plus</span><span class="i-code">0xe808</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-search"></i> <span class="i-name">icon-search</span><span class="i-code">0xe809</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe839"><i class="demo-icon icon-loading animate-spin"></i> <span class="i-name">icon-loading</span><span class="i-code">0xe839</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-external"></i> <span class="i-name">icon-external</span><span class="i-code">0xf08e</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf123"><i class="demo-icon icon-star-half"></i> <span class="i-name">icon-star-half</span><span class="i-code">0xf123</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Binary file not shown.
|
@ -18,10 +18,14 @@
|
|||
|
||||
<glyph glyph-name="comment" unicode="" d="M781 662l-625 0q-21 0-37-15t-15-36l0-365q0-21 15-37t37-16l157 0 0-8 8 8 460 0q22 0 37 16t16 37l0 365q0 21-16 36t-37 15z m0 105q65 0 111-46t46-110l0-365q0-65-46-111t-111-45l-416 0-156-157 0 157-53 0q-65 0-110 45t-46 111l0 365q0 65 46 110t110 46l625 0z" horiz-adv-x="938" />
|
||||
|
||||
<glyph glyph-name="reload" unicode="" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="check" unicode="" d="M625 676q38-20 49-63t-9-80l-260-469q-30-53-91-53-43 0-74 31l-209 207q-31 32-31 74t31 74 74 31 74-31l111-110 193 347q20 38 62 50t80-8z" horiz-adv-x="679" />
|
||||
|
||||
<glyph glyph-name="plus" unicode="" d="M729 454q44 0 74-31t31-73-31-73-74-30l-208 3 0-212q0-43-30-73t-75-31-73 31-30 73l3 212-212-3q-44 0-74 30t-30 73 30 73 74 31l212 0-3 209q0 42 30 73t73 31 75-31 30-73l0-209 208 0z" horiz-adv-x="834" />
|
||||
|
||||
<glyph glyph-name="search" unicode="" d="M335 246l25-25q-28-28-85-86t-73-74l-27 27q16 15 74 73t86 85z m245 551q136 0 234-97t97-234-97-234-234-96q-64 0-123 24l-255-257-184 185 256 255q-26 63-26 123 0 137 98 234t234 97z m0-551q91 0 155 64t64 156-64 155-155 64-156-64-64-155 64-156 156-64z" horiz-adv-x="928" />
|
||||
|
||||
<glyph glyph-name="loading" unicode="" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="external" unicode="" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
|
||||
|
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -71,6 +71,18 @@
|
|||
"css": "comment",
|
||||
"code": 59397,
|
||||
"src": "typicons"
|
||||
},
|
||||
{
|
||||
"uid": "a73c5deb486c8d66249811642e5d719a",
|
||||
"css": "reload",
|
||||
"code": 59398,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "c6344a6ed148da12354cc90705287696",
|
||||
"css": "search",
|
||||
"code": 59401,
|
||||
"src": "iconic"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"name": "English",
|
||||
"locale": "en",
|
||||
"global": {
|
||||
"searchbar_placeholder": "Search for Books",
|
||||
"menu_search": "Search for Books",
|
||||
"menu_login": "Log In",
|
||||
"menu_logout": "Log Out",
|
||||
"footer_repo": "Repo",
|
||||
|
@ -18,6 +18,10 @@
|
|||
"logged_out_recent_updates": "Recent Updates",
|
||||
"logged_out_join_now": "Join Now!"
|
||||
},
|
||||
"404": {
|
||||
"header": "Oops!",
|
||||
"subheader": "It looks like the page you requested doesn't exist. Please try a different one!"
|
||||
},
|
||||
"login": {
|
||||
"log_in": "Log In",
|
||||
"email": "Email",
|
||||
|
@ -31,17 +35,25 @@
|
|||
},
|
||||
"search": {
|
||||
"header": "Search",
|
||||
"placeholder": "Search for Books",
|
||||
"button_text": "Search",
|
||||
"search_source_label": "Search Source",
|
||||
"search_source_help_button": "What's This?",
|
||||
"search_source_help_header": "What does \"Search Source\" mean?",
|
||||
"search_source_help_text": "This refers to where the search tries to look for data. Each source can be easily contributed to if you want to add missing books or correct errors.",
|
||||
"search_source_help_inventaire": "Sources and extends data from WikiData, the service structure that powers Wikipedia and the like. Offers as many language options as possible.",
|
||||
"search_source_help_openLibrary": "Sources data from Internet Archive and may provide a digital copy to read. Only offers English unless a work is in another language.",
|
||||
"search_by_label": "Search By",
|
||||
"search_by_title": "Title",
|
||||
"search_by_author": "Author",
|
||||
"loading": "Loading...",
|
||||
"results_header": "Results for:",
|
||||
"no_results": "None Found",
|
||||
"no_results_suggestion": "If you're expecting book data, go and help fill out the Inventaire database!",
|
||||
"people_header": "People",
|
||||
"series_header": "Series",
|
||||
"books_header": "Books",
|
||||
"see_interaction_details": "See All Interactions",
|
||||
"see_book_details": "See Book Details",
|
||||
"see_inventaire_details": "See Details",
|
||||
"see_details_tooltip": "Opens Inventaire in a new tab/window"
|
||||
"see_book_details": "See Book Details"
|
||||
},
|
||||
"interaction": {
|
||||
"reload": "Reload",
|
||||
|
|
87
app/index.js
87
app/index.js
|
@ -3,7 +3,10 @@ import 'babel-polyfill';
|
|||
import choo from 'choo';
|
||||
|
||||
import config from './config.json';
|
||||
import { viewManager } from './views/manager';
|
||||
import { appRoutes } from './appRoutes';
|
||||
import { appListeners } from './appListeners';
|
||||
import { appState } from './appState.js';
|
||||
import { appUtilities } from './appUtilities.js';
|
||||
|
||||
const app = choo();
|
||||
|
||||
|
@ -13,88 +16,18 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
}
|
||||
|
||||
app.use((state, emitter) => {
|
||||
app.getSettingsItem = settingsKey => {
|
||||
let savedSettings = window.localStorage.getItem('settings');
|
||||
if (savedSettings) {
|
||||
savedSettings = JSON.parse(savedSettings);
|
||||
if (typeof savedSettings[settingsKey] !== 'undefined') {
|
||||
return savedSettings[settingsKey];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
app.setSettingsItem = (settingsKey, value) => {
|
||||
let savedSettings = window.localStorage.getItem('settings');
|
||||
if (savedSettings) {
|
||||
savedSettings = JSON.parse(savedSettings);
|
||||
} else {
|
||||
savedSettings = {};
|
||||
}
|
||||
savedSettings[settingsKey] = value;
|
||||
return window.localStorage.setItem('settings', 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.siteConfig = config;
|
||||
appUtilities(app);
|
||||
});
|
||||
|
||||
// App state and emitters
|
||||
app.use((state, emitter) => {
|
||||
const sessionState = app.getSessionState();
|
||||
if (sessionState) {
|
||||
Object.keys(sessionState).forEach(key => {
|
||||
if (typeof state[key] === 'undefined') {
|
||||
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 = {};
|
||||
state.isLoggedIn = false;
|
||||
}
|
||||
appState(app, state);
|
||||
|
||||
// Listeners
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.title = config.siteName;
|
||||
// Emitter listeners
|
||||
emitter.on('render', callback => {
|
||||
app.setSessionState();
|
||||
// This is a dirty hack to get the callback to call *after* re-rendering.
|
||||
if (callback && typeof callback === "function") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('change-view', newView => {
|
||||
// Change the view and call render. Makes it easier to call within views.
|
||||
state.currentView = newView;
|
||||
emitter.emit('render', () => {});
|
||||
});
|
||||
|
||||
emitter.on('set-language', newLanguage => {
|
||||
app.setSettingsItem('lang', newLanguage);
|
||||
state.language = newLanguage;
|
||||
emitter.emit('render', () => {});
|
||||
});
|
||||
});
|
||||
appListeners(app, state, emitter);
|
||||
});
|
||||
|
||||
// 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);
|
||||
// Routes
|
||||
appRoutes(app);
|
||||
|
||||
app.mount('body'); // Overwrite the `<body>` tag with the content of the Choo app
|
||||
|
|
|
@ -19,6 +19,11 @@ nav {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
// For some reason, hidden is not important by default!
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pseudo[data-tooltip]::after {
|
||||
background-color: $picnic-black;
|
||||
color: $picnic-white;
|
||||
|
@ -107,12 +112,38 @@ footer nav {
|
|||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
.card.info {
|
||||
background: $picnic-info;
|
||||
color: $picnic-white;
|
||||
|
||||
* {
|
||||
.card {
|
||||
&.info {
|
||||
background: $picnic-info;
|
||||
color: $picnic-white;
|
||||
|
||||
* {
|
||||
color: $picnic-white;
|
||||
}
|
||||
}
|
||||
&.success {
|
||||
background: $picnic-success;
|
||||
color: $picnic-white;
|
||||
|
||||
* {
|
||||
color: $picnic-white;
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
background: $picnic-warning;
|
||||
color: $picnic-white;
|
||||
|
||||
* {
|
||||
color: $picnic-white;
|
||||
}
|
||||
}
|
||||
&.error {
|
||||
background: $picnic-error;
|
||||
color: $picnic-white;
|
||||
|
||||
* {
|
||||
color: $picnic-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,6 +170,9 @@ th {
|
|||
}
|
||||
|
||||
// Handy Utilities
|
||||
.marginless {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.paddingless {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
export const errorView = (state, emit, i18n) => {
|
||||
return html`<section class="error card">
|
||||
<header>
|
||||
<h1>${i18n.__('404.header')}</h1>
|
||||
</header>
|
||||
<footer>
|
||||
<h2>${i18n.__('404.subheader')}</h2>
|
||||
</footer>
|
||||
</section>`;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
import headerImage from '../../dev/images/header.png';
|
||||
|
||||
export const globalView = (state, emit, view) => {
|
||||
const { i18n } = state;
|
||||
// Create a wrapper for view content that includes global header/footer
|
||||
return html`<body>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="brand">
|
||||
<a href="/">
|
||||
<span><img src=${headerImage} alt="Readlebee"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- responsive-->
|
||||
<input id="navMenu" type="checkbox" class="show">
|
||||
<label for="navMenu" class="burger pseudo button">${'\u2261'}</label>
|
||||
|
||||
<div class="menu">
|
||||
<a href="/search" class="pseudo button"><i class="icon-search" aria-label=${i18n.__('global.menu_search')}></i></a>
|
||||
<a href="/login" class="pseudo button">${i18n.__('global.menu_login')}</a>
|
||||
<a href="/logout" class="pseudo button">${i18n.__('global.menu_logout')}</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
${view(state, emit, i18n)}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<nav>
|
||||
<div class="links">
|
||||
<a href="https://gitlab.com/Alamantus/Readlebee" class="pseudo button">
|
||||
${i18n.__('global.footer_repo')}
|
||||
</a>
|
||||
<a href="https://gitter.im/Readlebee/community" class="pseudo button">
|
||||
${i18n.__('global.footer_chat')}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</body>`;
|
||||
}
|
|
@ -53,7 +53,7 @@ export const loggedOutView = (homeController, emit) => {
|
|||
<header>
|
||||
<h3>${__('home.logged_out_recent_reviews')}</h3>
|
||||
<button class="small pseudo pull-right tooltip-left" data-tooltip=${__('interaction.reload')}>
|
||||
<i class="icon-loading"></i><!--/* This needs to get updated to a reload icon */-->
|
||||
<i class="icon-reload"></i>
|
||||
</button>
|
||||
</header>
|
||||
<footer>
|
||||
|
@ -66,7 +66,7 @@ export const loggedOutView = (homeController, emit) => {
|
|||
<header>
|
||||
<h3>${__('home.logged_out_recent_updates')}</h3>
|
||||
<button class="small pseudo pull-right tooltip-left" data-tooltip=${__('interaction.reload')}>
|
||||
<i class="icon-loading"></i><!--/* This needs to get updated to a reload icon */-->
|
||||
<i class="icon-reload"></i>
|
||||
</button>
|
||||
</header>
|
||||
<footer>
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import html from 'choo/html';
|
||||
|
||||
import headerImage from '../../dev/images/header.png';
|
||||
|
||||
import { I18n } from '../i18n';
|
||||
import { homeView } from './home';
|
||||
import { loginView } from './login';
|
||||
import { searchView } from './search';
|
||||
|
||||
export const viewManager = (state, emit) => {
|
||||
const i18n = new I18n(state); // Global I18n class passed to all views
|
||||
// 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, i18n);
|
||||
break;
|
||||
}
|
||||
case 'login': {
|
||||
htmlContent = loginView(state, emit, i18n);
|
||||
break;
|
||||
}
|
||||
case 'search': {
|
||||
htmlContent = searchView(state, emit, i18n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a wrapper for view content that includes global header/footer
|
||||
let view = html`<body>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="brand">
|
||||
<a href="/">
|
||||
<span><img src=${headerImage} alt="Readlebee"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- responsive-->
|
||||
<input id="navMenu" type="checkbox" class="show">
|
||||
<label for="navMenu" class="burger pseudo button">${'\u2261'}</label>
|
||||
|
||||
<div class="menu">
|
||||
<label style="display: inline-block;">
|
||||
<input type="text" name="search"
|
||||
placeholder=${i18n.__('global.searchbar_placeholder')}
|
||||
onchange=${e => {
|
||||
emit('pushState', '/search?for=' + encodeURIComponent(e.target.value.trim()));
|
||||
}}
|
||||
>
|
||||
</label>
|
||||
<a href="/login" class="pseudo button">${i18n.__('global.menu_login')}</a>
|
||||
<a href="/logout" class="pseudo button">${i18n.__('global.menu_logout')}</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
${htmlContent}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<nav>
|
||||
<div class="links">
|
||||
<a href="https://gitlab.com/Alamantus/Readlebee" class="pseudo button">
|
||||
${i18n.__('global.footer_repo')}
|
||||
</a>
|
||||
<a href="https://gitter.im/Readlebee/community" class="pseudo button">
|
||||
${i18n.__('global.footer_chat')}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</body>`;
|
||||
|
||||
return view;
|
||||
}
|
|
@ -5,6 +5,7 @@ export const modal = (modalId, controller, contentHTML, options = {}) => {
|
|||
* controller <class>: Pass the controller class with state; Requires get/set for openModal in state.
|
||||
* buttonHTML <choo/html>: Displayed in place of the default button to open the modal
|
||||
* buttonText <string>: Displayed if no buttonHTML is specified
|
||||
* buttonClasses <string>: Used with buttonText. If excluded, 'button' is used.
|
||||
* noHeader <bool>: Set to `true` and exclude headerHTML to not include a modal header
|
||||
* headerHTML <choo/html>: Displayed in place of the default header; Recommended to use `<header>` tag
|
||||
* headerText <string>: Displayed in an `<h3>` if no header is specified
|
||||
|
@ -19,7 +20,9 @@ export const modal = (modalId, controller, contentHTML, options = {}) => {
|
|||
return [
|
||||
(
|
||||
typeof options.buttonHTML === 'undefined'
|
||||
? html`<label for=${modalId} class="button">${options.buttonText}</label>`
|
||||
? html`<label for=${modalId} class=${typeof options.buttonClasses === 'undefined' ? 'button' : options.buttonClasses}>
|
||||
${options.buttonText}
|
||||
</label>`
|
||||
: options.buttonHTML
|
||||
),
|
||||
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import { ViewController } from '../controller';
|
||||
|
||||
export class SearchController extends ViewController {
|
||||
constructor(state, i18n) {
|
||||
constructor(state, emit, i18n) {
|
||||
// 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, i18n, 'search', {
|
||||
lastSearch: undefined,
|
||||
done: false,
|
||||
results: {
|
||||
humans: [],
|
||||
series: [],
|
||||
works: [],
|
||||
},
|
||||
lastSearch: '',
|
||||
lastSource: 'inventaire',
|
||||
lastBy: 'title',
|
||||
searchSource: 'inventaire',
|
||||
searchBy: 'title',
|
||||
done: true,
|
||||
results: [],
|
||||
openModal: null,
|
||||
});
|
||||
|
||||
this.emit = emit;
|
||||
|
||||
// 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.
|
||||
|
@ -32,6 +34,12 @@ export class SearchController extends ViewController {
|
|||
return this.appState.query.hasOwnProperty('for') && this.appState.query.for.trim() !== '';
|
||||
}
|
||||
|
||||
get queryIsNew() {
|
||||
return this.state.lastSearch !== this.appState.query.for.trim()
|
||||
|| this.state.lastSource !== this.state.searchSource
|
||||
|| this.state.lastBy !== this.state.searchBy;
|
||||
}
|
||||
|
||||
get openModal() {
|
||||
return this.state.openModal;
|
||||
}
|
||||
|
@ -43,18 +51,22 @@ export class SearchController extends ViewController {
|
|||
search() {
|
||||
if (this.hasQuery) {
|
||||
this.state.done = false;
|
||||
this.state.lastSearch = this.appState.query.for;
|
||||
this.emit('render', () => {
|
||||
this.state.lastSearch = this.appState.query.for;
|
||||
this.state.lastSource = this.state.searchSource;
|
||||
this.state.lastBy = this.state.searchBy;
|
||||
|
||||
const searchTerm = this.appState.query.for.trim();
|
||||
const searchTerm = this.appState.query.for.trim();
|
||||
|
||||
return fetch(`/api/search?for=${searchTerm}&lang=${this.appState.language}`)
|
||||
.then(response => response.json())
|
||||
.then(responseJSON => {
|
||||
this.state.results = responseJSON;
|
||||
this.state.done = true;
|
||||
});
|
||||
return fetch(`/api/search?for=${searchTerm}&by=${this.state.searchBy}&lang=${this.appState.language}&source=${this.state.searchSource}`)
|
||||
.then(response => response.json())
|
||||
.then(responseJSON => {
|
||||
this.state.results = responseJSON;
|
||||
this.state.done = true;
|
||||
})
|
||||
.then(() => this.emit('render'));
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getCovers(inventaireURI) {
|
||||
|
|
|
@ -2,52 +2,156 @@ import html from 'choo/html';
|
|||
|
||||
import { SearchController } from './controller'; // The controller for this view, where processing should happen.
|
||||
import { resultDetails } from './resultDetails';
|
||||
import { modal } from '../partials/modal';
|
||||
|
||||
// This is the view function that is exported and used in the view manager.
|
||||
export const searchView = (state, emit, i18n) => {
|
||||
const controller = new SearchController(state, i18n);
|
||||
const controller = new SearchController(state, emit, i18n);
|
||||
const { __ } = controller.i18n;
|
||||
|
||||
if (controller.state.lastSearch !== state.query.for) {
|
||||
controller.search().then(() => {
|
||||
emit('render');
|
||||
});
|
||||
if (controller.hasQuery && controller.queryIsNew) {
|
||||
controller.search();
|
||||
} else if (controller.state.lastSearch !== '') {
|
||||
controller.appState.query.for = controller.state.lastSearch;
|
||||
}
|
||||
|
||||
// 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>
|
||||
<h1 class="title">${__('search.header')}</h1>
|
||||
html`<h1 class="title">${__('search.header')}</h1>`,
|
||||
|
||||
<article>
|
||||
<h2>
|
||||
${controller.doneSearching
|
||||
? html`<span>${__('search.results_header')}</span> <code>${controller.state.lastSearch}</code>`
|
||||
: html`<i class="icon-loading animate-spin"></i> <span>${__('search.loading')}</span>`
|
||||
html`<section class="flex">
|
||||
<label class="three-fourth">
|
||||
<input type="text" name="search"
|
||||
aria-label=${i18n.__('search.placeholder')}
|
||||
placeholder=${i18n.__('search.placeholder')}
|
||||
value=${controller.state.lastSearch}
|
||||
onchange=${e => {
|
||||
emit('pushState', '/search?for=' + encodeURIComponent(e.target.value.trim()));
|
||||
}}
|
||||
${!controller.doneSearching ? 'disabled' : null}
|
||||
>
|
||||
</label>
|
||||
<button class="fourth" style="margin-top:0;height:2.1em;"
|
||||
onclick=${() => emit('pushState', '/search?for=' + encodeURIComponent(controller.appState.query.for))}
|
||||
${!controller.doneSearching ? 'disabled' : null}
|
||||
>
|
||||
${!controller.doneSearching
|
||||
? html`<i class="icon-loading animate-spin"></i>`
|
||||
: __('search.button_text')
|
||||
}
|
||||
</h2>
|
||||
</button>
|
||||
</section>`,
|
||||
|
||||
${!controller.doneSearching || controller.results.works < 1
|
||||
? [
|
||||
html`<h3>${__('search.no_results')}</h3>`,
|
||||
html`<a class="button" href="https://wiki.inventaire.io/wiki/How-to-contribute" target="_blank">
|
||||
${__('search.no_results_suggestion')}
|
||||
</a>`
|
||||
]
|
||||
: controller.results.works.map(result => {
|
||||
return html`<div class="flex search-result">
|
||||
<div class="two-third-800 half-500">
|
||||
<h3 class="title">${result.name}</h3>
|
||||
${result.description ? html`<h4 class="subtitle">${result.description}</h4>` : null}
|
||||
</div>
|
||||
<div class="third-800 half-500">
|
||||
${resultDetails(controller, result, emit)}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
// Search Options Section
|
||||
html`<section class="flex one two-700">
|
||||
<div>
|
||||
${modal('searchSourceInfo', controller, [
|
||||
html`<p>
|
||||
${__('search.search_source_help_text')}
|
||||
</p>`,
|
||||
html`<ul>
|
||||
<li>
|
||||
<a href="https://inventaire.io" target="_blank">
|
||||
Inventaire
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
${__('search.search_source_help_inventaire')}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openlibrary.org" target="_blank">
|
||||
Open Library
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
${__('search.search_source_help_openLibrary')}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>`,
|
||||
], {
|
||||
buttonText: __('search.search_source_help_button'),
|
||||
buttonClasses: 'small marginless pseudo button pull-right',
|
||||
headerText: __('search.search_source_help_header'),
|
||||
})}
|
||||
<label>
|
||||
${__('search.search_source_label')}
|
||||
|
||||
<select onchange=${event => {
|
||||
controller.state.searchSource = event.target.value;
|
||||
}}>
|
||||
<option value="inventaire" ${controller.state.searchSource === 'inventaire' ? 'selected' : null}>
|
||||
Inventaire
|
||||
</option>
|
||||
<option value="openLibrary" ${controller.state.searchSource === 'openLibrary' ? 'selected' : null}>
|
||||
Open Library
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
${__('search.search_by_label')}<br>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="searchBy" value="title"
|
||||
${controller.state.searchBy === 'title' ? 'checked' : null}
|
||||
onchange=${(event) => {
|
||||
if (event.target.checked) {
|
||||
controller.state.searchBy = event.target.value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="checkable">
|
||||
${__('search.search_by_title')}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="searchBy" value="author"
|
||||
${controller.state.searchBy === 'author' ? 'checked' : null}
|
||||
onchange=${(event) => {
|
||||
if (event.target.checked) {
|
||||
controller.state.searchBy = event.target.value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="checkable">
|
||||
${__('search.search_by_author')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>`,
|
||||
|
||||
// Search Results section
|
||||
html`<section>
|
||||
<h2>
|
||||
${controller.hasQuery && !controller.doneSearching
|
||||
? html`<span>${__('search.loading')}</span> <i class="icon-loading animate-spin"></i>`
|
||||
: null
|
||||
}
|
||||
</article>
|
||||
</h2>
|
||||
|
||||
${controller.hasQuery && controller.doneSearching && controller.results < 1
|
||||
? [
|
||||
html`<h3>${__('search.no_results')}</h3>`,
|
||||
html`<a class="button" href="https://wiki.inventaire.io/wiki/How-to-contribute" target="_blank">
|
||||
${__('search.no_results_suggestion')}
|
||||
</a>`
|
||||
]
|
||||
: controller.results.map(result => {
|
||||
return html`<article class="flex search-result">
|
||||
<header class="two-third-800 half-500">
|
||||
<h3 class="title">${result.name}</h3>
|
||||
${result.description ? html`<h4 class="subtitle">${result.description}</h4>` : null}
|
||||
</header>
|
||||
<footer class="third-800 half-500">
|
||||
${resultDetails(controller, result, emit)}
|
||||
</footer>
|
||||
</article>`;
|
||||
})
|
||||
}
|
||||
</section>`,
|
||||
];
|
||||
}
|
|
@ -83,11 +83,9 @@ export const resultDetails = (searchController, result, emit = () => {}) => {
|
|||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span data-tooltip=${__('search.see_details_tooltip')}>
|
||||
<a class="small pseudo button" href=${result.link} target="_blank">
|
||||
${__('search.see_book_details')}
|
||||
</a>
|
||||
</span>
|
||||
<a class="small button" href=${result.link} target="_blank">
|
||||
${__('search.see_book_details')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</article>`;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
class BooksController {
|
||||
constructor(inventaireDomain, bookURI, language) {
|
||||
this.inventaire = inventaireDomain;
|
||||
constructor(bookSource, bookURI, language) {
|
||||
this.source = bookSource;
|
||||
this.inventaire = 'https://inventaire.io';
|
||||
this.openLibrary = 'https://openlibrary.org';
|
||||
this.bookBrainz = 'https://bookbrainz.org';
|
||||
this.uri = bookURI;
|
||||
this.lang = language;
|
||||
}
|
||||
|
@ -87,7 +90,7 @@ class BooksController {
|
|||
typeof entityObject.image !== 'undefined'
|
||||
? entityObject.image.map(imageId => {
|
||||
return {
|
||||
uri: imageId,
|
||||
uri: imageId.toString(),
|
||||
url: `${this.inventaire}/img/entities/${imageId}`,
|
||||
}
|
||||
})
|
||||
|
@ -132,6 +135,36 @@ class BooksController {
|
|||
};
|
||||
}
|
||||
|
||||
handleOpenLibraryEntity(entityObject) {
|
||||
return {
|
||||
name: (
|
||||
typeof entityObject.title_suggest !== 'undefined'
|
||||
? entityObject.title_suggest
|
||||
: null
|
||||
),
|
||||
description: (
|
||||
typeof entityObject.author_name !== 'undefined'
|
||||
? `${entityObject.type} by ${entityObject.author_name.map(name => name.trim()).join(', ')}`
|
||||
: null
|
||||
),
|
||||
link: (
|
||||
typeof entityObject.key !== 'undefined'
|
||||
? `${this.openLibrary}${entityObject.key}`
|
||||
: null
|
||||
),
|
||||
uri: (
|
||||
typeof entityObject.key !== 'undefined'
|
||||
? entityObject.key.substr(entityObject.key.lastIndexOf('/') + 1)
|
||||
: null
|
||||
),
|
||||
coverId: (
|
||||
typeof entityObject.cover_i !== 'undefined'
|
||||
? entityObject.cover_i.toString()
|
||||
: false
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async getBookDataFromInventaire() {
|
||||
if (this.uri) {
|
||||
const request = fetch(`${this.inventaire}/api/entities?action=by-uris&uris=${encodeURIComponent(this.uri)}`)
|
||||
|
|
|
@ -3,8 +3,7 @@ const fetch = require('node-fetch');
|
|||
const BooksController = require('./books');
|
||||
|
||||
class SearchController {
|
||||
constructor(inventaireDomain, searchTerm, language = 'en') {
|
||||
this.inventaire = inventaireDomain;
|
||||
constructor(searchTerm, language = 'en') {
|
||||
this.term = searchTerm;
|
||||
this.lang = language;
|
||||
}
|
||||
|
@ -15,7 +14,7 @@ class SearchController {
|
|||
|
||||
quickSearchInventaire() {
|
||||
if (this.hasQuery) {
|
||||
const request = fetch(`${this.inventaire}/api/search?types=works&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}&limit=10`)
|
||||
const request = fetch(`https://inventaire.io/api/search?types=works&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}&limit=10`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
|
@ -32,9 +31,11 @@ class SearchController {
|
|||
}
|
||||
});
|
||||
return json.then(responseJSON => {
|
||||
const works = responseJSON.results.map(work => {
|
||||
const booksController = new BooksController(this.inventaire, work.uri, this.lang);
|
||||
const booksController = new BooksController('inventaire', undefined, this.lang);
|
||||
|
||||
return responseJSON.results.map(work => {
|
||||
const bookData = booksController.handleQuickInventaireEntity(work);
|
||||
booksController.uri = bookData.uri; // Update booksController.uri for each book when fetching community data.
|
||||
const communityData = booksController.getCommunityData(5);
|
||||
|
||||
return {
|
||||
|
@ -42,19 +43,13 @@ class SearchController {
|
|||
...communityData,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
humans: [],
|
||||
series: [],
|
||||
works,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchInventaire() {
|
||||
searchInventaire(searchBy = 'title') {
|
||||
if (this.hasQuery) {
|
||||
const request = fetch(`${this.inventaire}/api/entities?action=search&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}`)
|
||||
const request = fetch(`https://inventaire.io/api/entities?action=search&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
|
@ -71,74 +66,8 @@ class SearchController {
|
|||
}
|
||||
});
|
||||
return json.then(responseJSON => {
|
||||
const humans = responseJSON.humans.map(human => {
|
||||
const hasLabels = typeof human.labels !== 'undefined';
|
||||
const hasDescriptions = typeof human.descriptions !== 'undefined';
|
||||
const hasImage = typeof human.image !== 'undefined';
|
||||
return {
|
||||
name: (
|
||||
hasLabels && typeof human.labels[this.lang] !== 'undefined'
|
||||
? human.labels[this.lang]
|
||||
: (
|
||||
hasLabels && Object.keys(human.labels).length > 0
|
||||
? human.labels[Object.keys(human.labels)[0]]
|
||||
: null
|
||||
)
|
||||
),
|
||||
description: (
|
||||
hasDescriptions && typeof human.descriptions[this.lang] !== 'undefined'
|
||||
? human.descriptions[this.lang]
|
||||
: (
|
||||
hasDescriptions && Object.keys(human.descriptions).length > 0
|
||||
? human.descriptions[Object.keys(human.descriptions)[0]]
|
||||
: null
|
||||
)
|
||||
),
|
||||
link: (
|
||||
typeof human.uri !== 'undefined'
|
||||
? `${this.inventaire}/entity/${human.uri}`
|
||||
: null
|
||||
),
|
||||
image: (
|
||||
hasImage && typeof human.image.url !== 'undefined'
|
||||
? human.image
|
||||
: null
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const series = responseJSON.series.map(serie => {
|
||||
const hasLabels = typeof serie.labels !== 'undefined';
|
||||
const hasDescriptions = typeof serie.descriptions !== 'undefined';
|
||||
return {
|
||||
name: (
|
||||
hasLabels && typeof serie.labels[this.lang] !== 'undefined'
|
||||
? serie.labels[this.lang]
|
||||
: (
|
||||
hasLabels && Object.keys(serie.labels).length > 0
|
||||
? serie.labels[Object.keys(serie.labels)[0]]
|
||||
: null
|
||||
)
|
||||
),
|
||||
description: (
|
||||
hasDescriptions && typeof serie.descriptions[this.lang] !== 'undefined'
|
||||
? serie.descriptions[this.lang]
|
||||
: (
|
||||
hasDescriptions && Object.keys(serie.descriptions).length > 0
|
||||
? serie.descriptions[Object.keys(serie.descriptions)[0]]
|
||||
: null
|
||||
)
|
||||
),
|
||||
link: (
|
||||
typeof serie.uri !== 'undefined'
|
||||
? `${this.inventaire}/entity/${serie.uri}`
|
||||
: null
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const works = responseJSON.works.map(work => {
|
||||
const booksController = new BooksController(this.inventaire, work.uri, this.lang);
|
||||
const booksController = new BooksController('inventaire', undefined, this.lang);
|
||||
return responseJSON.works.map(work => {
|
||||
const bookData = booksController.handleInventaireEntity(work);
|
||||
const communityData = booksController.getCommunityData(5);
|
||||
|
||||
|
@ -147,12 +76,6 @@ class SearchController {
|
|||
...communityData,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
humans,
|
||||
series,
|
||||
works,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -245,44 +168,62 @@ class SearchController {
|
|||
});
|
||||
}
|
||||
|
||||
async searchOpenLibrary() {
|
||||
async searchOpenLibrary(searchBy = 'title') {
|
||||
if (!this.hasQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fetch('http://openlibrary.org/search.json?q=' + encodeURIComponent(this.term))
|
||||
return fetch(`https://openlibrary.org/search.json?${searchBy}=${encodeURIComponent(this.term)}`)
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
if (!response.hasOwnProperty('docs')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const booksController = new BooksController('openLibrary', undefined, this.lang);
|
||||
|
||||
// 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,
|
||||
};
|
||||
return booksController.handleOpenLibraryEntity(doc);
|
||||
});
|
||||
|
||||
let results = [];
|
||||
// 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 => {
|
||||
docs.forEach(doc => {
|
||||
const existingDoc = results.find((filterResult) => {
|
||||
return filterResult.title === doc.title && filterResult.description === doc.description;
|
||||
});
|
||||
|
||||
if (!existingDoc) {
|
||||
results.push(doc);
|
||||
}
|
||||
});
|
||||
|
||||
results = results.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);
|
||||
return doc.name.toLowerCase() === result.name.toLowerCase() && doc.description === result.description;
|
||||
});
|
||||
result.covers = [];
|
||||
duplicates.forEach(duplicate => {
|
||||
if (duplicate.cover !== false) {
|
||||
result.covers.push(duplicate.cover);
|
||||
if (duplicate.coverId !== false) {
|
||||
result.covers.push({
|
||||
uri: duplicate.coverId,
|
||||
url: `//covers.openlibrary.org/b/id/${duplicate.coverId}-M.jpg`,
|
||||
});
|
||||
}
|
||||
});
|
||||
delete result.coverId;
|
||||
return result;
|
||||
}).map(bookData => {
|
||||
// Use bookController to get community data
|
||||
booksController.uri = bookData.uri; // Update booksController.uri for each book when fetching community data.
|
||||
const communityData = booksController.getCommunityData(5);
|
||||
|
||||
return {
|
||||
...bookData,
|
||||
...communityData,
|
||||
};
|
||||
});
|
||||
|
||||
return results;
|
||||
|
|
|
@ -3,10 +3,27 @@ const SearchController = require('../controllers/search');
|
|||
async function routes(fastify, options) {
|
||||
fastify.get('/api/search', async (request, reply) => {
|
||||
const searchTerm = typeof request.query.for !== 'undefined' ? request.query.for.trim() : '';
|
||||
const searchBy = typeof request.query.by !== 'undefined' ? request.query.by.trim() : 'title';
|
||||
const language = typeof request.query.lang !== 'undefined' ? request.query.lang.trim().split('-')[0] : undefined; // Get base language in cases like 'en-US'
|
||||
const search = new SearchController(fastify.siteConfig.inventaireDomain, searchTerm, language);
|
||||
const searchSource = typeof request.query.source !== 'undefined' ? request.query.source.trim() : undefined; // Get base language in cases like 'en-US'
|
||||
const search = new SearchController(searchTerm, language);
|
||||
|
||||
return await search.quickSearchInventaire();
|
||||
switch (searchSource) {
|
||||
case 'openLibrary': {
|
||||
return await search.searchOpenLibrary(searchBy);
|
||||
}
|
||||
case 'bookBrainz': {
|
||||
return await search.searchOpenLibrary(searchBy);
|
||||
}
|
||||
case 'inventaire':
|
||||
default: {
|
||||
if (searchBy === 'title') {
|
||||
return await search.quickSearchInventaire();
|
||||
} else {
|
||||
return await search.searchInventaire(searchBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/api/search/cover', async (request, reply) => {
|
||||
|
|
Loading…
Reference in New Issue