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 => {
app.setSettingsItem('lang', 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('/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('/404', (state, emit) => globalView(state, emit, errorView));

View File

@ -19,4 +19,19 @@ export const appUtilities = (app) => {
savedSettings[settingsKey] = value;
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,
en,
};
this.language = appState.language;
this.appState = appState;
}
get language () {
return this.appState.language;
}
translate (section, phrase) {

View File

@ -3,10 +3,13 @@
"locale": "en",
"global": {
"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",
"footer_repo": "Repo",
"footer_chat": "Chat"
"footer_chat": "Chat",
"change_language": "Change Language"
},
"home": {
"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_recent_reviews": "Recent Reviews",
"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": {
"header": "Oops!",

View File

@ -19,9 +19,18 @@ export const globalView = (state, emit, view) => {
<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>
<a href="/search" class="pseudo button">
<i class="icon-search" aria-labeledBy="searchLabel"></i> <span id="searchLabel">${i18n.__('global.menu_search')}</span>
</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>
</nav>
</header>
@ -31,14 +40,30 @@ export const globalView = (state, emit, view) => {
</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>
<nav class="flex one">
<div class="two-third-600">
<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>
</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>
</nav>
</footer>

View File

@ -5,19 +5,18 @@ export class HomeController extends 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
super(state, i18n, 'home', {
recentReviews: [],
recentUpdates: [],
loggedOut: {
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,
// either bind the class's 'this' instance to the method first...
// or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead.
}
get 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 { loggedOutView } from './loggedOut';
import { loggedInView } from './loggedIn';
// This is the view function that is exported and used in the view manager.
export const homeView = (state, emit, i18n) => {
@ -12,7 +13,7 @@ export const homeView = (state, emit, i18n) => {
return [
(!controller.isLoggedIn
? 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>
</header>
<footer>
${homeController.recentReviews.map(review => reviewCard(homeController, review))}
${homeController.state.loggedOut.recentReviews.map(review => reviewCard(homeController, review))}
</footer>
</div>
</div>
@ -70,7 +70,7 @@ export const loggedOutView = (homeController, emit) => {
</button>
</header>
<footer>
${homeController.recentUpdates.map(review => reviewCard(homeController, review))}
${homeController.state.loggedOut.recentUpdates.map(update => reviewCard(homeController, update))}
</footer>
</div>
</div>

View File

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

View File

@ -6,6 +6,28 @@ export const loginView = (state, emit, i18n) => {
const controller = new LoginController(state, emit, 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>
${

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);
if (formDataIsValid !== true) {
return reply.code(400).send(formDataIsValid);
}
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);
}
@ -173,7 +173,7 @@ async function routes(fastify, options) {
})
.send({
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) => {
return reply.clearCookie('token', { path: '/' }).send({
error: false,
message: 'api._account_logout_success',
});
fastify.get('/logout', async (request, reply) => {
return reply.clearCookie('token', { path: '/' }).redirect('/');
});
}