Compare commits

...

6 Commits

Author SHA1 Message Date
Robbie Antenesse 5aeee0bc01 Add language picker to footer 2019-10-17 22:15:31 -06:00
Robbie Antenesse 4498ed002a Validate token on initial load 2019-10-17 21:35:32 -06:00
Robbie Antenesse cd0baa7605 Handle log out
It's a little funky because of Choo, but it works right
2019-10-17 21:20:36 -06:00
Robbie Antenesse d8f0de9ec4 Update global header based on loggedIn status 2019-10-17 21:10:15 -06:00
Robbie Antenesse 43a8c006a1 Add loggedIn view for home page 2019-10-17 20:56:57 -06:00
Robbie Antenesse bcde0c6dc7 Fix login errors and redirect to home after success 2019-10-17 20:37:16 -06:00
13 changed files with 154 additions and 40 deletions

View File

@ -14,9 +14,11 @@ export const appListeners = (app, state, emitter) => {
emitter.on('set-language', newLanguage => { emitter.on('set-language', newLanguage => {
app.setSettingsItem('lang', newLanguage); app.setSettingsItem('lang', newLanguage);
state.language = newLanguage; state.language = newLanguage;
emitter.emit('render', () => { }); 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 app.checkIfLoggedIn(state).then(isLoggedIn => {
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
});
}); });
} }

View File

@ -9,6 +9,8 @@ export const appRoutes = (app) => {
app.route('/login', (state, emit) => globalView(state, emit, loginView)); app.route('/login', (state, emit) => globalView(state, emit, loginView));
app.route('/logout', () => window.location.reload()); // If Choo navigates here, refresh the page instead so the server can handle it and log out
app.route('/search', (state, emit) => globalView(state, emit, searchView)); app.route('/search', (state, emit) => globalView(state, emit, searchView));
app.route('/404', (state, emit) => globalView(state, emit, errorView)); app.route('/404', (state, emit) => globalView(state, emit, errorView));

View File

@ -19,4 +19,19 @@ export const appUtilities = (app) => {
savedSettings[settingsKey] = value; savedSettings[settingsKey] = value;
return window.localStorage.setItem('settings', JSON.stringify(savedSettings)); return window.localStorage.setItem('settings', JSON.stringify(savedSettings));
} }
app.checkIfLoggedIn = (appState) => {
return fetch('/api/account/validate', { method: 'post' })
.then(response => response.json())
.then(response => {
if (response.error !== false) {
console.warn(response);
return false;
}
console.info(response.message);
appState.isLoggedIn = true;
return true;
});
}
} }

View File

