diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/.eslintrc | 5 | ||||
| -rw-r--r-- | app/components/comment.js | 17 | ||||
| -rw-r--r-- | app/components/comment_form.js | 73 | ||||
| -rw-r--r-- | app/components/comments.js | 76 | ||||
| -rw-r--r-- | app/components/login.js | 46 | ||||
| -rw-r--r-- | app/components/post.js | 23 | ||||
| -rw-r--r-- | app/components/post_form.js | 70 | ||||
| -rw-r--r-- | app/components/posts.js | 77 | ||||
| -rw-r--r-- | app/components/welcome.js | 39 | ||||
| -rw-r--r-- | app/dasein.js | 83 | ||||
| -rw-r--r-- | app/filters/datetime.js | 10 | ||||
| -rw-r--r-- | app/filters/usertime.js | 20 | ||||
| -rw-r--r-- | app/services/auth.js | 75 |
13 files changed, 614 insertions, 0 deletions
diff --git a/app/.eslintrc b/app/.eslintrc new file mode 100644 index 0000000..bd14a19 --- /dev/null +++ b/app/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +} diff --git a/app/components/comment.js b/app/components/comment.js new file mode 100644 index 0000000..33ce2c7 --- /dev/null +++ b/app/components/comment.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; + +const internals = {}; + +export default internals.CommentComponent = Vue.component('comment', { + template: '<article class="comment">' + + '<aside class="comment-meta">' + + '<img :src="comment.userImage" v-bind:alt="\'Avatar for @\' + comment.userId">' + + '<a v-bind:href="\'https://twitter.com/\' + comment.userId">{{comment.userName}}</a> said on ' + + '<time v-bind:datetime="comment.timestamp | datetime">{{comment.timestamp | usertime}}</time>' + + '</aside>' + + '<div class="comment-content">{{comment.content}}</div>' + + '</article>', + + props: ['comment'] +}); + diff --git a/app/components/comment_form.js b/app/components/comment_form.js new file mode 100644 index 0000000..bf60d06 --- /dev/null +++ b/app/components/comment_form.js @@ -0,0 +1,73 @@ +import Axios from 'axios'; +import Vue from 'vue'; +import AuthService from '../services/auth'; + +const internals = {}; + +internals.kPostsRoute = '/api/posts'; +internals.kCommentsRoute = '/comments'; + +export default internals.CommentFormComponent = Vue.component('comment-form', { + template: '<div class="comment-form-container">' + + '<p v-show="!active" class="comment-form-error">' + + '<button class="comment-activate" v-on:click="activate">Add comment.</button>' + + '</p>' + + '<textarea v-show="active" :disabled="submitting" v-model="content" class="comment-content-input" placeholder="tell us something" maxlength=255></textarea>' + + '<p v-show="message" class="comment-form-error">{{message}}</p>' + + '<button v-show="active" :disabled="submitting" class="comment-submit" v-on:click="submit">Go.</button>' + + '</div>', + + props: ['postUuid'], + + data() { + + return { + content: '', + message: '', + active: false, + submitting: false, + authService: new AuthService() + }; + }, + + methods: { + + // Activates the form. + + activate() { + + this.active = true; + }, + + // Creates a comment + + submit() { + + this.submitting = true; + const route = `${internals.kPostsRoute}/${this.postUuid}${internals.kCommentsRoute}`; + + return Axios({ + method: 'post', + headers: { + Authorization: `Bearer ${this.authService.token}` + }, + data: { + content: this.content + }, + url: route + }).then((response) => { + + this.$emit('comment-submitted', response.data); + this.content = ''; + this.message = ''; + this.submitting = false; + this.active = false; + }).catch((err) => { + + console.error(err.stack); + this.submitting = false; + this.message = 'Error while creating the post...'; + }); + } + } +}); diff --git a/app/components/comments.js b/app/components/comments.js new file mode 100644 index 0000000..e29cedc --- /dev/null +++ b/app/components/comments.js @@ -0,0 +1,76 @@ +import Axios from 'axios'; +import Vue from 'vue'; +import AuthService from '../services/auth'; + +import CommentFormComponent from './comment_form'; +import CommentComponent from './comment'; +import DatetimeFilter from '../filters/datetime'; +import UsertimeFilter from '../filters/usertime'; + +const internals = {}; + +internals.kPostsRoute = '/api/posts'; +internals.kCommentsRoute = '/comments'; +internals.kPollFrequency = 2000; + +export default internals.CommentsComponent = Vue.component('comments', { + template: '<div class="comments-container">' + + '<p v-show="message" class="comments-error">{{message}}</p>' + + '<comment v-for="comment in comments" v-bind:comment="comment" :key="comment.uuid"></comment>' + + '<comment-form v-bind:postUuid="postUuid" v-on:comment-submitted="addComment"></comment-form>' + + '</div>', + + props: ['postUuid'], + + data() { + + return { + message: '', + poller: null, + authService: new AuthService(), + comments: [] + }; + }, + + methods: { + fetchComments() { + + const route = `${internals.kPostsRoute}/${this.postUuid}${internals.kCommentsRoute}`; + + return Axios({ + method: 'get', + headers: { + Authorization: `Bearer ${this.authService.token}` + }, + url: route + }).then((response) => { + + this.comments = response.data; + if (!this._isBeingDestroyed) { + setTimeout(this.fetchComments.bind(this), internals.kPollFrequency); + } + }).catch((err) => { + + console.error(err.stack); + this.message = 'Error while loading the comments...'; + }); + }, + + addComment(comment) { + + this.comments.push(comment); + } + }, + + components: { + commentForm: CommentFormComponent, + comment: CommentComponent, + datetime: DatetimeFilter, + usertime: UsertimeFilter + }, + + mounted() { + + this.fetchComments(); + } +}); diff --git a/app/components/login.js b/app/components/login.js new file mode 100644 index 0000000..d900c72 --- /dev/null +++ b/app/components/login.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import AuthService from '../services/auth'; + +const internals = {}; + +export default internals.LoginComponent = Vue.component('login', { + template: '<div class="login-container">' + + '<p>{{message}}</p>' + + '</div>', + + props: ['oAuthToken', 'oAuthVerifier'], + + data() { + + return { + message: 'Logging you in... Wait a sec.', + authService: new AuthService() + }; + }, + methods: { + login() { + + if (this.authService.authenticated) { + return this.$router.push('/'); + } + + return this.authService.getUserObject(this.oAuthToken, this.oAuthVerifier).then((response) => { + + console.log(response); + + return this.authService.login(response.user, response.token, response.expiresAt); + }).then(() => { + + this.$router.push('/'); + }).catch((err) => { + + console.error(err); + this.message = 'Oh no! There was a problem logging you in.'; + }); + } + }, + mounted: function mounted() { + + this.login(); + } +}); diff --git a/app/components/post.js b/app/components/post.js new file mode 100644 index 0000000..bb72189 --- /dev/null +++ b/app/components/post.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; + +import CommentsComponent from './comments'; + +const internals = {}; + +export default internals.PostComponent = Vue.component('post', { + template: '<article class="post">' + + '<aside class="post-meta">' + + '<img :src="post.userImage" v-bind:alt="\'Avatar for @\' + post.userId">' + + '<a v-bind:href="\'https://twitter.com/\' + post.userId">{{post.userName}}</a> said on ' + + '<time v-bind:datetime="post.timestamp | datetime">{{post.timestamp | usertime}}</time>' + + '</aside>' + + '<div class="post-content">{{post.content}}</div>' + + '<comments v-bind:postUuid="post.uuid"></post>' + + '</article>', + + props: ['post'], + + components: { + comments: CommentsComponent + } +}); diff --git a/app/components/post_form.js b/app/components/post_form.js new file mode 100644 index 0000000..1f45f0c --- /dev/null +++ b/app/components/post_form.js @@ -0,0 +1,70 @@ +import Axios from 'axios'; +import Vue from 'vue'; +import AuthService from '../services/auth'; + +const internals = {}; + +internals.kPostsRoute = '/api/posts'; + +export default internals.PostFormComponent = Vue.component('post-form', { + template: '<div class="post-form-container">' + + '<h1>hi {{authService.user.name}}</h1>' + + '<button v-show="!active" class="post-submit" v-on:click="activate">Post something.</button>' + + '<textarea v-show="active" :disabled="submitting" v-model="content" class="post-content-input" placeholder="tell us something" maxlength=255></textarea>' + + '<p v-show="message" class="post-form-error">{{message}}</p>' + + '<button v-show="active" :disabled="submitting" class="post-submit" v-on:click="submit">Go.</button>' + + '</div>', + + data() { + + return { + content: '', + message: '', + submitting: false, + active: false, + authService: new AuthService() + }; + }, + + methods: { + + // Activates the form. + + activate() { + + this.active = true; + }, + + // Creates a post + + submit() { + + this.submitting = true; + + return Axios({ + method: 'post', + headers: { + Authorization: `Bearer ${this.authService.token}` + }, + data: { + content: this.content + }, + url: internals.kPostsRoute + }).then((response) => { + + this.$emit('post-submitted', response.data); + this.content = ''; + this.message = ''; + this.submitting = false; + this.active = false; + }).catch((err) => { + + console.error(err.stack); + this.submitting = false; + this.message = 'Error while creating the post...'; + }); + } + } +}); + + diff --git a/app/components/posts.js b/app/components/posts.js new file mode 100644 index 0000000..d0e0338 --- /dev/null +++ b/app/components/posts.js @@ -0,0 +1,77 @@ +import Axios from 'axios'; +import Vue from 'vue'; +import AuthService from '../services/auth'; + +import PostFormComponent from './post_form'; +import PostComponent from './post'; +import DatetimeFilter from '../filters/datetime'; +import UsertimeFilter from '../filters/usertime'; + +const internals = {}; + +internals.kPostsRoute = '/api/posts'; +internals.kPollFrequency = 5000; + +export default internals.PostsComponent = Vue.component('posts', { + template: '<div v-if="authService.authenticated" class="posts-container">' + + '<post-form v-on:post-submitted="addPost"></post-form>' + + '<h1>Posts.</h1>' + + '<p v-show="message" class="posts-error">{{message}}</p>' + + '<post v-for="post in posts" v-bind:post="post" :key="post.uuid"></post>' + + '</div>', + + data() { + + return { + message: '', + authService: new AuthService(), + posts: [] + }; + }, + + methods: { + fetchPosts() { + + return Axios({ + method: 'get', + headers: { + Authorization: `Bearer ${this.authService.token}` + }, + url: internals.kPostsRoute + }).then((response) => { + + this.posts = response.data; + if (!this._isBeingDestroyed) { + setTimeout(this.fetchPosts.bind(this), internals.kPollFrequency); + } + }).catch((err) => { + + console.error(err.stack); + this.message = 'Error while loading the posts...'; + }); + }, + + addPost(post) { + + this.posts.unshift(post); + } + }, + + components: { + postForm: PostFormComponent, + post: PostComponent, + datetime: DatetimeFilter, + usertime: UsertimeFilter + }, + + mounted() { + + if (!this.authService.authenticated) { + return this.$router.push('/login'); + } + + this.fetchPosts(); + + } +}); + diff --git a/app/components/welcome.js b/app/components/welcome.js new file mode 100644 index 0000000..4fcee0f --- /dev/null +++ b/app/components/welcome.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import AuthService from '../services/auth'; + +/* global window */ + +const internals = {}; + +export default internals.WelcomeComponent = Vue.component('welcome', { + template: '<div class="welcome-container">' + + '<p>{{message}}</p>' + + '<a href="#" v-on:click=initiateLogin v-if=!loggingIn>Login</a>' + + '</div>', + + data() { + + return { + message: 'Welcome to Dasein, a social network with posts that disappear when the conversation stops', + loggingIn: false, + authService: new AuthService() + }; + }, + + methods: { + initiateLogin() { + + this.message = 'Logging you in...'; + this.loggingIn = true; + + this.authService.initiateLogin().then((authData) => { + + window.location.href = authData.loginUrl; + }).catch((err) => { + + console.error(err); + this.message = 'Oh no! There was a problem logging you in.'; + }); + } + } +}); diff --git a/app/dasein.js b/app/dasein.js new file mode 100644 index 0000000..699a01f --- /dev/null +++ b/app/dasein.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; + +import AuthService from './services/auth'; + +import LoginComponent from './components/login'; +import WelcomeComponent from './components/welcome'; +import PostsComponent from './components/posts'; + +/* global window */ + +const internals = {}; + +export default internals.Dasein = { + + start() { + + this._setupVue(); + + internals.authService = new AuthService(); + + this.vm = new Vue({ + router: this._setupRouter(), + el: '#dasein', + methods: { + authenticated() { + + return internals.authService.authenticated; + } + } + }); + + }, + + // Initializes vue options + + _setupVue() { + + Vue.use(VueRouter); + }, + + // Sets up the routes + + _setupRouter() { + + const routes = [ + { + path: '/login', + component: WelcomeComponent + }, + { + path: '/', + component: PostsComponent + }, + { + path: '/login-callback', + component: LoginComponent, + props: (route) => { + + return { + oAuthToken: route.query.oauth_token, + oAuthVerifier: route.query.oauth_verifier + }; + } + } + ]; + + const router = new VueRouter({ + mode: 'history', + routes + }); + + return router; + } +}; + + +internals.run = function () { + + internals.Dasein.start(); +}; + +window.addEventListener('load', internals.run); diff --git a/app/filters/datetime.js b/app/filters/datetime.js new file mode 100644 index 0000000..e4bf432 --- /dev/null +++ b/app/filters/datetime.js @@ -0,0 +1,10 @@ +import Vue from 'vue'; + +const internals = {}; + +export default internals.DatetimeFilter = Vue.filter('datetime', (timestamp) => { + + const date = new Date(parseInt(timestamp)); + + return date.toISOString(); +}); diff --git a/app/filters/usertime.js b/app/filters/usertime.js new file mode 100644 index 0000000..de67894 --- /dev/null +++ b/app/filters/usertime.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; + +const internals = {}; + +export default internals.UsertimeFilter = Vue.filter('usertime', (timestamp) => { + + const date = new Date(parseInt(timestamp)); + + const dateString = date.toISOString(); + + const dateComponents = dateString.split('T'); + const dateFragment = dateComponents[0]; + const timeComponents = dateComponents[1].split(':'); + + const hourFragment = timeComponents[0]; + const minuteFragment = timeComponents[1]; + + return `${dateFragment} ${hourFragment}:${minuteFragment}`; +}); + diff --git a/app/services/auth.js b/app/services/auth.js new file mode 100644 index 0000000..32db321 --- /dev/null +++ b/app/services/auth.js @@ -0,0 +1,75 @@ +import Axios from 'axios'; + +const internals = {}; + +/* global localStorage */ + +internals.kInitiateLoginRoute = '/api/auth/login'; +internals.kHandleCallbackRoute = '/api/auth/callback'; + +export default internals.AuthService = class AuthService { + + constructor() { + + this.authenticated = false; + + // Bootstrap from local storage. + if (localStorage.hasOwnProperty('user') && localStorage.hasOwnProperty('token') + && localStorage.hasOwnProperty('expiresAt')) { + + if (parseInt(localStorage.getItem('expiresAt')) > Date.now()) { + this.expiresAt = localStorage.getItem('expiresAt'); + this.user = JSON.parse(localStorage.getItem('user')); + this.token = localStorage.getItem('token'); + this.authenticated = true; + } + } + } + + // Initiates the login process. Resolves to an object that contains the URL + // to redirect to to continue processing. + initiateLogin() { + + return Axios.get(internals.kInitiateLoginRoute).then((response) => { + + return response.data; + }); + } + + // Posts the oAuthToken and verifier to the API to get back a user object + // and a signed JWT + getUserObject(oAuthToken, oAuthVerifier) { + + return Axios.post(internals.kHandleCallbackRoute, { + oAuthToken, + oAuthVerifier + }).then((response) => { + + return response.data; + }); + } + + // Logs in the user by setting the user and token + login(user, token, expiresAt) { + + localStorage.setItem('user', JSON.stringify(user)); + localStorage.setItem('token', token); + localStorage.setItem('expiresAt', expiresAt); + this.user = user; + this.token = token; + this.expiresAt = expiresAt; + this.authenticated = true; + } + + // Logs a user out by removing the user and token + logout() { + + localStorage.removeItem('user'); + localStorage.removeItem('token'); + localStorage.removeItem('expiresAt'); + delete this.user; + delete this.token; + delete this.expiresAt; + this.authenticated = false; + } +}; |