├── .gitignore
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── default_profile.png
├── favicon.ico
├── img
│ └── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── android-chrome-maskable-192x192.png
│ │ ├── android-chrome-maskable-512x512.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── msapplication-icon-144x144.png
│ │ ├── mstile-150x150.png
│ │ └── safari-pinned-tab.svg
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.vue
├── assets
│ ├── logo.png
│ └── tailwind.css
├── components
│ ├── DefaultRightBar.vue
│ ├── EditProfileOverlay.vue
│ ├── Loader.vue
│ ├── NewMessageOverlay.vue
│ ├── ReplyOverlay.vue
│ ├── Results.vue
│ ├── ResultsNewMessage.vue
│ ├── Retweet.vue
│ ├── SearchBar.vue
│ ├── SetUpProfileOverlay.vue
│ ├── SideNav.vue
│ ├── TrendingForYou.vue
│ ├── Tweet.vue
│ ├── Tweets.vue
│ ├── User.vue
│ ├── UserNewMessage.vue
│ ├── Users.vue
│ └── WhoToFollow.vue
├── directives
│ ├── escape.directive.js
│ ├── index.js
│ ├── linkify.directive.js
│ └── scroll.directive.js
├── filters
│ ├── index.js
│ ├── time.js
│ └── timeago.js
├── lib
│ └── backend.js
├── main.js
├── registerServiceWorker.js
├── router
│ ├── auth.guard.js
│ └── index.js
├── service-worker.js
├── store
│ ├── index.js
│ └── modules
│ │ ├── authentication
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ │ ├── signup
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ │ └── twitter
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
└── views
│ ├── Followers.vue
│ ├── Following.vue
│ ├── Hashtag.vue
│ ├── Home.vue
│ ├── LogIn.vue
│ ├── Messages.vue
│ ├── Notifications.vue
│ ├── Profile.vue
│ ├── Root.vue
│ └── Search.vue
├── tailwind.config.js
├── vue.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # appsyncmasterclass-frontend
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | yarn lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "appsyncmasterclass-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@tailwindcss/postcss7-compat": "^2.0.2",
12 | "autoprefixer": "^9",
13 | "aws-sdk": "^2.1195.0",
14 | "axios": "^0.21.4",
15 | "core-js": "^3.6.5",
16 | "graphql": "^16.0.0",
17 | "linkify-html": "^3.0.4",
18 | "linkify-plugin-hashtag": "^3.0.4",
19 | "linkify-plugin-mention": "^3.0.4",
20 | "linkifyjs": "^3.0.4",
21 | "moment": "^2.29.1",
22 | "postcss": "^7",
23 | "register-service-worker": "^1.7.1",
24 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
25 | "vue": "^2.6.11",
26 | "vue-router": "^3.2.0"
27 | },
28 | "devDependencies": {
29 | "@aws-amplify/ui-vue": "^0.2.33",
30 | "@vue/cli-plugin-babel": "~4.5.0",
31 | "@vue/cli-plugin-eslint": "~4.5.0",
32 | "@vue/cli-plugin-pwa": "~4.5.10",
33 | "@vue/cli-plugin-router": "~4.5.0",
34 | "@vue/cli-service": "~4.5.0",
35 | "aws-amplify": "^3.3.14",
36 | "babel-eslint": "^10.1.0",
37 | "eslint": "^6.7.2",
38 | "eslint-plugin-vue": "^6.2.2",
39 | "graphql-tag": "^2.11.0",
40 | "vue-cli-plugin-tailwind": "~2.0.5",
41 | "vue-template-compiler": "^2.6.11",
42 | "vuex": "^3.6.0"
43 | },
44 | "eslintConfig": {
45 | "root": true,
46 | "env": {
47 | "node": true
48 | },
49 | "extends": [
50 | "plugin:vue/essential",
51 | "eslint:recommended"
52 | ],
53 | "parserOptions": {
54 | "parser": "babel-eslint"
55 | },
56 | "rules": {}
57 | },
58 | "browserslist": [
59 | "> 1%",
60 | "last 2 versions",
61 | "not dead"
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/default_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/default_profile.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-maskable-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/android-chrome-maskable-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-maskable-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/android-chrome-maskable-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
12 |
13 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "appsyncmasterclass-frontend",
3 | "short_name": "appsyncmasterclass-frontend",
4 | "theme_color": "#4DBA87",
5 | "icons": [
6 | {
7 | "src": "./img/icons/android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "./img/icons/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "./img/icons/android-chrome-maskable-192x192.png",
18 | "sizes": "192x192",
19 | "type": "image/png",
20 | "purpose": "maskable"
21 | },
22 | {
23 | "src": "./img/icons/android-chrome-maskable-512x512.png",
24 | "sizes": "512x512",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | }
28 | ],
29 | "start_url": ".",
30 | "display": "standalone",
31 | "background_color": "#000000"
32 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theburningmonk/appsyncmasterclass-frontend/949c7ed18d6c05695a6e5e681576dbf1f2e7c219/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | html, body {height: 100%;}
7 |
8 | button:focus, input:focus {
9 | outline: none;
10 | }
11 |
12 | .modal-main {
13 | height: 40rem;
14 | }
15 |
16 | textarea {
17 | resize: none;
18 | }
19 |
20 | input[type="search"]::-webkit-search-cancel-button {
21 | -webkit-appearance: none;
22 | height: 1.6em;
23 | width: 1.6em;
24 | border-radius: 50em;
25 | background: url(https://pro.fontawesome.com/releases/v5.10.0/svgs/solid/times-circle.svg) no-repeat 50% 50%;
26 | background-size: contain;
27 | opacity: 0;
28 | pointer-events: none;
29 | }
30 |
31 | input[type="search"]:focus::-webkit-search-cancel-button {
32 | opacity: .2;
33 | pointer-events: all;
34 | }
35 |
36 | a:link, a {
37 | color: #1b95e0;
38 | }
39 |
40 | a:hover {
41 | text-decoration: underline;
42 | }
--------------------------------------------------------------------------------
/src/components/DefaultRightBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/EditProfileOverlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 | Save
10 |
11 |
12 |
13 |
Edit profile
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Name
49 |
50 |
51 |
52 | Bio
53 |
55 |
56 |
57 | Location
58 |
60 |
61 |
62 | Website
63 |
65 |
66 |
67 | Birth date
68 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/NewMessageOverlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
24 |
25 |
26 |
27 |
28 |
No results for "{{noResults}}"
29 |
The query you entered did not bring up any results.
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/ReplyOverlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
{{ tweet.profile.name }}
22 |
@{{ tweet.profile.screenName }}
23 |
.
24 |
{{ tweet.createdAt | timeago }}
25 |
26 |
27 | {{ tweet.text }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/Results.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/ResultsNewMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Retweet.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{ label }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/SearchBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/SetUpProfileOverlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 | Skip for now
11 |
12 |
15 | Next
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Pick a profile picture
23 |
24 |
Have a favorite selfie? Upload it now.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/SideNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Home
11 |
12 |
13 |
14 | Explore
15 |
16 |
17 |
18 |
19 |
20 | {{ this.newNotifications }}
21 |
22 |
23 | Notifications
24 |
25 |
26 |
27 |
28 |
29 | {{ this.newMessages }}
30 |
31 |
32 | Messages
33 |
34 |
35 | Bookmarks
36 |
37 |
38 |
39 | Lists
40 |
41 |
42 |
43 | Profile
44 |
45 |
46 | More
47 |
48 |
49 |
50 |
51 | Tweet
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
{{ profile.name }}
60 |
{{ profile.ScreenName }}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
{{ profile.name }}
69 |
{{ profile.ScreenName }}
70 |
71 |
72 |
73 |
74 | Add an existing account
75 |
76 |
77 | Log out {{ profile.ScreenName }}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/TrendingForYou.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Trends for you
5 |
6 |
7 |
8 |
9 |
{{ trend.top }}
10 |
{{ trend.title }}
11 |
{{ trend.bottom }}
12 |
13 |
14 |
15 |
16 | Show More
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Tweet.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
{{ tweet.profile.name }}
11 |
@{{ tweet.profile.screenName }}
12 |
·
13 |
{{ tweet.createdAt | timeago }}
14 |
15 |
16 |
17 | Replying to {{ tweet.inReplyToUsers.map(x => `@${x.screenName}`).join(",") }}
18 |
19 |
20 | {{ tweet.text }}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
{{ tweet.replies }}
28 |
29 |
30 |
31 |
32 |
33 |
{{ tweet.retweets }}
34 |
35 |
36 |
37 |
38 |
39 |
{{ tweet.likes }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/Tweets.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/components/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
{{ user.name }}
12 |
@{{ user.screenName }}
13 |
14 |
15 |
18 | Follow
19 |
20 |
25 | {{ followingLabel }}
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/UserNewMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{ user.name }}
10 |
@{{ user.screenName }} {{!user.followedBy?"cant'be messaged":''}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/Users.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/WhoToFollow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
{{ suggestion.name }}
10 |
{{ suggestion.screenName }}
11 |
12 |
13 | Follow
14 |
15 |
16 |
17 | Show More
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/directives/escape.directive.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind: function(el, binding) {
3 | if (typeof binding.value !== 'function') return;
4 | el.__callback__ = (event) => {
5 | if (event.keyCode === 27) {
6 | binding.value(event, el);
7 | }
8 | }
9 | el.addEventListener('keyup', el.__callback__);
10 | },
11 | unbind: function(el) {
12 | el.removeEventListener('keyup', el.__callback__);
13 | el.__callback__ = null;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/directives/index.js:
--------------------------------------------------------------------------------
1 | import escape from './escape.directive';
2 | import scroll from './scroll.directive';
3 | import linkify from './linkify.directive';
4 |
5 | export default {
6 | install(Vue) {
7 | Vue.directive('escape', escape);
8 | Vue.directive('scroll', scroll);
9 | Vue.directive('linkify', linkify);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/directives/linkify.directive.js:
--------------------------------------------------------------------------------
1 | import linkifyHtml from 'linkify-html';
2 | import 'linkify-plugin-hashtag';
3 | import 'linkify-plugin-mention';
4 |
5 | const options = {
6 | formatHref: {
7 | hashtag: (href) => `#/hashtag?m=Latest&hash=${Date.now()}&q=${escape('#' + href.substr(1))}`,
8 | mention: (href) => `#/${href.substr(1)}`
9 | },
10 | target: (href, type) => (type == 'mention' || type == 'hashtag') ? undefined : '_blank',
11 | truncate: 25,
12 | ignoreTags: ['script', 'style']
13 | }
14 |
15 | export default {
16 | bind: function (el) {
17 | let text = el.innerHTML;
18 | el.innerHTML = linkifyHtml(text, options);
19 | },
20 | }
--------------------------------------------------------------------------------
/src/directives/scroll.directive.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind: function (el, binding) {
3 | if (typeof binding.value !== 'function') return;
4 | el.__callback__ = (event) => {
5 | if (binding.arg == 'top') {
6 | if (!el) return;
7 | const isTop = el.scrollTop==0;
8 | if (!isTop) return;
9 | binding.value(event, el);
10 | } else if (binding.arg == 'bottom') {
11 | if (!el) return;
12 | const isBottom = Math.ceil(el.offsetHeight + el.scrollTop) >= el.scrollHeight;
13 | if (!isBottom) return;
14 | binding.value(event, el);
15 | } else {
16 | binding.value(event, el);
17 | }
18 | }
19 | el.addEventListener('scroll', el.__callback__);
20 | },
21 | unbind: function (el) {
22 | el.removeEventListener('scroll', el.__callback__);
23 | el.__callback__ = null;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/filters/index.js:
--------------------------------------------------------------------------------
1 | import timeago from './timeago';
2 | import time from './time';
3 |
4 | export default {
5 | install(Vue) {
6 | Vue.filter('timeago', timeago);
7 | Vue.filter('time', time);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/filters/time.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | moment.updateLocale('en', {
4 | monthsShort: [
5 | "Jan", "Feb", "Mar", "Apr", "May", "Jun",
6 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
7 | ],
8 | });
9 |
10 | export default function time(_date) {
11 | const date = moment(_date);
12 | const today = moment().startOf('day');
13 | const yesterday = moment().subtract(1, 'day').startOf('day');
14 |
15 | if (date <= yesterday) { // Jan 31, 6:30 PM
16 | return date.format("MMM D, h:mm A");
17 | } else if (date <= today) { // Yesterday, 6:30 PM
18 | return `Yesterday, ${date.format("h:mm A")}`;
19 | } else { // 6:30 PM
20 | return date.format("h:mm A");
21 | }
22 | }
--------------------------------------------------------------------------------
/src/filters/timeago.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | moment.updateLocale('en', {
4 | monthsShort: [
5 | "Jan", "Feb", "Mar", "Apr", "May", "Jun",
6 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
7 | ],
8 | relativeTime: {
9 | past: '%ss',
10 | s: 'Now',
11 | ss: '%ss',
12 | m: '%dm',
13 | mm: '%dm',
14 | h: '%dh',
15 | hh: '%dh',
16 | }
17 | });
18 |
19 | export default function timeago(date) {
20 | if (moment().diff(date, "days") == 0) {
21 | return moment(date).fromNow(true); // Now, 15s, 5m, 3h
22 | } else {
23 | return moment(date).format("MMM D"); // Jan 31
24 | }
25 | }
--------------------------------------------------------------------------------
/src/lib/backend.js:
--------------------------------------------------------------------------------
1 | import { API } from 'aws-amplify'
2 | import gql from 'graphql-tag'
3 |
4 | const getMyProfile = async () => {
5 | const result = await API.graphql({
6 | query: gql`
7 | query getMyProfile {
8 | getMyProfile {
9 | id
10 | name
11 | screenName
12 | imageUrl
13 | backgroundImageUrl
14 | bio
15 | location
16 | website
17 | birthdate
18 | createdAt
19 | followersCount
20 | followingCount
21 | tweetsCount
22 | likesCounts
23 | }
24 | }
25 | `,
26 | authMode: "AMAZON_COGNITO_USER_POOLS"
27 | })
28 | const profile = result.data.getMyProfile
29 |
30 | profile.imageUrl = profile.imageUrl || 'default_profile.png'
31 | return profile
32 | }
33 |
34 | const getProfileByScreenName = async (screenName) => {
35 | const result = await API.graphql({
36 | query: gql`
37 | query getProfile($screenName: String!) {
38 | getProfile(screenName: $screenName) {
39 | id
40 | name
41 | screenName
42 | imageUrl
43 | backgroundImageUrl
44 | bio
45 | location
46 | website
47 | birthdate
48 | createdAt
49 | followersCount
50 | followingCount
51 | tweetsCount
52 | likesCounts
53 | following
54 | followedBy
55 | }
56 | }
57 | `,
58 | variables: {
59 | screenName
60 | },
61 | authMode: "AMAZON_COGNITO_USER_POOLS"
62 | })
63 | const profile = result.data.getProfile || {};
64 |
65 | profile.imageUrl = profile.imageUrl || 'default_profile.png'
66 | return profile
67 | }
68 |
69 | const getMyTimeline = async (limit, nextToken) => {
70 | const result = await API.graphql({
71 | query: gql`
72 | query getMyTimeline($limit:Int!, $nextToken:String) {
73 | getMyTimeline(limit: $limit, nextToken: $nextToken) {
74 | nextToken
75 | tweets {
76 | __typename
77 | id
78 | profile {
79 | id
80 | name
81 | screenName
82 | imageUrl
83 | }
84 | createdAt
85 | ... on Tweet {
86 | text
87 | liked
88 | likes
89 | retweeted
90 | retweets
91 | replies
92 | }
93 | ... on Retweet {
94 | retweetOf {
95 | id
96 | profile {
97 | id
98 | name
99 | screenName
100 | imageUrl
101 | }
102 | createdAt
103 | ... on Tweet {
104 | text
105 | liked
106 | likes
107 | retweeted
108 | retweets
109 | replies
110 | }
111 | ... on Reply {
112 | text
113 | liked
114 | likes
115 | retweeted
116 | retweets
117 | replies
118 | }
119 | }
120 | }
121 | ... on Reply {
122 | text
123 | liked
124 | likes
125 | retweeted
126 | retweets
127 | replies
128 | inReplyToTweet {
129 | id
130 | profile {
131 | id
132 | name
133 | screenName
134 | imageUrl
135 | }
136 | createdAt
137 | ... on Tweet {
138 | text
139 | liked
140 | likes
141 | retweeted
142 | retweets
143 | replies
144 | }
145 | ... on Reply {
146 | text
147 | liked
148 | likes
149 | retweeted
150 | retweets
151 | replies
152 | }
153 | ... on Retweet {
154 | retweetOf {
155 | id
156 | }
157 | }
158 | }
159 | inReplyToUsers {
160 | id
161 | name
162 | screenName
163 | imageUrl
164 | }
165 | }
166 | }
167 | }
168 | }
169 | `,
170 | variables: {
171 | limit,
172 | nextToken
173 | },
174 | authMode: "AMAZON_COGNITO_USER_POOLS"
175 | })
176 |
177 | return result.data.getMyTimeline
178 | }
179 |
180 | const getTweets = async (userId, limit, nextToken) => {
181 | const result = await API.graphql({
182 | query: gql`
183 | query getTweets($userId:ID!, $limit:Int!, $nextToken:String) {
184 | getTweets(userId:$userId, limit:$limit, nextToken: $nextToken) {
185 | nextToken
186 | tweets {
187 | ... on Tweet {
188 | id
189 | createdAt
190 | text
191 | liked
192 | likes
193 | retweeted
194 | retweets
195 | replies
196 | profile {
197 | id
198 | name
199 | screenName
200 | imageUrl
201 | }
202 | }
203 | ... on Reply {
204 | text
205 | liked
206 | likes
207 | retweeted
208 | retweets
209 | replies
210 | inReplyToTweet {
211 | id
212 | profile {
213 | id
214 | name
215 | screenName
216 | imageUrl
217 | }
218 | createdAt
219 | ... on Tweet {
220 | text
221 | liked
222 | likes
223 | retweeted
224 | retweets
225 | replies
226 | }
227 | ... on Reply {
228 | text
229 | liked
230 | likes
231 | retweeted
232 | retweets
233 | replies
234 | }
235 | }
236 | inReplyToUsers {
237 | id
238 | name
239 | screenName
240 | imageUrl
241 | }
242 | }
243 | }
244 | }
245 | }
246 | `,
247 | variables: {
248 | userId,
249 | limit,
250 | nextToken
251 | },
252 | authMode: "AMAZON_COGNITO_USER_POOLS"
253 | })
254 |
255 | return result.data.getTweets
256 | }
257 |
258 | const getImageUploadUrl = async (extension, contentType) => {
259 | const result = await API.graphql({
260 | query: gql`
261 | query getImageUploadUrl($extension:String, $contentType:String) {
262 | getImageUploadUrl(extension:$extension, contentType:$contentType)
263 | }
264 | `,
265 | variables: {
266 | extension,
267 | contentType
268 | },
269 | authMode: "AMAZON_COGNITO_USER_POOLS"
270 | })
271 |
272 | return result.data.getImageUploadUrl
273 | }
274 |
275 | const editMyProfile = async (newProfile) => {
276 | const result = await API.graphql({
277 | query: gql`
278 | mutation editMyProfile($newProfile:ProfileInput!) {
279 | editMyProfile(newProfile: $newProfile) {
280 | backgroundImageUrl
281 | bio
282 | createdAt
283 | birthdate
284 | followersCount
285 | followingCount
286 | id
287 | imageUrl
288 | likesCounts
289 | location
290 | name
291 | screenName
292 | tweetsCount
293 | website
294 | }
295 | }
296 | `,
297 | variables: {
298 | newProfile
299 | },
300 | authMode: "AMAZON_COGNITO_USER_POOLS"
301 | })
302 | return result.data.editMyProfile
303 | }
304 |
305 | const tweet = async (text) => {
306 | const result = await API.graphql({
307 | query: gql`
308 | mutation tweet($text:String!) {
309 | tweet(text: $text) {
310 | createdAt
311 | id
312 | liked
313 | likes
314 | profile {
315 | imageUrl
316 | name
317 | screenName
318 | }
319 | replies
320 | retweeted
321 | retweets
322 | text
323 | }
324 | }
325 | `,
326 | variables: {
327 | text
328 | },
329 | authMode: "AMAZON_COGNITO_USER_POOLS"
330 | })
331 | return result.data.tweet
332 | }
333 |
334 | const retweet = async (tweetId) => {
335 | await API.graphql({
336 | query: gql`
337 | mutation retweet($tweetId:ID!) {
338 | retweet(tweetId: $tweetId) {
339 | id
340 | createdAt
341 | }
342 | }
343 | `,
344 | variables: {
345 | tweetId
346 | },
347 | authMode: "AMAZON_COGNITO_USER_POOLS"
348 | })
349 | }
350 |
351 | const unretweet = async (tweetId) => {
352 | await API.graphql({
353 | query: gql`
354 | mutation unretweet($tweetId:ID!) {
355 | unretweet(tweetId: $tweetId)
356 | }
357 | `,
358 | variables: {
359 | tweetId
360 | },
361 | authMode: "AMAZON_COGNITO_USER_POOLS"
362 | })
363 | }
364 |
365 | const like = async (tweetId) => {
366 | await API.graphql({
367 | query: gql`
368 | mutation like($tweetId:ID!) {
369 | like(tweetId: $tweetId)
370 | }
371 | `,
372 | variables: {
373 | tweetId
374 | },
375 | authMode: "AMAZON_COGNITO_USER_POOLS"
376 | })
377 | }
378 |
379 | const unlike = async (tweetId) => {
380 | await API.graphql({
381 | query: gql`
382 | mutation unlike($tweetId:ID!) {
383 | unlike(tweetId: $tweetId)
384 | }
385 | `,
386 | variables: {
387 | tweetId
388 | },
389 | authMode: "AMAZON_COGNITO_USER_POOLS"
390 | })
391 | }
392 |
393 | const reply = async (tweetId, text) => {
394 | const result = await API.graphql({
395 | query: gql`
396 | mutation reply($tweetId:ID!, $text:String!) {
397 | reply(tweetId: $tweetId, text: $text) {
398 | id
399 | createdAt
400 | liked
401 | likes
402 | profile {
403 | imageUrl
404 | name
405 | screenName
406 | }
407 | replies
408 | retweeted
409 | retweets
410 | text
411 | }
412 | }
413 | `,
414 | variables: {
415 | tweetId,
416 | text
417 | },
418 | authMode: "AMAZON_COGNITO_USER_POOLS"
419 | })
420 |
421 | return result.data.reply
422 | }
423 |
424 | const follow = async (userId) => {
425 | await API.graphql({
426 | query: gql`
427 | mutation follow($userId:ID!) {
428 | follow(userId: $userId)
429 | }
430 | `,
431 | variables: {
432 | userId
433 | },
434 | authMode: "AMAZON_COGNITO_USER_POOLS"
435 | })
436 | }
437 |
438 | const unfollow = async (userId) => {
439 | await API.graphql({
440 | query: gql`
441 | mutation unfollow($userId:ID!) {
442 | unfollow(userId: $userId)
443 | }
444 | `,
445 | variables: {
446 | userId
447 | },
448 | authMode: "AMAZON_COGNITO_USER_POOLS"
449 | })
450 | }
451 |
452 | const getFollowers = async (userId, limit = 10) => {
453 | const result = await API.graphql({
454 | query: gql`
455 | query getFollowers($userId:ID!, $limit:Int!) {
456 | getFollowers(userId: $userId, limit: $limit) {
457 | profiles {
458 | id
459 | name
460 | screenName
461 | imageUrl
462 | bio
463 | ... on OtherProfile {
464 | following
465 | followedBy
466 | }
467 | },
468 | nextToken
469 | }
470 | }
471 | `,
472 | variables: {
473 | userId,
474 | limit
475 | },
476 | authMode: "AMAZON_COGNITO_USER_POOLS"
477 | })
478 |
479 | return result.data.getFollowers
480 | }
481 |
482 | const getFollowing = async (userId, limit = 10) => {
483 | const result = await API.graphql({
484 | query: gql`
485 | query getFollowing($userId:ID!, $limit:Int!) {
486 | getFollowing(userId: $userId, limit: $limit) {
487 | profiles {
488 | id
489 | name
490 | screenName
491 | imageUrl
492 | bio
493 | ... on OtherProfile {
494 | following
495 | followedBy
496 | }
497 | },
498 | nextToken
499 | }
500 | }
501 | `,
502 | variables: {
503 | userId,
504 | limit
505 | },
506 | authMode: "AMAZON_COGNITO_USER_POOLS"
507 | })
508 |
509 | return result.data.getFollowing
510 | }
511 |
512 | const search = async (query, mode, limit, nextToken) => {
513 | const result = await API.graphql({
514 | query: gql`
515 | query search($query: String!, $mode: SearchMode!, $limit: Int!, $nextToken: String) {
516 | search(query: $query, mode: $mode, limit: $limit, nextToken: $nextToken) {
517 | nextToken
518 | results {
519 | __typename
520 | ... on Tweet {
521 | id
522 | createdAt
523 | text
524 | liked
525 | likes
526 | retweeted
527 | retweets
528 | replies
529 | profile {
530 | id
531 | name
532 | screenName
533 | imageUrl
534 | }
535 | }
536 | ... on Reply {
537 | id
538 | text
539 | liked
540 | likes
541 | retweeted
542 | retweets
543 | replies
544 | profile {
545 | id
546 | name
547 | screenName
548 | imageUrl
549 | }
550 | inReplyToTweet {
551 | id
552 | profile {
553 | id
554 | name
555 | screenName
556 | imageUrl
557 | }
558 | createdAt
559 | ... on Tweet {
560 | text
561 | liked
562 | likes
563 | retweeted
564 | retweets
565 | replies
566 | }
567 | ... on Reply {
568 | text
569 | liked
570 | likes
571 | retweeted
572 | retweets
573 | replies
574 | }
575 | }
576 | inReplyToUsers {
577 | id
578 | name
579 | screenName
580 | imageUrl
581 | }
582 | }
583 | ... on OtherProfile {
584 | id
585 | name
586 | screenName
587 | imageUrl
588 | bio
589 | following
590 | followedBy
591 | }
592 | ... on MyProfile {
593 | id
594 | name
595 | screenName
596 | imageUrl
597 | bio
598 | }
599 | }
600 | }
601 | }
602 | `,
603 | variables: {
604 | query,
605 | mode,
606 | limit,
607 | nextToken
608 | },
609 | authMode: "AMAZON_COGNITO_USER_POOLS"
610 | })
611 |
612 | return result.data.search;
613 | }
614 |
615 | const getHashTag = async (hashTag, mode, limit, nextToken) => {
616 | const result = await API.graphql({
617 | query: gql`
618 | query getHashTag($hashTag: String!, $mode: HashTagMode!, $limit: Int!, $nextToken: String) {
619 | getHashTag(hashTag: $hashTag, mode: $mode, limit: $limit, nextToken: $nextToken) {
620 | nextToken
621 | results {
622 | __typename
623 | ... on Tweet {
624 | id
625 | createdAt
626 | text
627 | liked
628 | likes
629 | retweeted
630 | retweets
631 | replies
632 | profile {
633 | id
634 | name
635 | screenName
636 | imageUrl
637 | }
638 | }
639 | ... on Reply {
640 | id
641 | text
642 | liked
643 | likes
644 | retweeted
645 | retweets
646 | replies
647 | profile {
648 | id
649 | name
650 | screenName
651 | imageUrl
652 | }
653 | inReplyToTweet {
654 | id
655 | profile {
656 | id
657 | name
658 | screenName
659 | imageUrl
660 | }
661 | createdAt
662 | ... on Tweet {
663 | text
664 | liked
665 | likes
666 | retweeted
667 | retweets
668 | replies
669 | }
670 | ... on Reply {
671 | text
672 | liked
673 | likes
674 | retweeted
675 | retweets
676 | replies
677 | }
678 | }
679 | inReplyToUsers {
680 | id
681 | name
682 | screenName
683 | imageUrl
684 | }
685 | }
686 | ... on OtherProfile {
687 | id
688 | name
689 | screenName
690 | imageUrl
691 | bio
692 | following
693 | followedBy
694 | }
695 | ... on MyProfile {
696 | id
697 | name
698 | screenName
699 | imageUrl
700 | bio
701 | }
702 | }
703 | }
704 | }
705 | `,
706 | variables: {
707 | hashTag,
708 | mode,
709 | limit,
710 | nextToken
711 | },
712 | authMode: "AMAZON_COGNITO_USER_POOLS"
713 | })
714 |
715 | return result.data.getHashTag;
716 | }
717 |
718 | const getOnNotifiedSubscription = (userId) => {
719 | const onNotified = {
720 | query: gql`
721 | subscription onNotified ($userId: ID!) {
722 | onNotified(userId: $userId) {
723 | ... on Retweeted {
724 | id
725 | createdAt
726 | retweetedBy
727 | tweetId
728 | retweetId
729 | type
730 | }
731 | ... on Liked {
732 | id
733 | createdAt
734 | likedBy
735 | tweetId
736 | type
737 | }
738 | ... on Mentioned {
739 | id
740 | createdAt
741 | mentionedBy
742 | mentionedByTweetId
743 | type
744 | }
745 | ... on Replied {
746 | id
747 | createdAt
748 | repliedBy
749 | tweetId
750 | replyTweetId
751 | type
752 | }
753 | ... on DMed {
754 | id
755 | message
756 | createdAt
757 | otherUserId
758 | type
759 | }
760 | }
761 | }`,
762 | variables: {
763 | userId: userId
764 | }
765 | };
766 |
767 | return API.graphql(onNotified);
768 | }
769 |
770 | const listConversations = async (limit, nextToken) => {
771 | const result = await API.graphql({
772 | query: gql`
773 | query listConversations($limit: Int!, $nextToken: String) {
774 | listConversations(
775 | limit: $limit
776 | nextToken: $nextToken
777 | ) {
778 | conversations {
779 | id
780 | otherUser {
781 | id
782 | name
783 | screenName
784 | imageUrl
785 | backgroundImageUrl
786 | bio
787 | location
788 | website
789 | birthdate
790 | createdAt
791 | followersCount
792 | followingCount
793 | tweetsCount
794 | likesCounts
795 | following
796 | followedBy
797 | }
798 | lastMessage
799 | lastModified
800 | }
801 | nextToken
802 | }
803 | }
804 | `,
805 | variables: {
806 | limit,
807 | nextToken
808 | },
809 | authMode: "AMAZON_COGNITO_USER_POOLS"
810 | })
811 |
812 | return result.data.listConversations;
813 | }
814 |
815 | const getDirectMessages = async (otherUserId, limit, nextToken) => {
816 | const result = await API.graphql({
817 | query: gql`
818 | query getDirectMessages($otherUserId: ID!, $limit: Int!, $nextToken: String) {
819 | getDirectMessages(
820 | otherUserId: $otherUserId
821 | limit: $limit
822 | nextToken: $nextToken
823 | ) {
824 | messages {
825 | messageId
826 | message
827 | timestamp
828 | from {
829 | imageUrl
830 | screenName
831 | }
832 | }
833 | nextToken
834 | }
835 | }
836 | `,
837 | variables: {
838 | otherUserId,
839 | limit,
840 | nextToken
841 | },
842 | authMode: "AMAZON_COGNITO_USER_POOLS"
843 | })
844 |
845 | return result.data.getDirectMessages;
846 | }
847 |
848 | const sendDirectMessage = async (message, otherUserId) => {
849 | const result = await API.graphql({
850 | query: gql`
851 | mutation sendDirectMessage($message: String!, $otherUserId: ID!) {
852 | sendDirectMessage(
853 | message: $message
854 | otherUserId: $otherUserId
855 | ) {
856 | id
857 | message:lastMessage
858 | lastModified
859 | otherUser {
860 | name
861 | screenName
862 | imageUrl
863 | }
864 | }
865 | }
866 | `,
867 | variables: {
868 | message,
869 | otherUserId,
870 | },
871 | authMode: "AMAZON_COGNITO_USER_POOLS"
872 | })
873 |
874 | return result.data.sendDirectMessage;
875 | }
876 |
877 | export {
878 | getMyProfile,
879 | getProfileByScreenName,
880 | getMyTimeline,
881 | getTweets,
882 | getImageUploadUrl,
883 | editMyProfile,
884 | tweet,
885 | retweet,
886 | unretweet,
887 | like,
888 | unlike,
889 | reply,
890 | follow,
891 | unfollow,
892 | getFollowers,
893 | getFollowing,
894 | search,
895 | getHashTag,
896 | getOnNotifiedSubscription,
897 | listConversations,
898 | getDirectMessages,
899 | sendDirectMessage,
900 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import './assets/tailwind.css'
4 | import Amplify from 'aws-amplify';
5 | import '@aws-amplify/ui-vue';
6 | import router from './router';
7 | import store from './store';
8 | import directives from './directives';
9 | import filters from './filters';
10 | import './registerServiceWorker'
11 |
12 | Amplify.configure({
13 | Auth: {
14 | region: process.env.VUE_APP_AUTH_REGION,
15 | userPoolId: process.env.VUE_APP_AUTH_USER_POOL_ID,
16 | userPoolWebClientId: process.env.VUE_APP_AUTH_USER_POOL_WEB_CLIENT_ID,
17 | mandatorySignIn: true
18 | }
19 | })
20 |
21 | const myAppConfig = {
22 | 'aws_appsync_graphqlEndpoint': process.env.VUE_APP_AWS_APPSYNC_GRAPHQL_ENDPOINT,
23 | 'aws_appsync_region': process.env.VUE_APP_AWS_APPSYNC_REGION,
24 | 'aws_appsync_authenticationType': process.env.VUE_APP_AWS_APPSYNC_AUTHENTICATION_TYPE
25 | }
26 |
27 | Amplify.configure(myAppConfig);
28 |
29 | Vue.config.productionTip = false
30 |
31 | Vue.use(directives);
32 | Vue.use(filters);
33 |
34 | new Vue({
35 | router,
36 | store,
37 | render: h => h(App)
38 | }).$mount('#app')
39 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from 'register-service-worker'
4 |
5 | if (process.env.NODE_ENV === 'production') {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready () {
8 | console.log(
9 | 'App is being served from cache by a service worker.\n' +
10 | 'For more details, visit https://goo.gl/AFskqB'
11 | )
12 | },
13 | registered () {
14 | console.log('Service worker has been registered.')
15 | },
16 | cached () {
17 | console.log('Content has been cached for offline use.')
18 | },
19 | updatefound () {
20 | console.log('New content is downloading.')
21 | },
22 | updated () {
23 | console.log('New content is available; please refresh.')
24 | },
25 | offline () {
26 | console.log('No internet connection found. App is running in offline mode.')
27 | },
28 | error (error) {
29 | console.error('Error during service worker registration:', error)
30 | }
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/src/router/auth.guard.js:
--------------------------------------------------------------------------------
1 | import { Auth } from 'aws-amplify';
2 |
3 | export default async (to, from, next) => {
4 | const isProtected = to.matched.some(route => route.meta.protected);
5 | const loggedIn = await Auth.currentUserInfo();
6 | if (isProtected && !loggedIn) {
7 | next("/");
8 | return;
9 | }
10 | next();
11 | }
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import Root from '../views/Root.vue'
4 | import AuthMiddleware from './auth.guard'
5 |
6 | // avoid NavigationDuplicated
7 | // https://github.com/vuejs/vue-router/issues/2881#issuecomment-520554378
8 | const originalPush = VueRouter.prototype.push
9 | VueRouter.prototype.push = function push(location, onResolve, onReject) {
10 | if (onResolve || onReject)
11 | return originalPush.call(this, location, onResolve, onReject)
12 | return originalPush.call(this, location).catch((err) => {
13 | if (VueRouter.isNavigationFailure(err)) {
14 | // resolve err
15 | return err
16 | }
17 | // rethrow error
18 | return Promise.reject(err)
19 | })
20 | }
21 |
22 | Vue.use(VueRouter)
23 |
24 | const routes = [
25 | {
26 | path: '/',
27 | name: 'Root',
28 | component: Root
29 | },
30 | {
31 | path: '/login',
32 | name: 'LogIn',
33 | // route level code-splitting
34 | // this generates a separate chunk (about.[hash].js) for this route
35 | // which is lazy-loaded when the route is visited.
36 | component: () => import(/* webpackChunkName: "login" */ '../views/LogIn.vue')
37 | },
38 | {
39 | path: '/home',
40 | name: 'Home',
41 | // route level code-splitting
42 | // this generates a separate chunk (about.[hash].js) for this route
43 | // which is lazy-loaded when the route is visited.
44 | component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue'),
45 | meta: { protected: true }
46 | },
47 | {
48 | path: '/search',
49 | name: 'Search',
50 | // route level code-splitting
51 | // this generates a separate chunk (about.[hash].js) for this route
52 | // which is lazy-loaded when the route is visited.
53 | component: () => import(/* webpackChunkName: "search" */ '../views/Search.vue'),
54 | meta: { protected: true }
55 | },
56 | {
57 | path: '/hashtag',
58 | name: 'Hashtag',
59 | // route level code-splitting
60 | // this generates a separate chunk (about.[hash].js) for this route
61 | // which is lazy-loaded when the route is visited.
62 | component: () => import(/* webpackChunkName: "hashtag" */ '../views/Hashtag.vue'),
63 | meta: { protected: true }
64 | },
65 | {
66 | path: '/notifications',
67 | name: 'Notifications',
68 | // route level code-splitting
69 | // this generates a separate chunk (about.[hash].js) for this route
70 | // which is lazy-loaded when the route is visited.
71 | component: () => import(/* webpackChunkName: "notifications" */ '../views/Notifications.vue'),
72 | meta: { protected: true }
73 | },
74 | {
75 | path: '/messages',
76 | name: 'Messages',
77 | // route level code-splitting
78 | // this generates a separate chunk (about.[hash].js) for this route
79 | // which is lazy-loaded when the route is visited.
80 | component: () => import(/* webpackChunkName: "messages" */ '../views/Messages.vue'),
81 | props: true,
82 | meta: { protected: true }
83 | },
84 | {
85 | path: '/:screenName',
86 | name: 'Profile',
87 | // route level code-splitting
88 | // this generates a separate chunk (about.[hash].js) for this route
89 | // which is lazy-loaded when the route is visited.
90 | component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
91 | meta: { protected: true }
92 | },
93 | {
94 | path: '/:screenName/followers',
95 | name: 'Followers',
96 | // route level code-splitting
97 | // this generates a separate chunk (about.[hash].js) for this route
98 | // which is lazy-loaded when the route is visited.
99 | component: () => import(/* webpackChunkName: "followers" */ '../views/Followers.vue'),
100 | props: true,
101 | meta: { protected: true }
102 | },
103 | {
104 | path: '/:screenName/following',
105 | name: 'Following',
106 | // route level code-splitting
107 | // this generates a separate chunk (about.[hash].js) for this route
108 | // which is lazy-loaded when the route is visited.
109 | component: () => import(/* webpackChunkName: "following" */ '../views/Following.vue'),
110 | props: true,
111 | meta: { protected: true }
112 | },
113 | ]
114 |
115 | const router = new VueRouter({
116 | routes
117 | })
118 |
119 | router.beforeEach(AuthMiddleware)
120 |
121 | export default router
122 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | workbox.core.setCacheNameDetails({ prefix: "appsyncmasterclass-frontend" });
2 |
3 | workbox.core.skipWaiting()
4 | workbox.core.clientsClaim()
5 |
6 | const cacheFiles = [
7 | {
8 | "revision": "931caf57a56b47ef400c",
9 | "url": "/img/icons/android-chrome-192x192.png"
10 | },
11 | {
12 | "revision": "81b4ff3d9ff09591cc33",
13 | "url": "/img/icons/favicon-16x16.png"
14 | },
15 | {
16 | "revision": "29372bb30481defdc33d",
17 | "url": "/img/icons/favicon-32x32.png"
18 | },
19 | {
20 | "revision": "e653ab4d124bf16b5232",
21 | "url": "https://ka-f.fontawesome.com/releases/v5.15.4/css/free.min.css?token=11146ec54d"
22 | },
23 | {
24 | "revision": "e653ab4d124bf16b5232",
25 | "url": "https://ka-f.fontawesome.com/releases/v5.15.4/css/free-v4-shims.min.css?token=11146ec54d"
26 | },
27 | {
28 | "revision": "e653ab4d124bf16b5232",
29 | "url": "https://ka-f.fontawesome.com/releases/v5.15.4/css/free-v4-font-face.min.css?token=11146ec54d"
30 | },
31 | {
32 | "revision": "66b946627566f0b6fa10",
33 | "url": "service-worker.js"
34 | }
35 | ]
36 |
37 | self.__precacheManifest = cacheFiles.concat(self.__precacheManifest || [])
38 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {})
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import authentication from './modules/authentication';
4 | import signup from './modules/signup';
5 | import twitter from './modules/twitter';
6 | import profilePage from './modules/twitter';
7 |
8 | Vue.use(Vuex);
9 |
10 | export default new Vuex.Store({
11 | modules: {
12 | authentication,
13 | signup,
14 | twitter,
15 | profilePage
16 | }
17 | })
--------------------------------------------------------------------------------
/src/store/modules/authentication/actions.js:
--------------------------------------------------------------------------------
1 | import { Auth } from 'aws-amplify'
2 | import AWS from 'aws-sdk'
3 | import router from '../../../router'
4 |
5 | AWS.config.region = process.env.VUE_APP_AUTH_REGION;
6 | const IDENTITY_POOL_ID = process.env.VUE_APP_AUTH_IDENTITY_POOL_ID;
7 | const STREAM_NAME = process.env.VUE_APP_AWS_KINESIS_FIREHOSE_DELIVERY_STREAM_NAME;
8 |
9 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({
10 | IdentityPoolId: IDENTITY_POOL_ID
11 | })
12 |
13 | const FirehoseClient = new AWS.Firehose();
14 |
15 | export default {
16 | loginUser({ commit }, user) {
17 | commit("USER_LOGIN", user);
18 | },
19 | async logoutUser({ commit, dispatch }) {
20 | await Auth.signOut({
21 | global: true
22 | })
23 | commit("USER_LOGOUT");
24 | dispatch("signup/setSignupStep", '', { root: true });
25 | dispatch("twitter/unsubscribeNotifications", null, { root: true });
26 | dispatch("twitter/resetState", null, { root: true });
27 | dispatch("profilePage/resetState", null, { root: true });
28 | router.push('/')
29 | },
30 | async signUp({ commit }, form) {
31 | const user = await Auth.signUp({
32 | username: form.username,
33 | password: form.password,
34 | attributes: {
35 | name: form.name,
36 | }
37 | });
38 | commit("USER_SIGNUP", user);
39 | },
40 | async confirmSignUp(_, form) {
41 | await Auth.confirmSignUp(form.email, form.verificationCode)
42 | },
43 | async resendSignUp(_, form) {
44 | await Auth.resendSignUp(form.email);
45 | },
46 |
47 | async signInUser({ dispatch }, form) {
48 | const user = await Auth.signIn(form.email, form.password);
49 | await dispatch("loginUser", user);
50 | await dispatch("twitter/setProfile", null, { root: true });
51 | await dispatch("twitter/subscribeNotifications", null, { root: true });
52 | router.push({ name: 'Home' });
53 | },
54 |
55 | async loginUserIfAlreadyAuthenticated({ dispatch }) {
56 | const user = await Auth.currentUserInfo();
57 | if (user) {
58 | console.log('user is logged in already')
59 | await dispatch("loginUser", user);
60 | await dispatch("twitter/setProfile", null, { root: true });
61 | await dispatch("twitter/subscribeNotifications", null, { root: true });
62 | }
63 | },
64 |
65 | async trackEvent(_, event) {
66 | const response = await FirehoseClient.putRecord({
67 | DeliveryStreamName: STREAM_NAME,
68 | Record: {
69 | Data: JSON.stringify(event)
70 | }
71 | }).promise();
72 | console.log(response);
73 | }
74 | };
--------------------------------------------------------------------------------
/src/store/modules/authentication/getters.js:
--------------------------------------------------------------------------------
1 | export default {
2 | loggedIn: state => state.loggedIn,
3 | user: state => state.user
4 | }
--------------------------------------------------------------------------------
/src/store/modules/authentication/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions'
2 | import mutations from './mutations'
3 | import getters from './getters'
4 |
5 | const state = () => ({
6 | loggedIn: false,
7 | user: undefined,
8 | });
9 |
10 | export default {
11 | namespaced: true,
12 | actions,
13 | mutations,
14 | getters,
15 | state,
16 | }
--------------------------------------------------------------------------------
/src/store/modules/authentication/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | USER_LOGIN(state, user) {
3 | state.user = user;
4 | state.loggedIn = true;
5 | },
6 | USER_LOGOUT(state) {
7 | state.loggedIn = false;
8 | state.user = undefined;
9 | },
10 | USER_SIGNUP(state, user) {
11 | state.user = user;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/store/modules/signup/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setSignupStep({ commit }, step) {
3 | commit("SIGNUP_STEP_SET", step);
4 | },
5 | }
--------------------------------------------------------------------------------
/src/store/modules/signup/getters.js:
--------------------------------------------------------------------------------
1 | export default {
2 | showModal: state => state.step,
3 | }
--------------------------------------------------------------------------------
/src/store/modules/signup/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions';
2 | import mutations from './mutations';
3 | import getters from './getters';
4 |
5 | const state = () => ({
6 | step: ''
7 | });
8 |
9 | export default {
10 | namespaced: true,
11 | actions,
12 | mutations,
13 | getters,
14 | state,
15 | }
--------------------------------------------------------------------------------
/src/store/modules/signup/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | SIGNUP_STEP_SET(state, step) {
3 | state.step = step;
4 | },
5 | }
--------------------------------------------------------------------------------
/src/store/modules/twitter/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | getMyProfile, getProfileByScreenName, getImageUploadUrl, editMyProfile,
3 | getMyTimeline, tweet, getTweets,
4 | like, unlike, retweet, unretweet, reply,
5 | follow, unfollow,
6 | getFollowers, getFollowing,
7 | search,
8 | getHashTag,
9 | getOnNotifiedSubscription,
10 | listConversations,
11 | getDirectMessages,
12 | sendDirectMessage
13 | } from '../../../lib/backend'
14 |
15 | export default {
16 | async setProfile({ commit }) {
17 | const profile = await getMyProfile();
18 | commit("PROFILE_SET", profile);
19 | },
20 |
21 | async loadProfile({ commit, rootState }, screenName) {
22 | if (!screenName) return;
23 | if (rootState.twitter.profile.screenName == screenName) {
24 | const profile = await getMyProfile();
25 | commit("PROFILE_SET", profile);
26 | } else {
27 | const profile = await getProfileByScreenName(screenName);
28 | commit("PROFILE_SET", profile);
29 | }
30 | },
31 |
32 | async loadMyTimeline({ dispatch }) {
33 | await dispatch("getMyTimeline", 10);
34 | },
35 |
36 | async loadTweets({ dispatch, rootState }, screenName) {
37 | if (!screenName) return;
38 |
39 | if (rootState.twitter.profile.screenName == screenName) {
40 | await dispatch("getTweets", { userId: rootState.twitter.profile.id, limit: 10 });
41 | } else {
42 | const profile = await getProfileByScreenName(screenName);
43 | await dispatch("getTweets", { userId: profile.id, limit: 10 });
44 | }
45 | },
46 |
47 | async getMyTimeline({ commit }, limit) {
48 | const timeline = await getMyTimeline(limit);
49 | commit("TWITTER_TIMELINE", timeline);
50 | },
51 | async createTweet({ commit, dispatch }, { text }) {
52 | const newTweet = await tweet(text);
53 | commit("TWITTER_CREATE", newTweet);
54 | await dispatch("getMyTimeline", 10);
55 | },
56 |
57 | async getTweets({ commit }, { userId, limit, nextToken }) {
58 | const tweets = await getTweets(userId, limit, nextToken);
59 | commit("TWITTER_TIMELINE", tweets);
60 | },
61 |
62 | async likeTweet(_, tweetId) {
63 | await like(tweetId);
64 | },
65 | async unlikeTweet(_, tweetId) {
66 | await unlike(tweetId);
67 | },
68 | async retweetTweet(_, tweetId) {
69 | await retweet(tweetId);
70 | },
71 | async unretweetTweet(_, tweetId) {
72 | await unretweet(tweetId);
73 | },
74 | async replyTweet({ dispatch }, { tweetId, text }) {
75 | await reply(tweetId, text);
76 | await dispatch("getMyTimeline", 10);
77 | },
78 |
79 | async getImageUploadUrl(_, { extension, contentType }) {
80 | return await getImageUploadUrl(extension, contentType);
81 | },
82 | async editMyProfile({ commit }, newProfile) {
83 | const profile = await editMyProfile(newProfile);
84 | commit("PROFILE_SET", profile);
85 | return profile;
86 | },
87 |
88 | async followUser(_, profileId) {
89 | await follow(profileId);
90 | },
91 | async unfollowUser(_, profileId) {
92 | await unfollow(profileId);
93 | },
94 |
95 | async getFollowers({ commit }, { userId, limit }) {
96 | const followers = await getFollowers(userId, limit);
97 | commit("TWITTER_FOLLOWERS", followers);
98 | },
99 | async getFollowing({ commit }, { userId, limit }) {
100 | const following = await getFollowing(userId, limit);
101 | commit("TWITTER_FOLLOWING", following);
102 | },
103 |
104 | async loadMoreTweets({ commit, getters }, limit) {
105 | if (!getters.nextTokenTweets) return;
106 | const tweets = await getTweets(getters.profile.id, limit, getters.nextTokenTweets);
107 | commit("TWITTER_LOADMORE_TWEETS", tweets);
108 | },
109 | async loadMoreMyTimeline({ commit, getters }, limit) {
110 | if (!getters.nextTokenTweets) return;
111 | const timeline = await getMyTimeline(limit, getters.nextTokenTweets);
112 | commit("TWITTER_LOADMORE_TWEETS", timeline);
113 | },
114 |
115 | async loadSearch({ commit }, { query, mode, limit }) {
116 | const searchResults = await search(query, mode, limit);
117 | commit("TWITTER_SEARCH", searchResults);
118 | },
119 | async loadMoreSearch({ commit, getters }, { query, mode, limit }) {
120 | if (!getters.nextTokenSearch) return;
121 | const searchResults = await search(query, mode, limit, getters.nextTokenSearch);
122 | commit("TWITTER_LOADMORE_SEARCH", searchResults);
123 | },
124 | resetSearch({ commit }) {
125 | const searchResults = {
126 | results: [],
127 | nextToken: undefined,
128 | }
129 | commit("TWITTER_SEARCH", searchResults);
130 | },
131 |
132 | async loadSearchHashTag({ commit }, { query, mode, limit }) {
133 | const q = query || ' '; // mandatory field
134 | const searchResults = await getHashTag(q, mode, limit);
135 | commit("TWITTER_SEARCH_HASHTAG", searchResults);
136 | },
137 | async loadMoreSearchHashTag({ commit, getters }, { query, mode, limit }) {
138 | if (!getters.nextTokenSearch) return;
139 | const q = query || ' '; // mandatory field
140 | const searchResults = await getHashTag(q, mode, limit, getters.nextTokenSearch);
141 | commit("TWITTER_LOADMORE_SEARCH_HASHTAG", searchResults);
142 | },
143 | resetSearchHashTag({ commit }) {
144 | const searchResults = {
145 | results: [],
146 | nextToken: undefined,
147 | }
148 | commit("TWITTER_SEARCH_HASHTAG", searchResults);
149 | },
150 |
151 | async subscribeNotifications({ commit, getters, dispatch }) {
152 | if (!getters.profile.id || getters.subscription) return;
153 | const isFromActiveConversation = (userId, notification, activeConversation) => {
154 | const conversationId = userId < notification.otherUserId
155 | ? `${userId}_${notification.otherUserId}`
156 | : `${notification.otherUserId}_${userId}`
157 | return activeConversation && activeConversation.id == conversationId;
158 | }
159 |
160 | const userId = getters.profile.id;
161 | const subscription = getOnNotifiedSubscription(userId).subscribe({
162 | next: async ({ value }) => {
163 | const notification = value.data.onNotified;
164 | if (notification.type == 'DMed') {
165 | await dispatch("loadConversations", 10);
166 | // only load messages if they are from the active conversation
167 | if (isFromActiveConversation(userId, notification, getters.conversation)) {
168 | await dispatch("getDirectMessages", {
169 | limit: 10,
170 | message: notification.message,
171 | otherUserId: notification.otherUserId,
172 | });
173 | }
174 | commit("TWITTER_MESSAGES_NEW", notification);
175 | } else {
176 | await dispatch("getMyTimeline", 10); //cheeky update to see latest data
177 | commit("TWITTER_NOTIFICATIONS_NEW", notification);
178 | }
179 | },
180 | });
181 | commit("TWITTER_NOTIFICATIONS_SUBSCRIBE", subscription);
182 | },
183 |
184 | resetNotifications({ commit }) {
185 | commit("TWITTER_NOTIFICATIONS_RESET");
186 | },
187 | unsubscribeNotifications({ commit }) {
188 | commit("TWITTER_NOTIFICATIONS_UNSUBSCRIBE");
189 | },
190 |
191 | async loadConversations({ commit }, limit) {
192 | const conversations = await listConversations(limit);
193 | commit("TWITTER_CONVERSATIONS_LOAD", conversations);
194 | },
195 | resetMessages({ commit }) {
196 | commit("TWITTER_MESSAGES_RESET");
197 | },
198 | async getDirectMessages({ commit }, { otherUserId, limit, nextToken }) {
199 | const messages = await getDirectMessages(otherUserId, limit, nextToken);
200 | commit("TWITTER_MESSAGES_LOAD", messages);
201 | },
202 | async loadMoreDirectMessages({ commit, getters }, { otherUserId, limit }) {
203 | if (!getters.nextTokenMessages) return;
204 | const messages = await getDirectMessages(otherUserId, limit, getters.nextTokenMessages);
205 | commit("TWITTER_LOADMORE_MESSAGES", messages);
206 | },
207 | async sendDirectMessage({ commit, dispatch }, { message, otherUserId }) {
208 | await sendDirectMessage(message, otherUserId);
209 | await dispatch("loadConversations", 10);
210 | const messages = await getDirectMessages(otherUserId, 10);
211 | commit("TWITTER_MESSAGES_LOAD", messages);
212 | },
213 | setActiveConversation({ commit }, conversation) {
214 | commit("TWITTER_CONVERSATION_ACTIVE_SET", conversation);
215 | },
216 |
217 | resetState({ commit }) {
218 | commit("TWITTER_RESET_STATE");
219 | },
220 | };
--------------------------------------------------------------------------------
/src/store/modules/twitter/getters.js:
--------------------------------------------------------------------------------
1 | import Moment from 'moment';
2 |
3 | export default {
4 | profile: state => state.profile,
5 | tweets: state => state.tweets.tweets,
6 | nextTokenTweets: state => state.tweets.nextToken,
7 | joinedDate: state => Moment(state.profile.createdAt).format('MMMM YYYY'),
8 | isSelf: state => screenName => state.profile.screenName == screenName,
9 | followers: state => state.followers.followers,
10 | nextTokenFollowers: state => state.followers.nextToken,
11 | following: state => state.following.following,
12 | nextTokenFollowing: state => state.following.nextToken,
13 | search: state => state.search.results,
14 | nextTokenSearch: state => state.search.nextToken,
15 | searcHashTag: state => state.search.results,
16 | nextTokenSearchHashTag: state => state.search.nextToken,
17 | all: state => state.notifications.all,
18 | mentions: state => state.notifications.mentions,
19 | newNotifications: state => state.notifications.newNotifications,
20 | subscription: state => state.notifications.subscription,
21 |
22 | newMessages: state => state.notifications.messages.newMessages,
23 | conversationsSet: state => state.notifications.messages.conversationsSet,
24 | hasNewMessages: state => conversation => {
25 | const set = state.notifications.messages.conversationsSet;
26 | const active = state.notifications.messages.active.conversation;
27 | return set.has(conversation.id) && (
28 | (active && conversation.id != active.id) || !active
29 | )
30 | },
31 | conversations: state => state.notifications.messages.conversations,
32 | nextTokenConversations: state => state.notifications.messages.conversations.nextToken,
33 | messages: state => state.notifications.messages.active.messages,
34 | nextTokenMessages: state => state.notifications.messages.active.nextTokenMessages,
35 | conversation: state => state.notifications.messages.active.conversation,
36 | };
--------------------------------------------------------------------------------
/src/store/modules/twitter/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions';
2 | import mutations from './mutations';
3 | import getters from './getters';
4 |
5 | const initialState = {
6 | profile: {
7 | id: '',
8 | createdAt: '1970-01-01',
9 | imageUrl: 'default_profile.png',
10 | },
11 | tweets: {
12 | tweets: [],
13 | nextToken: undefined,
14 | },
15 | followers: {
16 | followers: [],
17 | nextToken: undefined,
18 | },
19 | following: {
20 | following: [],
21 | nextToken: undefined,
22 | },
23 | search: {
24 | results: [],
25 | nextToken: undefined,
26 | },
27 | notifications: {
28 | all: [],
29 | mentions: [],
30 | newNotifications: 0,
31 | subscription: undefined,
32 | messages: {
33 | conversations: [],
34 | nextToken: undefined,
35 | newMessages: 0,
36 | conversationsSet: new Set(),
37 | active: {
38 | conversation: undefined,
39 | messages: [],
40 | nextTokenMessages: undefined,
41 | },
42 | },
43 | },
44 | };
45 | const state = () => ({...initialState});
46 |
47 | export default {
48 | namespaced: true,
49 | actions,
50 | mutations,
51 | getters,
52 | state,
53 | }
--------------------------------------------------------------------------------
/src/store/modules/twitter/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | PROFILE_SET(state, profile) {
3 | state.profile = profile;
4 | },
5 |
6 | TWITTER_TIMELINE(state, { tweets, nextToken }) {
7 | state.tweets.tweets = tweets;
8 | state.tweets.nextToken = nextToken;
9 | },
10 | TWITTER_CREATE(state, newTweet) {
11 | const tweets = [...state.tweets.tweets];
12 | state.tweets.tweets = tweets.unshift(newTweet);
13 | },
14 |
15 | TWITTER_FOLLOWERS(state, { profiles, nextToken }) {
16 | state.followers.followers = profiles;
17 | state.followers.nextToken = nextToken;
18 | },
19 | TWITTER_FOLLOWING(state, { profiles, nextToken }) {
20 | state.following.following = profiles;
21 | state.following.nextToken = nextToken;
22 | },
23 |
24 | TWITTER_LOADMORE_TWEETS(state, { tweets, nextToken }) {
25 | state.tweets.tweets = [...state.tweets.tweets, ...tweets];
26 | state.tweets.nextToken = nextToken;
27 | },
28 |
29 | TWITTER_SEARCH(state, { results, nextToken }) {
30 | state.search.results = results;
31 | state.search.nextToken = nextToken;
32 | },
33 | TWITTER_LOADMORE_SEARCH(state, { results, nextToken }) {
34 | state.search.results = [...state.search.results, ...results];
35 | state.search.nextToken = nextToken;
36 | },
37 |
38 | TWITTER_SEARCH_HASHTAG(state, { results, nextToken }) {
39 | state.search.results = results;
40 | state.search.nextToken = nextToken;
41 | },
42 | TWITTER_LOADMORE_SEARCH_HASHTAG(state, { results, nextToken }) {
43 | state.search.results = [...state.search.results, ...results];
44 | state.search.nextToken = nextToken;
45 | },
46 |
47 | TWITTER_NOTIFICATIONS_SUBSCRIBE(state, subscription) {
48 | state.notifications.subscription = subscription;
49 | state.notifications.newNotifications = 0;
50 | state.notifications.messages.newMessages = 0;
51 | },
52 | TWITTER_NOTIFICATIONS_UNSUBSCRIBE(state) {
53 | let subscription = state.notifications.subscription;
54 | if (subscription) {
55 | subscription.unsubscribe();
56 | state.notifications.subscription = undefined;
57 | }
58 | },
59 | TWITTER_NOTIFICATIONS_NEW(state, newMessage) {
60 | if (newMessage?.type == 'Mentioned') {
61 | state.notifications.mentions = [newMessage, ...state.notifications.mentions]; // reverse add order
62 | } else {
63 | state.notifications.all = [newMessage, ...state.notifications.all]; // reverse add order
64 | }
65 | state.notifications.newNotifications = state.notifications.newNotifications + 1;
66 | },
67 | TWITTER_NOTIFICATIONS_RESET(state) {
68 | state.notifications.newNotifications = 0;
69 | },
70 |
71 | TWITTER_CONVERSATIONS_LOAD(state, { conversations, nextToken }) {
72 | state.notifications.messages.conversations = conversations;
73 | state.notifications.messages.nextToken = nextToken;
74 | },
75 | TWITTER_MESSAGES_NEW(state, notification) {
76 | let set = state.notifications.messages.conversationsSet;
77 | let c = state.notifications.messages.active.conversation;
78 | const conversationId = state.profile.id < notification.otherUserId
79 | ? `${state.profile.id}_${notification.otherUserId}`
80 | : `${notification.otherUserId}_${state.profile.id}`;
81 |
82 | // if conversation is active we ignore this
83 | const notActive = c => (!c || c.id != conversationId);
84 | if (notActive(c)) {
85 | if (!set.has(conversationId)) {
86 | set.add(conversationId);
87 | }
88 | state.notifications.messages.newMessages = state.notifications.messages.newMessages + 1;
89 | // trigger getters
90 | state.notifications.messages.conversations = [...state.notifications.messages.conversations];
91 | }
92 | },
93 | TWITTER_MESSAGES_RESET(state) {
94 | state.notifications.messages.newMessages = 0;
95 | },
96 | TWITTER_MESSAGES_LOAD(state, { messages, nextToken }) {
97 | state.notifications.messages.active.messages = messages;
98 | state.notifications.messages.active.nextTokenMessages = nextToken;
99 | },
100 | TWITTER_LOADMORE_MESSAGES(state, { messages, nextToken }) {
101 | state.notifications.messages.active.messages = [...state.notifications.messages.active.messages, ...messages];
102 | state.notifications.messages.active.nextTokenMessages = nextToken;
103 | },
104 | TWITTER_MESSAGES_SEND(state, newMessage) {
105 | state.notifications.messages.active.messages = [...state.notifications.messages.active.messages, newMessage];
106 | },
107 | TWITTER_CONVERSATION_ACTIVE_SET(state, conversation) {
108 | let set = state.notifications.messages.conversationsSet;
109 | if (set && conversation && set.has(conversation.id)) {
110 | set.delete(conversation.id);
111 | // we had a hit enough to clear all notifications in this implementation
112 | // we still can show there's messages for other conversations with new messages just not how many messages
113 | state.notifications.messages.newMessages = 0;
114 | }
115 | state.notifications.messages.conversationsSet = set;
116 | state.notifications.messages.active.conversation = conversation;
117 | state.notifications.messages.active.messages = [];
118 | state.notifications.messages.active.nextTokenMessages = undefined;
119 | },
120 |
121 | TWITTER_RESET_STATE(state) {
122 | Object.assign(state, {
123 | profile: {
124 | id: '',
125 | createdAt: '1970-01-01',
126 | imageUrl: 'default_profile.png',
127 | },
128 | tweets: {
129 | tweets: [],
130 | nextToken: undefined,
131 | },
132 | followers: {
133 | followers: [],
134 | nextToken: undefined,
135 | },
136 | following: {
137 | following: [],
138 | nextToken: undefined,
139 | },
140 | search: {
141 | results: [],
142 | nextToken: undefined,
143 | },
144 | notifications: {
145 | all: [],
146 | mentions: [],
147 | newNotifications: 0,
148 | subscription: undefined,
149 | messages: {
150 | conversations: [],
151 | nextToken: undefined,
152 | newMessages: 0,
153 | conversationsSet: new Set(),
154 | active: {
155 | conversation: undefined,
156 | messages: [],
157 | nextTokenMessages: undefined,
158 | },
159 | },
160 | },
161 | });
162 | }
163 | };
--------------------------------------------------------------------------------
/src/views/Followers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/views/Following.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/views/Hashtag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/views/LogIn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Log in to Twitter
5 |
6 |
Phone, email, or username
7 |
8 |
9 |
13 |
Log in
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/views/Messages.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
46 |
47 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
{{ active.otherUser.name }}
84 |
@{{ active.otherUser.screenName }}
85 |
86 |
87 |
88 |
89 |
125 |
126 |
127 |
133 |
138 |
139 |
140 |
141 |
142 |
143 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/src/views/Notifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/views/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
128 |
129 |
130 |
131 |
132 |
133 |
136 |
137 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/src/views/Root.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
Follow your interests.
10 |
11 |
12 |
13 |
Hear what people are talking about.
14 |
15 |
16 |
17 |
Join the conversation.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
Phone, email, or username
29 |
30 |
31 |
35 |
36 | Log in
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
See what's happening in the world right now
45 |
Join Twitter today.
46 |
Sign up
47 |
Log in
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Next
60 |
61 |
62 |
63 |
64 |
Create your account
65 |
66 |
70 |
74 |
Date of birth
75 |
This will not be shown publicly. Confirm your own age, even if this account is for a business, a pet, or something else.
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Next
88 |
89 |
90 |
91 |
92 |
Customize your experience
93 |
94 |
95 |
96 |
Get more out of Twitter
97 |
98 |
Receive email about your Twitter activity and recommendations.
99 |
100 |
101 |
102 |
103 |
104 |
Connect with people you know
105 |
106 |
Let others find your Twitter account by your email address.
107 |
108 |
109 |
110 |
111 |
112 |
Personalized ads
113 |
114 |
You will always see ads on Twitter based on your Twitter activity. When this setting is enabled, Twitter may further personalise ads from Twitter advertisers, on and off Twitter, by combining your Twitter activity with other online activity and information from our partners.
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Next
127 |
128 |
129 |
130 |
131 |
You'll need a password
132 |
133 |
134 |
Make sure it's 8 characters or more.
135 |
136 |
137 |
Password
138 |
139 |
140 |
141 |
Reveal password
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
Create your account
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
By signing up, you agree to our Terms , Privacy Policy and Cookie Use .
168 |
Sign up
169 |
170 |
171 |
172 |
173 |
174 | Next
175 |
176 |
177 |
178 |
179 |
We sent you a code
180 |
181 |
182 |
Enter it below to verify {{email}}.
183 |
184 |
185 |
Verification code
186 |
187 |
188 |
189 |
Didn't receive an email?
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
--------------------------------------------------------------------------------
/src/views/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | purge: { content: ['./public/**/*.html', './src/**/*.vue'] },
5 | darkMode: false, // or 'media' or 'class'
6 | theme: {
7 | container: {
8 | center: true
9 | },
10 | maxHeight: {
11 | 'full': '85%',
12 | },
13 | extend: {
14 | fontFamily: {
15 | sans: [...defaultTheme.fontFamily.sans]
16 | },
17 | colors: {
18 | 'blue': '#1DA1F2',
19 | 'darkblue': '#2795D9',
20 | 'lightblue': '#EFF9FF',
21 | 'dark': '#657786',
22 | 'light': '#AAB8C2',
23 | 'lighter': '#E1E8ED',
24 | 'lightest': '#F5F8FA',
25 | }
26 | },
27 | screens: {
28 | 'sm': '360px',
29 | // => @media (min-width: 360px) { ... }
30 |
31 | 'md': '768px',
32 | // => @media (min-width: 768px) { ... }
33 |
34 | 'lg': '1024px',
35 | // => @media (min-width: 1024px) { ... }
36 |
37 | 'xl': '1280px',
38 | // => @media (min-width: 1280px) { ... }
39 |
40 | '2xl': '1536px',
41 | // => @media (min-width: 1536px) { ... }
42 | },
43 | },
44 | variants: {
45 | extend: {},
46 | },
47 | plugins: [],
48 | }
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const manifest = require('./public/manifest.json')
2 |
3 | module.exports = {
4 | pwa: {
5 | name: manifest.short_name,
6 | themeColor: manifest.theme_color,
7 | msTileColor: manifest.background_color,
8 | appleMobileWebAppCapable: 'yes',
9 | appleMobileWebAppStatusBarStyle: 'black',
10 | workboxPluginMode: 'InjectManifest',
11 | workboxOptions: {
12 | swSrc: 'src/service-worker.js',
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------