@ -7,7 +7,11 @@ export class I18n {
default: en, default: en,
en, en,
}; };
this.language = appState.language; this.appState = appState;
}
get language () {
return this.appState.language;
} }
translate (section, phrase) { translate (section, phrase) {

View File

@ -3,10 +3,13 @@
"locale": "en", "locale": "en",
"global": { "global": {
"menu_search": "Search for Books", "menu_search": "Search for Books",
"menu_login": "Log In", "menu_about": "About",
"menu_login": "Log In / Create Account",
"menu_account": "My Profile",
"menu_logout": "Log Out", "menu_logout": "Log Out",
"footer_repo": "Repo", "footer_repo": "Repo",
"footer_chat": "Chat" "footer_chat": "Chat",
"change_language": "Change Language"
}, },
"home": { "home": {
"logged_out_subtitle": "All the Book Buzz in Once Place", "logged_out_subtitle": "All the Book Buzz in Once Place",
@ -16,7 +19,10 @@
"logged_out_community_header": "A Look Inside the Hive", "logged_out_community_header": "A Look Inside the Hive",
"logged_out_recent_reviews": "Recent Reviews", "logged_out_recent_reviews": "Recent Reviews",
"logged_out_recent_updates": "Recent Updates", "logged_out_recent_updates": "Recent Updates",
"logged_out_join_now": "Join Now!" "logged_out_join_now": "Join Now!",
"logged_in_subtitle": "Welcome!",
"logged_in_updates": "Updates",
"logged_in_interactions": "Interactions"
}, },
"404": { "404": {
"header": "Oops!", "header": "Oops!",

View File

@ -19,9 +19,18 @@ export const globalView = (state, emit, view) => {
<label for="navMenu" class="burger pseudo button">${'\u2261'}</label> <label for="navMenu" class="burger pseudo button">${'\u2261'}</label>
<div class="menu"> <div class="menu">
<a href="/search" class="pseudo button"><i class="icon-search" aria-label=${i18n.__('global.menu_search')}></i></a> <a href="/search" class="pseudo button">
<a href="/login" class="pseudo button">${i18n.__('global.menu_login')}</a> <i class="icon-search" aria-labeledBy="searchLabel"></i> <span id="searchLabel">${i18n.__('global.menu_search')}</span>
<a href="/logout" class="pseudo button">${i18n.__('global.menu_logout')}</a> </a>
<a href="/about" class="pseudo button">${i18n.__('global.menu_about')}</a>
${
state.isLoggedIn === true
? [
html`<a href="/account" class="pseudo button">${i18n.__('global.menu_account')}</a>`,
html`<a href="/logout" class="pseudo button">${i18n.__('global.menu_logout')}</a>`,
]
: html`<a href="/login" class="pseudo button">${i18n.__('global.menu_login')}</a>`
}
</div> </div>
</nav> </nav>
</header> </header>
@ -31,14 +40,30 @@ export const globalView = (state, emit, view) => {
</main> </main>
<footer> <footer>
<nav> <nav class="flex one">
<div class="links"> <div class="two-third-600">
<a href="https://gitlab.com/Alamantus/Readlebee" class="pseudo button"> <div class="links">
${i18n.__('global.footer_repo')} <a href="https://gitlab.com/Alamantus/Readlebee" class="pseudo button">
</a> ${i18n.__('global.footer_repo')}
<a href="https://gitter.im/Readlebee/community" class="pseudo button"> </a>
${i18n.__('global.footer_chat')} <a href="https://gitter.im/Readlebee/community" class="pseudo button">
</a> ${i18n.__('global.footer_chat')}
</a>
</div>
</div>
<div class="third-600">
<label class="flex">
<span class="third">${i18n.__('global.change_language')}:</span>
<select class="two-third" onchange=${e => emit('set-language', e.target.value)}>
${Object.keys(i18n.availableLanguages).map(languageKey => {
if (languageKey === 'default') return null;
const language = i18n.availableLanguages[languageKey];
return html`<option value=${language.locale} ${state.language === language.locale ? 'selected' : null}>
${language.name}
</option>`;
})}
</select>
</label>
</div> </div>
</nav> </nav>
</footer> </footer>

View File

@ -5,19 +5,18 @@ export class HomeController extends ViewController {
// Super passes state, view name, and default state to ViewController, // 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 // which stores state in this.appState and the view controller's state to this.state
super(state, i18n, 'home', { super(state, i18n, 'home', {
recentReviews: [], loggedOut: {
recentUpdates: [], recentReviews: [],
recentUpdates: [],
},
loggedIn: {
updates: [], // statuses, ratings, and reviews from people you follow.
interactions: [], // likes, comments, recommendations, etc.
},
}); });
// If using controller methods in an input's onchange or onclick instance, // If using controller methods in an input's onchange or onclick instance,
// either bind the class's 'this' instance to the method first... // either bind the class's 'this' instance to the method first...
// or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead. // or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead.
} }
get recentReviews() {
return [...this.state.recentReviews];
}
get recentUpdates() {
return [...this.state.recentUpdates];
}
} }

View File

@ -2,6 +2,7 @@ import html from 'choo/html';
import { HomeController } from './controller'; // The controller for this view, where processing should happen. import { HomeController } from './controller'; // The controller for this view, where processing should happen.
import { loggedOutView } from './loggedOut'; import { loggedOutView } from './loggedOut';
import { loggedInView } from './loggedIn';
// This is the view function that is exported and used in the view manager. // This is the view function that is exported and used in the view manager.
export const homeView = (state, emit, i18n) => { export const homeView = (state, emit, i18n) => {
@ -12,7 +13,7 @@ export const homeView = (state, emit, i18n) => {
return [ return [
(!controller.isLoggedIn (!controller.isLoggedIn
? loggedOutView(controller, emit) ? loggedOutView(controller, emit)
: html`<p>lol wut how are u logged in</p>` : loggedInView(controller, emit)
), ),
]; ];
} }

View File

@ -0,0 +1,39 @@
import html from 'choo/html';
export const loggedInView = (homeController, emit) => {
const { __ } = homeController.i18n;
return [
html`<section>
<h2>${__('home.logged_in_subtitle')}</h2>
<div class="flex one two-700">
<div>
<div class="card">
<header>
<h3>${__('home.logged_in_updates')}</h3>
<button class="small pseudo pull-right tooltip-left" data-tooltip=${__('interaction.reload')}>
<i class="icon-reload"></i>
</button>
</header>
<footer>
${homeController.state.loggedIn.updates.map(update => reviewCard(homeController, update))}
</footer>
</div>
</div>
<div>
<div class="card">
<header>
<h3>${__('home.logged_in_interactions')}</h3>
<button class="small pseudo pull-right tooltip-left" data-tooltip=${__('interaction.reload')}>
<i class="icon-reload"></i>
</button>
</header>
<footer>
${homeController.state.loggedIn.interactions.map(interaction => reviewCard(homeController, interaction))}
</footer>
</div>
</div>
</div>
</section>`,
];
}

View File

@ -57,7 +57,7 @@ export const loggedOutView = (homeController, emit) => {
</button> </button>
</header> </header>
<footer> <footer>
${homeController.recentReviews.map(review => reviewCard(homeController, review))} ${homeController.state.loggedOut.recentReviews.map(review => reviewCard(homeController, review))}
</footer> </footer>
</div> </div>
</div> </div>
@ -70,7 +70,7 @@ export const loggedOutView = (homeController, emit) => {
</button> </button>
</header> </header>
<footer> <footer>
${homeController.recentUpdates.map(review => reviewCard(homeController, review))} ${homeController.state.loggedOut.recentUpdates.map(update => reviewCard(homeController, update))}
</footer> </footer>
</div> </div>
</div> </div>

View File

@ -16,6 +16,7 @@ export class LoginController extends ViewController {
}, },
loginError: '', loginError: '',
createError: '', createError: '',
loginMessage: '',
createMessage: '', createMessage: '',
pageMessage: '', pageMessage: '',
isChecking: false, isChecking: false,
@ -47,7 +48,7 @@ export class LoginController extends ViewController {
validateLogin () { validateLogin () {
const { __ } = this.i18n; const { __ } = this.i18n;
this.state.createError = ''; this.state.loginError = '';
this.state.isChecking = true; this.state.isChecking = true;
this.emit('render', () => { this.emit('render', () => {
@ -60,7 +61,7 @@ export class LoginController extends ViewController {
loginEmail, loginEmail,
loginPassword, loginPassword,
].includes('')) { ].includes('')) {
this.state.createError = __('login.create_required_field_blank'); this.state.loginError = __('login.login_required_field_blank');
this.state.isChecking = false; this.state.isChecking = false;
this.emit('render'); this.emit('render');
return; return;
@ -132,6 +133,7 @@ export class LoginController extends ViewController {
return; return;
} }
this.appState.isLoggedIn = true;
this.state.loginMessage = __(response.message); this.state.loginMessage = __(response.message);
this.state.isChecking = false; this.state.isChecking = false;
this.clearLoginForm(); this.clearLoginForm();

View File

@ -6,6 +6,28 @@ export const loginView = (state, emit, i18n) => {
const controller = new LoginController(state, emit, i18n); const controller = new LoginController(state, emit, i18n);
const { __ } = controller.i18n; const { __ } = controller.i18n;
if (controller.appState.isLoggedIn === true) {
setTimeout(() => {
controller.state.loginMessage = '';
emit('pushState', '/')
}, 3000);
return html`<div class="modal">
<input type="checkbox" checked>
<label class="overlay"></label>
<article class="success card">
<header>
${
controller.state.loginMessage === ''
? __('login.already_logged_in')
: controller.state.loginMessage
}
</header>
</article>
</div>`;
}
return html`<section> return html`<section>
${ ${

View File

@ -147,16 +147,16 @@ async function routes(fastify, options) {
} }
}); });
fastify.get('/api/account/login', async (request, reply) => { fastify.post('/api/account/login', async (request, reply) => {
const formDataIsValid = Account.loginDataIsValid(request.body); const formDataIsValid = Account.loginDataIsValid(request.body);
if (formDataIsValid !== true) { if (formDataIsValid !== true) {
return reply.code(400).send(formDataIsValid); return reply.code(400).send(formDataIsValid);
} }
const account = new Account(fastify.models.User); const account = new Account(fastify.models.User);
const user = account.validateLogin(request.body.email, request.body.password); const user = await account.validateLogin(request.body.email, request.body.password);
if (user.error !== true) { if (user.error === true) {
return reply.code(400).send(user); return reply.code(400).send(user);
} }
@ -173,7 +173,7 @@ async function routes(fastify, options) {
}) })
.send({ .send({
error: false, error: false,
message: 'api.account_create_success', message: 'api.account_login_success',
}); });
}); });
@ -209,11 +209,8 @@ async function routes(fastify, options) {
}); });
}); });
fastify.get('/api/logout', async (request, reply) => { fastify.get('/logout', async (request, reply) => {
return reply.clearCookie('token', { path: '/' }).send({ return reply.clearCookie('token', { path: '/' }).redirect('/');
error: false,
message: 'api._account_logout_success',
});
}); });
} }