aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorBen Beltran <ben@nsovocal.com>2017-01-31 00:53:36 -0600
committerBen Beltran <ben@nsovocal.com>2017-01-31 00:53:36 -0600
commita3f9e2603dfdf8c492ec0dc355cd434fc6100f06 (patch)
tree52c84831a3a6070e3e01c5ada5b862fd56f28327 /app
parent7eb26514c478cfa06a797e9d63a29ef6a6d16d59 (diff)
parentf74591da2d5c42b8a1e658d17310e604bedcf856 (diff)
Merge branch 'release/1.0.0'HEADmain
Diffstat (limited to 'app')
-rw-r--r--app/.eslintrc5
-rw-r--r--app/components/comment.js17
-rw-r--r--app/components/comment_form.js73
-rw-r--r--app/components/comments.js76
-rw-r--r--app/components/login.js46
-rw-r--r--app/components/post.js23
-rw-r--r--app/components/post_form.js70
-rw-r--r--app/components/posts.js77
-rw-r--r--app/components/welcome.js39
-rw-r--r--app/dasein.js83
-rw-r--r--app/filters/datetime.js10
-rw-r--r--app/filters/usertime.js20
-rw-r--r--app/services/auth.js75
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;
+ }
+};