Restructure app initiation

- Split initialization steps into their own files.
- Use Choo routes instead of viewManager to properly set up 404
- Add styling for different colors of Picnic cards
This commit is contained in:
Robbie Antenesse 2019-09-25 12:32:52 -06:00
parent 49ab635660
commit b093595f1d
9 changed files with 153 additions and 112 deletions

27
app/appListeners.js Normal file
View File

@ -0,0 +1,27 @@
export const appListeners = (app, state, emitter) => {
emitter.on('DOMContentLoaded', () => {
document.title = app.siteConfig.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', () => { });
});
});
}

18
app/appRoutes.js Normal file
View File

@ -0,0 +1,18 @@
import { I18n } from './i18n';
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) => {
const i18n = new I18n(app.state); // Global I18n class passed to all views
app.route('/', (state, emit) => globalView(state, emit, i18n, homeView));
app.route('/login', (state, emit) => globalView(state, emit, i18n, loginView));
app.route('/search', (state, emit) => globalView(state, emit, i18n, searchView));
app.route('/404', (state, emit) => globalView(state, emit, i18n, errorView));
}

16
app/appState.js Normal file
View File

@ -0,0 +1,16 @@
export const appState = (app, 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;
}
}

32
app/appUtilities.js Normal file
View File

@ -0,0 +1,32 @@
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));
}
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));
}
}

View File

@ -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",

View File

@ -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

View File

@ -107,12 +107,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;
}
}
}

12
app/views/404.js Normal file
View File

@ -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>`;
}

View File

@ -2,34 +2,9 @@ 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;
}
}
export const globalView = (state, emit, i18n, view) => {
// Create a wrapper for view content that includes global header/footer
let view = html`<body>
return html`<body>
<header>
<nav>
<div class="brand">
@ -58,7 +33,7 @@ export const viewManager = (state, emit) => {
</header>
<main class="container">
${htmlContent}
${view(state, emit, i18n)}
</main>
<footer>
@ -74,6 +49,4 @@ export const viewManager = (state, emit) => {
</nav>
</footer>
</body>`;
return view;
}