diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..eb0295a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,82 @@ +// https://eslint.org/docs/user-guide/configuring + +module.exports = { + 'root': true, + 'parserOptions': { + ecmaVersion: 6, + }, + 'env': { + browser: true, + }, + 'extends': [ + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + 'eslint:recommended', + ], + // required to lint *.vue files + 'plugins': [], + 'globals': { + axios: true, + Vue: true, + }, + // add your custom rules here + 'rules': { + 'array-bracket-newline': ['error', { multiline: true }], + 'array-bracket-spacing': ['error'], + 'arrow-body-style': ['error', 'as-needed'], + 'arrow-parens': ['error', 'as-needed'], + 'arrow-spacing': ['error', { before: true, after: true }], + 'block-spacing': ['error'], + 'brace-style': ['error', '1tbs'], + 'comma-dangle': ['error', 'always-multiline'], // Apply Contentflow rules + 'comma-spacing': ['error'], + 'comma-style': ['error', 'last'], + 'curly': ['error'], + 'dot-location': ['error', 'property'], + 'dot-notation': ['error'], + 'eol-last': ['error', 'always'], + 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], + 'func-call-spacing': ['error', 'never'], + 'function-paren-newline': ['error', 'multiline'], + 'generator-star-spacing': ['off'], // allow async-await + 'implicit-arrow-linebreak': ['error'], + 'indent': ['error', 2], + 'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }], + 'keyword-spacing': ['error'], + 'linebreak-style': ['error', 'unix'], + 'lines-between-class-members': ['error'], + 'multiline-comment-style': ['warn'], + 'newline-per-chained-call': ['error'], + 'no-console': ['off'], + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // allow debugger during development + 'no-else-return': ['error'], + 'no-extra-parens': ['error'], + 'no-implicit-coercion': ['error'], + 'no-lonely-if': ['error'], + 'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 0, maxBOF: 0 }], + 'no-multi-spaces': ['error'], + 'no-trailing-spaces': ['error'], + 'no-unneeded-ternary': ['error'], + 'no-useless-return': ['error'], + 'no-whitespace-before-property': ['error'], + 'object-curly-newline': ['error', { consistent: true }], + 'object-curly-spacing': ['error', 'always'], + 'object-shorthand': ['error'], + 'padded-blocks': ['error', 'never'], + 'prefer-arrow-callback': ['error'], + 'prefer-const': ['error'], + 'prefer-object-spread': ['error'], + 'prefer-template': ['error'], + 'quote-props': ['error', 'consistent-as-needed', { keywords: true }], + 'quotes': ['error', 'single', { allowTemplateLiterals: true }], + 'semi': ['error', 'never'], + 'space-before-blocks': ['error', 'always'], + 'spaced-comment': ['warn', 'always'], + 'space-infix-ops': ['error'], + 'space-in-parens': ['error', 'never'], + 'space-unary-ops': ['error', { words: true, nonwords: false }], + 'switch-colon-spacing': ['error'], + 'unicode-bom': ['error', 'never'], + 'wrap-iife': ['error'], + 'yoda': ['error'], + }, +} diff --git a/frontend/app.css b/frontend/app.css new file mode 100644 index 0000000..e89d82e --- /dev/null +++ b/frontend/app.css @@ -0,0 +1,20 @@ +.card-img-top { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + padding-bottom: 100%; +} +.faved, .faved:hover, .faved:active { + color: yellow; +} +.user-image { + border-radius: 1.25rem; + height: 36px; +} +.tweet { + max-height: 50px; + white-space: pre-line; +} +.toast, .toast-header { + background-color: rgba(34,34,34,0.85); +} diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..bb9a30c --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,123 @@ +function sortOrder(i, j) { + switch (true) { + case i < j: + return -1 + case j < i: + return 1 + default: + return 0 + } +} + +new Vue({ + computed: { + }, + + data: { + tweets: [], + modalTweet: null, + }, + + el: '#app', + + methods: { + callModal(tweet) { + this.modalTweet = tweet + }, + + favourite(tweet) { + axios + .post('/api/favourite', { id: tweet.id }) + .then(res => { + if (res.data.length === 0) { + this.refetch(tweet) + return + } + + this.upsertTweets(res.data) + }) + .catch(err => console.log(err)) + }, + + notify(text, title = 'MediaTimeline Viewer') { + this.$bvToast.toast(text, { + title, + autoHideDelay: 3000, + appendToast: true, + }) + }, + + refetch(tweet) { + axios + .post('/api/refresh', { id: tweet.id }) + .then(res => { + if (res.data.length === 0) { + return + } + + this.upsertTweets(res.data) + }) + .catch(err => console.log(err)) + }, + + refresh(forceReload = false) { + let apiURL = '/api/page' // By default query page 1 + if (this.tweets.length > 0 && !forceReload) { + apiURL = `/api/since?id=${this.tweets[0].id}` + } + + axios + .get(apiURL) + .then(resp => { + if (resp.data.length === 0) { + return + } + + this.upsertTweets(resp.data) + }) + .catch(err => { + console.log(err) + }) + }, + + triggerForceFetch() { + axios + .post('/api/force-reload') + .then(() => { + this.notify('Force refresh triggered, reloading tweets in 10s') + window.setTimeout(() => this.refresh(true), 10000) + }) + .catch(err => console.log(err)) + }, + + upsertTweets(data) { + let tweets = this.tweets + + for (const idx in data) { + const tweet = data[idx] + let inserted = false + + for (let i = 0; i < tweets.length; i++) { + if (tweets[i].id === tweet.id) { + tweets[i] = tweet + inserted = true + break + } + } + + if (!inserted) { + tweets = [...tweets, tweet] + } + } + + tweets.sort((j, i) => sortOrder(i.id, j.id)) + this.tweets = tweets + }, + }, + + mounted() { + this.refresh() + window.setInterval(this.refresh, 30000) + }, + +}) diff --git a/frontend/index.html b/frontend/index.html index e81a42e..73388dc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,123 +8,85 @@ MediaTimeline - - + - +
- + + + Force reload + + + -
+ -
+ -
-
+ +
-
-
- -
-
{{ tweet.user.screen_name }}
- {{ moment(tweet.posted).format('lll') }} -
-
+ + + -
- -
-
+ {{ tweet.images.length }} + + -
+ + - - -
+
@@ -137,144 +99,16 @@ + - - +