├── Procfile ├── AUTHORS ├── .dockerignore ├── test ├── fixtures │ └── users.js ├── test_tweets.js └── test.js ├── Makefile ├── public ├── img │ ├── bird.png │ └── bird.svg ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── webfonts │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 ├── css │ ├── fa-brands.min.css │ ├── fa-solid.min.css │ ├── fa-regular.min.css │ ├── fa-brands.css │ ├── fa-solid.css │ ├── fa-regular.css │ ├── pygments-manni.css │ ├── bootstrap │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.css │ │ └── bootstrap-reboot.min.css.map │ ├── style.css.map │ └── style.css └── js │ └── app.js ├── app ├── views │ ├── components │ │ ├── tweets.pug │ │ ├── modals │ │ │ ├── tweet-modal.pug │ │ │ ├── delete-confirmation-modal.pug │ │ │ ├── new-tweet-modal.pug │ │ │ └── new-message-modal.pug │ │ ├── pagination.pug │ │ ├── followers.pug │ │ ├── recent-visits.pug │ │ ├── useful-links.pug │ │ ├── comments.pug │ │ ├── tweet.pug │ │ └── profile-card.pug │ ├── layouts │ │ ├── default.pug │ │ ├── footer.pug │ │ ├── head.pug │ │ └── header.pug │ ├── pages │ │ ├── 404.pug │ │ ├── 500.pug │ │ ├── followers.pug │ │ ├── profile.pug │ │ ├── activity.pug │ │ ├── login.pug │ │ ├── analytics.pug │ │ └── index.pug │ └── chat │ │ ├── chat.pug │ │ └── index.pug ├── styles │ ├── layout │ │ ├── _footer.scss │ │ └── _navigation.scss │ ├── abstracts │ │ ├── _placeholders.scss │ │ └── _variables.scss │ ├── components │ │ ├── _followers.scss │ │ ├── _pagination.scss │ │ ├── _profile-card.scss │ │ ├── _modals.scss │ │ └── _tweets.scss │ ├── pages │ │ ├── _login.scss │ │ ├── _messaging.scss │ │ ├── _analytics.scss │ │ └── _home.scss │ ├── main.scss │ └── base │ │ └── _reset.scss ├── models │ ├── notification.js │ ├── analytics.js │ ├── activity.js │ ├── chat.js │ ├── user.js │ └── tweets.js ├── controllers │ ├── activity.js │ ├── favorites.js │ ├── apiv1.js │ ├── follows.js │ ├── comments.js │ ├── chat.js │ ├── tweets.js │ ├── analytics.js │ └── users.js └── middlewares │ └── logger.js ├── .travis.yml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── -meta-.md │ ├── meta.md │ ├── Feature_request.md │ └── Bug_report.md ├── workflows │ ├── greetings.yml │ └── codeql.yml ├── FUNDING.yml ├── pull_request_template.md └── issue_template.md ├── Dockerfile ├── .env.example ├── docker-compose.yml ├── .eslintrc.js ├── gruntfile.js ├── config ├── config.example.js ├── config.local.js ├── middlewares │ ├── authorization.js │ └── logger.js ├── config.js ├── passport.js ├── express.js └── routes.js ├── lib └── utils.js ├── server.js ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── readme.md └── LICENSE /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Vinit Kumar 2 | Robert Cooper 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules* 2 | npm-debug.log -------------------------------------------------------------------------------- /test/fixtures/users.js: -------------------------------------------------------------------------------- 1 | exports.userA = { 2 | }; 3 | 4 | exports.userB = { 5 | }; 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha --reporter spec --exit 3 | .PHONY: test 4 | -------------------------------------------------------------------------------- /public/img/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/img/bird.png -------------------------------------------------------------------------------- /app/views/components/tweets.pug: -------------------------------------------------------------------------------- 1 | each tweet in tweets 2 | include tweet 3 | include modals/tweet-modal 4 | -------------------------------------------------------------------------------- /public/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/FontAwesome.otf -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - mongodb 3 | language: node_js 4 | node_js: 5 | - "12.18.3" 6 | script: npm test 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | .vscode/* 4 | .idea 5 | .DS_Store 6 | .sass-cache 7 | .env 8 | data/* 9 | -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/views/components/modals/tweet-modal.pug: -------------------------------------------------------------------------------- 1 | .modal.fade(id="tweet-modal-"+tweet._id) 2 | .modal-dialog(role='document') 3 | include ../tweet 4 | -------------------------------------------------------------------------------- /app/views/components/pagination.pug: -------------------------------------------------------------------------------- 1 | - if (pages > 1) 2 | nav(aria-label='Page navigation').pagination 3 | ul.pagination__list 4 | != pagination 5 | -------------------------------------------------------------------------------- /app/styles/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | /* Footer */ 2 | 3 | footer { 4 | text-align: center; 5 | background-color: #243447; 6 | padding-top: 30px; 7 | height: 150px; 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-meta-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[META]" 3 | about: Discuss anything that's not a feature request or bug report 4 | title: '' 5 | labels: question, discussion 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Meta 3 | about: Discuss anything that's not a feature request or bug report 4 | title: "[META]" 5 | labels: discussion, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/styles/abstracts/_placeholders.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | %block-content { 4 | background-color: $yankees-blue; 5 | padding: 15px; 6 | border-radius: 2px; 7 | border: 1px solid $black; 8 | margin-bottom: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /app/views/layouts/default.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | include head 4 | if (req.isAuthenticated()) 5 | body.login-screen 6 | include header 7 | .container 8 | block main 9 | block content 10 | include footer 11 | -------------------------------------------------------------------------------- /app/views/pages/404.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 404 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | WORKDIR /usr/src/app 4 | # Install app dependencies 5 | COPY package*.json ./ 6 | RUN npm install 7 | # Copy app source code 8 | COPY . . 9 | #Expose port and start application 10 | EXPOSE 3000 11 | 12 | CMD [ "node", "server.js" ] -------------------------------------------------------------------------------- /app/views/pages/500.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 500 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre.pre-scrollable 12 | code!=error 13 | -------------------------------------------------------------------------------- /app/models/notification.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Notification = new Schema({ 5 | type: { type: Number }, 6 | activity: { type: Schema.ObjectId, ref: "Activity" } 7 | }); 8 | 9 | mongoose.model("Notification", Notification); 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Example of Environment Variables 2 | 3 | NODE_ENV=development 4 | GITHUB_CLIENT_SECRET="your_github_client_secret" 5 | SECRET="your_secret" 6 | GITHUB_CLIENT_ID="your_github_client_id" 7 | DB="mongodb://localhost/ntwitter" 8 | # Use this when run via Docker 9 | # DB="mongodb://mongodb/ntwitter" 10 | PORT=3000 11 | -------------------------------------------------------------------------------- /app/styles/components/_followers.scss: -------------------------------------------------------------------------------- 1 | @import '../abstracts/placeholders'; 2 | 3 | .follower { 4 | @extend %block-content; 5 | text-align: center; 6 | } 7 | 8 | .follower__user-info { 9 | margin-top: 10px; 10 | } 11 | 12 | .follower__image { 13 | border-radius: 50%; 14 | width: 100px; 15 | max-width: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /app/views/components/followers.pug: -------------------------------------------------------------------------------- 1 | .col-lg-4 2 | .follower 3 | - if (user.github !== undefined) 4 | img(class="follower__image", src=user.github.avatar_url) 5 | div.follower__user-info 6 | span.follower__handle 7 | - if (user.github !== undefined) 8 | a(href="/users/" + user._id) @#{user.github.login} 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "3000:3000" 7 | depends_on: 8 | - mongodb 9 | restart: always 10 | mongodb: 11 | container_name: mongodb 12 | image: mongo 13 | volumes: 14 | - ./data:/data/db 15 | ports: 16 | - "27017:27017" 17 | -------------------------------------------------------------------------------- /app/views/components/recent-visits.pug: -------------------------------------------------------------------------------- 1 | h4.recent-visits__title Recent Visits 2 | ul.recent-visits__list 3 | each analytic in analytics 4 | if analytic.user 5 | -var name = analytic.user.name ? analytic.user.name : analytic.user.username 6 | li 7 | a(href="/users/"+analytic.user._id)= name 8 | span (#{moment(analytic.createdAt).format('lll')}) 9 | -------------------------------------------------------------------------------- /app/controllers/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Activity = mongoose.model("Activity"); 3 | 4 | exports.index = (req, res) => { 5 | let activities; 6 | let options = {}; 7 | Activity.list(options).then(result => { 8 | activities = result; 9 | return res.render("pages/activity", { 10 | activities: activities 11 | }); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /app/views/pages/followers.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row#followers-page 5 | .col-lg-4 6 | include ../components/profile-card 7 | .col-lg-8 8 | if (followers.length !== 0) 9 | .row 10 | each user in followers 11 | include ../components/followers 12 | else 13 | h4 Sorry! It looks like no one is following you. 14 | -------------------------------------------------------------------------------- /app/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | 3 | const level = process.env.LOG_LEVEL || 'debug'; 4 | 5 | const logger = new winston.createLogger({ 6 | transports: [ 7 | new winston.transports.Console({ 8 | level: level, 9 | timestamp: function () { 10 | return (new Date()).toISOString(); 11 | } 12 | }) 13 | ] 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /app/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $dark-jungle-green: #141D26; 3 | $grey-blue: #8899A6; 4 | $dodger-blue: #1DA1F2; 5 | $jungle-green: #2AAF81; 6 | $yankees-blue: #1B2836; 7 | $white: #ffffff; 8 | $go-red: #E53939; 9 | $black: #000; 10 | 11 | // Breakpoints 12 | $small: '(min-width: 576px)'; 13 | $medium: '(min-width: 768px)'; 14 | $large: '(min-width: 992px)'; 15 | $xlarge: '(min-width: 1200px)'; 16 | -------------------------------------------------------------------------------- /app/styles/pages/_login.scss: -------------------------------------------------------------------------------- 1 | /* Login Page */ 2 | 3 | .login { 4 | padding-top: 36px; 5 | } 6 | 7 | .login-container { 8 | text-align: center; 9 | margin: auto; 10 | } 11 | 12 | .github-btn { 13 | margin-left: 20px; 14 | } 15 | 16 | .icon-container { 17 | margin-left: 6px; 18 | } 19 | 20 | .stats-parent-container { 21 | margin-top: 60px; 22 | } 23 | 24 | .login-using-github { 25 | font-size: 20px; 26 | } 27 | -------------------------------------------------------------------------------- /app/views/components/useful-links.pug: -------------------------------------------------------------------------------- 1 | h4.useful-links__title Useful links 2 | ul.useful-links__list 3 | li 4 | a(href="https://github.com/vinitkumar/node-twitter/graphs/contributors") Authors 5 | li 6 | a(href="https://github.com/vinitkumar/node-twitter/blob/master/License") License 7 | li 8 | a(href="http://github.com/vinitkumar/node-twitter") Source Code 9 | li 10 | a(href="https://github.com/vinitkumar/node-twitter/issues/new") Report a Bug 11 | -------------------------------------------------------------------------------- /app/views/components/modals/delete-confirmation-modal.pug: -------------------------------------------------------------------------------- 1 | .delete-confirmation.modal.fade 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Are You Sure ? 7 | .modal-body 8 | form(method='post', action=`/users/${user._id}/delete`) 9 | button.btn.delete-confirmation__button(type='submit') Delete My Account 10 | -------------------------------------------------------------------------------- /app/views/components/modals/new-tweet-modal.pug: -------------------------------------------------------------------------------- 1 | .new-tweet.modal.fade 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Compose new Tweet 7 | .modal-body 8 | form(method='post', action='/tweets') 9 | textarea(type='text', name='body', placeholder='Enter your tweet here', maxlength='280') 10 | button.btn.new-tweet__button(type='submit') Tweet 11 | -------------------------------------------------------------------------------- /app/views/pages/profile.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | - var currentUserId = req.user._id.toString() 5 | - var userId = user._id.toString() 6 | .row#profile-page 7 | .col-lg-4 8 | include ../components/profile-card 9 | - if (currentUserId === userId) 10 | a.btn.btn__delete(data-toggle='modal', href='.delete-confirmation') 11 | span Delete 12 | .col-lg-8 13 | include ../components/tweets 14 | include ../components/modals/delete-confirmation-modal 15 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base/reset'; 2 | @import 'layout/navigation'; 3 | @import 'components/followers'; 4 | @import 'components/tweets'; 5 | @import 'components/pagination'; 6 | @import 'components/modals.scss'; 7 | @import 'components/profile-card'; 8 | @import 'pages/login'; 9 | @import 'pages/home'; 10 | @import 'pages/analytics'; 11 | @import 'pages/messaging'; 12 | @import 'layout/footer'; 13 | 14 | /* Media Queries */ 15 | 16 | #error-stack-trace { 17 | background: yellow; 18 | padding: 20px; 19 | margin-top: 20px; 20 | } 21 | -------------------------------------------------------------------------------- /app/controllers/favorites.js: -------------------------------------------------------------------------------- 1 | // ### Create Favorite 2 | exports.create = (req, res) => { 3 | const tweet = req.tweet; 4 | tweet._favorites = req.user; 5 | tweet.save(err => { 6 | if (err) { 7 | return res.send(400); 8 | } 9 | res.send(201, {}); 10 | }); 11 | }; 12 | 13 | // ### Delete Favorite 14 | exports.destroy = (req, res) => { 15 | const tweet = req.tweet; 16 | 17 | tweet._favorites = req.user; 18 | tweet.save(err => { 19 | if (err) { 20 | return res.send(400); 21 | } 22 | res.send(200); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Hi! Thanks for taking the time. Please fill up the issue in as much detail as possible'' first issue' 13 | pr-message: 'Thanks for making a contribution to the project. One of the maintainers will look into it and get back to you as soon as possible'' first pr' 14 | -------------------------------------------------------------------------------- /app/styles/components/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination 2 | 3 | @import '../abstracts/variables'; 4 | 5 | .pagination { 6 | text-align: center; 7 | 8 | a, 9 | li.out-of-range { 10 | padding: 6px 12px; 11 | } 12 | } 13 | 14 | .pagination { 15 | justify-content: center; 16 | } 17 | 18 | .pagination__list { 19 | display: flex; 20 | 21 | li a { 22 | color: $white; 23 | border-color: $black !important; 24 | } 25 | 26 | .no a { 27 | background-color: $yankees-blue; 28 | color: $white; 29 | } 30 | 31 | .active a { 32 | background-color: $dodger-blue !important; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/views/components/modals/new-message-modal.pug: -------------------------------------------------------------------------------- 1 | .new-message.modal.fade(id=modalId) 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Send new message to #{name} 7 | .modal-body 8 | form(method='post', action='/chats') 9 | textarea(type='text', name='body', placeholder='Enter your message here', maxlength='200') 10 | input(type="hidden", name='receiver', value=user._id) 11 | button.new-message__button.btn(type='submit') Send Message 12 | -------------------------------------------------------------------------------- /app/views/pages/activity.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .analytics 5 | h1 Activity 6 | each activity in activities 7 | - var senderName = activity.sender.name ? activity.sender.name : activity.sender.username 8 | - var receiverName = activity.receiver.name ? activity.receiver.name : activity.receiver.username 9 | - var notificationText = senderName + ' ' + activity.activityStream + ' ' + receiverName 10 | p 11 | i(class="far fa-bell notification-icon") 12 | span #{moment(activity.createdAt).format("MMM D, YYYY [at] h:mm a, ")} 13 | span #{notificationText} 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [vinitkumar] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: vinit-kumar 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2017 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /public/css/fa-brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands} -------------------------------------------------------------------------------- /public/css/fa-solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Issue Addressed (if applicable) 4 | _Issue number being fixed (Format: Fixes #xxx)_ 5 | 6 | ## Type 7 | 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 11 | 12 | ### Screenshots (if applicable): 13 | 14 | 17 | -------------------------------------------------------------------------------- /public/css/fa-regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} -------------------------------------------------------------------------------- /app/models/analytics.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const AnalyticsSchema = new Schema({ 5 | ip: String, 6 | user: { type: Schema.ObjectId, ref: "User" }, 7 | url: String, 8 | createdAt: { type: Date, default: Date.now } 9 | }); 10 | 11 | AnalyticsSchema.statics = { 12 | list: function(options) { 13 | const criteria = options.criteria || {}; 14 | return this.find(criteria) 15 | .populate("user", "name username provider") 16 | .sort({ createdAt: -1 }) 17 | .limit(options.perPage) 18 | .skip(options.perPage * options.page); 19 | } 20 | }; 21 | 22 | mongoose.model("Analytics", AnalyticsSchema); 23 | -------------------------------------------------------------------------------- /app/views/layouts/footer.pug: -------------------------------------------------------------------------------- 1 | footer(role='contentinfo') 2 | .container 3 | p Twitter is a registered trademark of Twitter Inc. This project is just for educational purpose only. 4 | p 5 | | Designed and built with all the love in the world by 6 | a(href='https://github.com/vinitkumar/node-twitter/graphs/contributors', target='_blank') Node Twitter Contributors 7 | | using 8 | a(href='https://nodejs.org', target='_blank') nodejs and other OSS technologies. 9 | | 10 | p 11 | | Code licensed 12 | a(rel='license', href='https://github.com/vinitkumar/node-twitter/blob/master/License', target='_blank') Apache License 2.0 13 | 14 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | sass: { 6 | options: { 7 | sourceMap: true 8 | }, 9 | dist: { 10 | files: { 11 | 'public/css/style.css' : 'app/styles/main.scss' 12 | } 13 | } 14 | }, 15 | watch: { 16 | css: { 17 | files: '**/*.scss', 18 | tasks: ['sass'] 19 | } 20 | } 21 | }); 22 | 23 | // Load Grunt plugins 24 | grunt.loadNpmTasks('grunt-sass'); 25 | grunt.loadNpmTasks('grunt-contrib-watch'); 26 | 27 | // Register custom tasks to run from the terminal 28 | grunt.registerTask('default',['watch']); 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /public/css/fa-brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: normal; 9 | src: url("../webfonts/fa-brands-400.eot"); 10 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 11 | 12 | .fab { 13 | font-family: 'Font Awesome 5 Brands'; } 14 | -------------------------------------------------------------------------------- /public/css/fa-solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | src: url("../webfonts/fa-solid-900.eot"); 10 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 11 | 12 | .fa, 13 | .fas { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 900; } 16 | -------------------------------------------------------------------------------- /public/css/fa-regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: url("../webfonts/fa-regular-400.eot"); 10 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 11 | 12 | .far { 13 | font-family: 'Font Awesome 5 Free'; 14 | font-weight: 400; } 15 | -------------------------------------------------------------------------------- /app/styles/components/_profile-card.scss: -------------------------------------------------------------------------------- 1 | /* Profile Card */ 2 | 3 | @import '../abstracts/placeholders'; 4 | @import '../abstracts/variables'; 5 | 6 | .profile { 7 | @extend %block-content; 8 | text-align: center; 9 | } 10 | 11 | .profile__handle { 12 | font-size: 18px; 13 | margin-top: 10px; 14 | margin-bottom: 5px; 15 | display: inline-block; 16 | } 17 | 18 | .profile__image { 19 | border: 0; 20 | border-radius: 50%; 21 | width: 150px; 22 | max-width: 100%; 23 | } 24 | 25 | .profile__messaging-options { 26 | .btn { 27 | margin: 10px; 28 | } 29 | } 30 | 31 | .profile__follow { 32 | margin: 10px 0; 33 | } 34 | 35 | .profile__follow-button.following { 36 | background-color: $go-red; 37 | 38 | &:hover { 39 | background-color: lighten($go-red, 5%); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/views/chat/chat.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row.twitter-container 5 | .col-xl-12.col-lg-12.first-column 6 | .logged-user 7 | .row.chatbox 8 | - if (chats.length == 0) 9 | h2 No chats sorry! 10 | ul.chatlist 11 | - if (chats) 12 | each chat in chats 13 | li 14 | - if (chat.sender != null) 15 | img(class='tweet__image', src=chat.sender.github.avatar_url) 16 | p.chatMessage 17 | a(class='sender', href="/users/" + chat.sender._id) 18 | span.sender #{chat.sender.username}: 19 | span.message #{chat.message} 20 | p.msgtime #{moment(chat.createdAt).format("MMM D, YYYY [at] h:mm a")} 21 | 22 | -------------------------------------------------------------------------------- /config/config.example.js: -------------------------------------------------------------------------------- 1 | const path = require("path"), 2 | rootPath = path.normalize(__dirname + "/.."); 3 | module.exports = { 4 | development: { 5 | db: "", 6 | root: rootPath, 7 | app: { 8 | name: "Node Twitter" 9 | }, 10 | github: { 11 | clientID: "", 12 | clientSecret: "", 13 | callbackURL: "" 14 | } 15 | }, 16 | test: { 17 | db: "", 18 | root: rootPath, 19 | app: { 20 | name: "Node Twitter" 21 | }, 22 | github: { 23 | clientID: "", 24 | clientSecret: "", 25 | callbackURL: "" 26 | } 27 | }, 28 | production: { 29 | db: "", 30 | root: rootPath, 31 | app: { 32 | name: "Node Twitter" 33 | }, 34 | github: { 35 | clientID: "", 36 | clientSecret: "", 37 | callbackURL: "" 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/models/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const ActivitySchema = new Schema({ 5 | activityStream: { type: String, default: "", maxlength: 400 }, 6 | activityKey: { type: Schema.ObjectId }, 7 | sender: { type: Schema.ObjectId, ref: "User" }, 8 | receiver: { type: Schema.ObjectId, ref: "User" }, 9 | createdAt: { type: Date, default: Date.now } 10 | }); 11 | 12 | ActivitySchema.statics = { 13 | list: function(options) { 14 | const criteria = options.criteria || {}; 15 | return this.find(criteria) 16 | .populate("sender", "name username provider") 17 | .populate("receiver", "name username provider") 18 | .sort({ createdAt: -1 }) 19 | .limit(options.perPage) 20 | .skip(options.perPage * options.page); 21 | } 22 | }; 23 | 24 | mongoose.model("Activity", ActivitySchema); 25 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Feature 4 | 5 | ### User story 6 | 7 | 8 | ### Conditions to be met 9 | 10 | 11 | ### Additonal specifications 12 | 13 | 14 | ---- 15 | 16 | ## Bug 17 | 18 | ### Expected behaviour 19 | 20 | 21 | ### Actual behaviour 22 | 23 | 24 | ### Steps to reproduce 25 | 26 | 27 | ### Screenshots 28 | 29 | 30 | ### Logs 31 | 32 | -------------------------------------------------------------------------------- /app/views/layouts/head.pug: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset='utf-8') 3 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 4 | meta(name='description', content='twitter written in node js') 5 | meta(name='author', content='Vinit Kumar') 6 | 7 | title Node Twitter 8 | 9 | link(href='/css/bootstrap/bootstrap.min.css', rel='stylesheet') 10 | link(href='/css/fontawesome-all.min.css', rel="stylesheet") 11 | link(href='/css/style.css', rel='stylesheet') 12 | // Favicon 13 | link(rel='shortcut icon', href='/img/bird.png') 14 | script(type='text/javascript', src='/js/jquery.min.js') 15 | // Bootstrap depedency 16 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.5/umd/popper.min.js') 17 | script(type='text/javascript', src='/js/bootstrap/bootstrap.min.js') 18 | script(type='text/javascript', src='/js/app.js') 19 | -------------------------------------------------------------------------------- /app/views/pages/login.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row 5 | .col-md-4.login-container 6 | h1 Node Twitter 7 | 8 | h3 9 | a(href="/auth/github", class="btn btn-primary btn-lg btn-block login-btn") 10 | span.login-using-github Login using github 11 | span(class="icon-container") 12 | i(class="fab fa-github fa-2x") 13 | 14 | .row.justify-content-center.text-center.stats-parent-container 15 | .col-md-3 16 | span.status-count #{tweetCount} 17 | h5 Tweets Created 18 | 19 | .col-md-3 20 | span.status-count #{userCount} 21 | h5 Unique Users 22 | 23 | .col-md-3 24 | span.status-count #{analyticsCount} 25 | h5 Visitors 26 | 27 | if (typeof errors !== 'undefined') 28 | .fade.in.alert.alert-block.alert-error 29 | a.close(data-dismiss="alert", href="javascript:void(0)") x 30 | ul 31 | each error in errors 32 | li = error.type 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /app/models/chat.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const ChatSchema = new Schema({ 5 | message: { type: String, default: "", trim: true, maxlength: 200 }, 6 | sender: { type: Schema.ObjectId, ref: "User" }, 7 | receiver: { type: Schema.ObjectId, ref: "User" }, 8 | createdAt: { type: Date, default: Date.now } 9 | }); 10 | 11 | ChatSchema.statics = { 12 | load: function(options, cb) { 13 | options.select = options.select || "message sender receiver createdAt"; 14 | return this.findOne(options.criteria) 15 | .select(options.select) 16 | .exec(cb); 17 | }, 18 | list: function(options) { 19 | const criteria = options.criteria || {}; 20 | return this.find(criteria) 21 | .populate("sender", "name username github") 22 | .populate("receiver", "name username github") 23 | .sort({ createdAt: -1 }) 24 | .limit(options.perPage) 25 | .skip(options.perPage * options.page); 26 | } 27 | }; 28 | 29 | mongoose.model("Chat", ChatSchema); 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | schedule: 9 | - cron: "15 12 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /app/styles/pages/_messaging.scss: -------------------------------------------------------------------------------- 1 | // Messaging 2 | 3 | .chatbox { 4 | padding: 20px; 5 | } 6 | 7 | .chatMessage { 8 | display: inline; 9 | } 10 | 11 | span.sender { 12 | color: red; 13 | margin-left: 5px; 14 | margin-right: 5px; 15 | } 16 | 17 | span.message { 18 | width: fit-content; 19 | padding: 6px; 20 | } 21 | 22 | .msgtime { 23 | font-size: 10px; 24 | } 25 | 26 | .chatlist { 27 | margin-left: 20px; 28 | } 29 | 30 | .chat-image { 31 | margin-left: 5px; 32 | margin-right: 5px 33 | } 34 | 35 | .chatrow { 36 | // border: 1px solid lightslategray; 37 | margin-bottom: 10px; 38 | padding: 4px; 39 | border-radius: 7px; 40 | } 41 | 42 | .inboxlink { 43 | margin-left: 40px; 44 | } 45 | 46 | .message-row { 47 | margin-top: 6px; 48 | } 49 | 50 | .following-image, .follower-image { 51 | margin-right: 6px; 52 | } 53 | 54 | a.logout { 55 | color: red !important; 56 | } 57 | 58 | .btn-message { 59 | margin-right: 12px; 60 | } 61 | 62 | .chatgroup { 63 | margin-bottom: 20px; 64 | } 65 | 66 | .chat-bubble { 67 | margin-bottom: 10px; 68 | } -------------------------------------------------------------------------------- /app/views/components/comments.pug: -------------------------------------------------------------------------------- 1 | .col-12.tweet__comments 2 | if tweet.comments.length > 0 3 | hr 4 | each comment in tweet.comments 5 | if (comment && comment.user) 6 | if (comment.commenterPicture) 7 | img(class='tweets__comment-image', src=comment.commenterPicture) 8 | else 9 | img(class='tweets__comment-image', src='http://via.placeholder.com/20x20') 10 | span.tweet__username-date 11 | span.tweet__username 12 | if (comment.commenterName && comment.user) 13 | a(href="/users/" + comment.user)= comment.commenterName 14 | else if (comment.user) 15 | a(href="/users/" + comment.user)= comment.user 16 | span.tweet__date • #{moment(comment.createdAt).format("MMM D, YYYY [at] h:mm a")} 17 | p.tweet__content= comment.body 18 | hr 19 | form(class="tweet__comment-form" method="post", action="/tweets/"+ tweet._id + "/comments") 20 | textarea(type='text', name="body", placeholder='Enter the comment') 21 | button.btn(type='submit') Comment 22 | -------------------------------------------------------------------------------- /app/styles/components/_modals.scss: -------------------------------------------------------------------------------- 1 | // Any Tweet Modal 2 | 3 | .modal-dialog { 4 | margin-top: 60px; 5 | } 6 | 7 | .modal-content { 8 | background: none; 9 | } 10 | 11 | .modal-header { 12 | background-color: $dark-jungle-green; 13 | justify-content: center; 14 | border-bottom: 0; 15 | 16 | button.close { 17 | color: $grey-blue; 18 | opacity: 1; 19 | text-shadow: none; 20 | position: absolute; 21 | right: 15px; 22 | &:hover { 23 | cursor: pointer; 24 | } 25 | } 26 | } 27 | 28 | .modal-body { 29 | background-color: $yankees-blue; 30 | padding: 20px 15px; 31 | } 32 | 33 | .modal { 34 | padding: 0; 35 | } 36 | 37 | .new-tweet__button, 38 | .new-message__button { 39 | font-size: 14px; 40 | padding: 4px 10px; 41 | background-color: #1da1f2; 42 | color: #fff; 43 | margin: auto; 44 | margin-top: 10px; 45 | display: block; 46 | } 47 | 48 | .delete-confirmation__button{ 49 | font-size: 14px; 50 | padding: 4px 10px; 51 | background-color: firebrick; 52 | color: #fff; 53 | margin: auto; 54 | margin-top: 10px; 55 | display: block; } 56 | 57 | .delete-confirmation__button:hover { 58 | background-color: #ff0000;} -------------------------------------------------------------------------------- /app/views/pages/analytics.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .analytics 5 | h1 Analytics Dashboard 6 | .analytics__stats 7 | ul 8 | li 9 | span.analytics__stats-title Visits: 10 | span.analytics__stats-value #{pageViews} 11 | li 12 | span.analytics__stats-title Users: 13 | span.analytics__stats-value #{userCount} 14 | li 15 | span.analytics__stats-title Tweets: 16 | span.analytics__stats-value #{tweetCount} 17 | table.analytics__table 18 | thead 19 | tr 20 | th.analytics__user-column User 21 | th.analytics__url-column URL 22 | th.analytics__date-column Time 23 | 24 | tbody 25 | each analytic in analytics 26 | tr 27 | - if (analytic.user != null && analytic.url != null) 28 | - var name = analytic.user.name ? analytic.user.name : analytic.user.username 29 | td.analytics__user #{name} 30 | td.analytics__url #{analytic.url} 31 | td.analytics__date #{moment(analytic.createdAt).format("MMM D, YYYY [at] h:mm a")} 32 | include ../components/pagination 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/test_tweets.js: -------------------------------------------------------------------------------- 1 | // Tests 2 | 3 | var expect = require('chai').expect; 4 | 5 | describe('Test parse and extract hashtags function', function() { 6 | var tweets = require('../app/controllers/tweets.js'); 7 | 8 | it('Expected behaivor: extract and store hashtags', function(done) { 9 | var testCase = '#bobo, #, n#, #, # 1, f, #a'; 10 | var expectedResult = ['bobo','a']; 11 | 12 | var result = tweets.parseHashtag(testCase); 13 | 14 | expect(result).to.eql(expectedResult); 15 | done(); 16 | }); 17 | 18 | it('Expected behaivor: return empty array', function(done) { 19 | var testCase = 'python, go, ##'; 20 | 21 | var result = tweets.parseHashtag(testCase); 22 | 23 | expect(result).to.eql([]); 24 | done(); 25 | }); 26 | 27 | it('Expected behaivor: return empty array', function(done) { 28 | var testCase = ''; 29 | 30 | var result = tweets.parseHashtag(testCase); 31 | 32 | expect(result).to.eql([]); 33 | done(); 34 | }); 35 | 36 | it('Expected behaivor: return empty array', function(done) { 37 | var testCase = '## #'; 38 | 39 | var result = tweets.parseHashtag(testCase); 40 | 41 | expect(result).to.eql([]); 42 | done(); 43 | }); 44 | }); -------------------------------------------------------------------------------- /app/styles/pages/_analytics.scss: -------------------------------------------------------------------------------- 1 | /* Analytics Page */ 2 | 3 | @import '../abstracts/placeholders'; 4 | 5 | .analytics { 6 | @extend %block-content; 7 | padding-top: 30px; 8 | padding-bottom: 30px; 9 | 10 | h1 { 11 | color: #fff; 12 | text-align: center; 13 | } 14 | } 15 | 16 | .analytics__table { 17 | table-layout: fixed; 18 | width: 100%; 19 | 20 | th, 21 | td { 22 | border: 1px solid #fff; 23 | padding: .25rem .75rem; 24 | } 25 | 26 | th { 27 | font-weight: bold; 28 | color: #fff; 29 | text-align: center; 30 | } 31 | 32 | td { 33 | word-wrap: break-word; 34 | vertical-align: middle; 35 | } 36 | } 37 | 38 | .analytics__user-column { 39 | width: 25%; 40 | } 41 | 42 | .analytics__stats { 43 | margin: 20px 0; 44 | 45 | ul { 46 | display: flex; 47 | justify-content: space-evenly; 48 | font-weight: bold; 49 | 50 | li { 51 | text-align: center; 52 | } 53 | } 54 | } 55 | 56 | .analytics__stats-title { 57 | font-size: 16px; 58 | } 59 | 60 | .analytics__stats-value { 61 | font-size: 18px; 62 | color: #1da1f2; 63 | display: block; 64 | } 65 | 66 | .analytics__user { 67 | font-weight: bold; 68 | text-align: center; 69 | } 70 | -------------------------------------------------------------------------------- /config/config.local.js: -------------------------------------------------------------------------------- 1 | const path = require("path"), 2 | rootPath = path.normalize(__dirname + "/.."); 3 | 4 | module.exports = { 5 | development: { 6 | db: "mongodb://localhost/ntw2", 7 | root: rootPath, 8 | app: { 9 | name: "Node Twitter" 10 | }, 11 | github: { 12 | clientID: "c2e0f478634366e1289d", 13 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 14 | callbackURL: "http://localhost:3000/auth/github/callback" 15 | } 16 | }, 17 | test: { 18 | db: "mongodb://localhost/noobjs_test21", 19 | root: rootPath, 20 | app: { 21 | name: "Nodejs Express Mongoose Demo" 22 | }, 23 | github: { 24 | clientID: "c2e0f478634366e1289d", 25 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 26 | callbackURL: "http://localhost:3000/auth/github/callback" 27 | } 28 | }, 29 | production: { 30 | db: "mongodb://localhost/noobjs_prodd", 31 | root: rootPath, 32 | app: { 33 | name: "Nodejs Express Mongoose Demo" 34 | }, 35 | github: { 36 | clientID: "c2e0f478634366e1289d", 37 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 38 | callbackURL: "http://localhost:3000/auth/github/callback" 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /app/views/components/tweet.pug: -------------------------------------------------------------------------------- 1 | .tweet(data-tweetId = tweet._id) 2 | .tweet(data-tweetId = tweet._id) 3 | .row 4 | .col-1 5 | img(class='tweet__image', src=tweet.user.github.avatar_url) 6 | .col-11.tweet__description 7 | span.tweet__username-date 8 | span.tweet__username 9 | -var name = tweet.user.name ? tweet.user.name : tweet.user.username 10 | a(href="/users/"+tweet.user._id) #{name} 11 | span.tweet__date 12 | a(data-toggle='modal', href="#tweet-modal-"+tweet._id) • #{moment(tweet.createdAt).format("MMM D, YYYY [at] h:mm a")} 13 | // The same replace method in app.js file 14 | // TODO: find solution to reuse the same code 15 | - 16 | var tweet_m = tweet.body 17 | tweet_m = tweet_m.replace(/#(\w+)/g, '#$1'); 18 | p.tweet__content!= tweet_m 19 | if (tweet.user._id == req.user.id) 20 | form.tweet__form(action="/tweets/"+tweet.id+"?_method=POST", method="post") 21 | a.btn.tweet__edit(href="/tweets/"+tweet._id, type="submit") Edit 22 | form.tweet__form(action="/tweets/"+tweet.id+"?_method=DELETE", method="post") 23 | button.btn.tweet__delete(type="submit") Delete 24 | 25 | include comments 26 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats mongoose errors into proper array 3 | * 4 | * @param {Array} errors 5 | * @return {Array} 6 | * @api public 7 | */ 8 | 9 | exports.errors = errors => { 10 | let keys = Object.keys(errors); 11 | const errs = []; 12 | 13 | // if there is no validation error, just display a generic error 14 | if (!keys) { 15 | return ['Oops! There was an error']; 16 | } 17 | 18 | keys.forEach(key => { 19 | errs.push(errors[key].message); 20 | }); 21 | 22 | return errs; 23 | }; 24 | 25 | /** 26 | * Index of object within an array 27 | * 28 | * @param {Array} arr 29 | * @param {Object} obj 30 | * @return {Number} 31 | * @api public 32 | */ 33 | 34 | /** 35 | * Find object in an array of objects that matches a condition 36 | * 37 | * @param {Array} arr 38 | * @param {Object} obj 39 | * @param {Function} cb - optional 40 | * @return {Object} 41 | * @api public 42 | */ 43 | 44 | exports.findByParam = (arr, obj, cb) => { 45 | let index = exports.indexof(arr, obj); 46 | if (~index && typeof cb === 'function') { 47 | return cb(undefined, arr[index]); 48 | } else if (~index && !cb) { 49 | return arr[index]; 50 | } else if (!~index && typeof cb === 'function') { 51 | return cb('not found'); 52 | } 53 | // else undefined is returned 54 | }; 55 | -------------------------------------------------------------------------------- /config/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic require login routing middlewares 3 | */ 4 | 5 | exports.requiresLogin = (req, res, next) => { 6 | console.log('authenticated', req.isAuthenticated()); 7 | if (!req.isAuthenticated()) { 8 | return res.redirect('/login'); 9 | } 10 | next(); 11 | }; 12 | 13 | /** 14 | * User authorization routing middleware 15 | */ 16 | 17 | exports.user = { 18 | hasAuthorization: (req, res, next) => { 19 | if (req.profile.id !== req.user.id) { 20 | return res.redirect('/users'+req.profile.id); 21 | } 22 | next(); 23 | } 24 | }; 25 | 26 | exports.tweet = { 27 | hasAuthorization: (req, res, next) => { 28 | if (req.tweet.user.id !== req.user.id) { 29 | return res.redirect('/tweets'+req.tweet.id); 30 | } 31 | next(); 32 | } 33 | }; 34 | 35 | 36 | /** 37 | * Comment authorization routing middleware 38 | */ 39 | 40 | exports.comment = { 41 | hasAuthorization: (req, res, next) => { 42 | // if the current user is comment owner or article owner 43 | // give them authority to delete 44 | if (req.user.id === req.comment.user.id || req.user.id === req.article.user.id) { 45 | next(); 46 | } else { 47 | req.flash('info', 'You are not authorized'); 48 | res.redirect('/articles/' + req.article.id); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /app/controllers/apiv1.js: -------------------------------------------------------------------------------- 1 | // ## Tweet Controller 2 | const mongoose = require("mongoose"); 3 | const Tweet = mongoose.model("Tweet"); 4 | const User = mongoose.model("User"); 5 | 6 | exports.tweetList = (req, res) => { 7 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 8 | const perPage = 15; 9 | const options = { 10 | perPage: perPage, 11 | page: page 12 | }; 13 | let tweets, count; 14 | Tweet.limitedList(options) 15 | .then(result => { 16 | tweets = result; 17 | return Tweet.countDocuments(); 18 | }) 19 | .then(result => { 20 | count = result; 21 | return res.send(tweets); 22 | }) 23 | .catch(error => { 24 | return res.render("pages/500", { errors: error.errors }); 25 | }); 26 | }; 27 | 28 | exports.usersList = (req, res) => { 29 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 30 | const perPage = 15; 31 | const options = { 32 | perPage: perPage, 33 | page: page 34 | }; 35 | let users, count; 36 | User.list(options) 37 | .then(result => { 38 | users = result; 39 | return User.countDocuments(); 40 | }) 41 | .then(result => { 42 | count = result; 43 | return res.send(users); 44 | }) 45 | .catch(error => { 46 | return res.render("pages/500", { errors: error.errors }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /app/controllers/follows.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const User = mongoose.model("User"); 3 | const Activity = mongoose.model("Activity"); 4 | const logger = require("../middlewares/logger"); 5 | 6 | exports.follow = (req, res) => { 7 | const user = req.user; 8 | const id = req.url.split("/")[2]; 9 | // push the current user in the follower list of the target user 10 | 11 | const currentId = user.id; 12 | 13 | User.findOne({ _id: id }, function(err, user) { 14 | if (user.followers.indexOf(currentId) === -1) { 15 | user.followers.push(currentId); 16 | } 17 | user.save(err => { 18 | if (err) { 19 | logger.error(err); 20 | } 21 | }); 22 | }); 23 | 24 | // Over here, we find the id of the user we want to follow 25 | // and add the user to the following list of the current 26 | // logged in user 27 | User.findOne({ _id: currentId }, function(err, user) { 28 | if (user.following.indexOf(id) === -1) { 29 | user.following.push(id); 30 | } 31 | user.save(err => { 32 | const activity = new Activity({ 33 | activityStream: "followed by", 34 | activityKey: user, 35 | sender: currentId, 36 | receiver: user 37 | }); 38 | 39 | activity.save(err => { 40 | if (err) { 41 | logger.error(err); 42 | res.render("pages/500"); 43 | } 44 | }); 45 | if (err) { 46 | res.status(400); 47 | } 48 | res.status(201).send({}); 49 | }); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /app/controllers/comments.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const utils = require("../../lib/utils"); 3 | const mongoose = require("mongoose"); 4 | const Activity = mongoose.model("Activity"); 5 | const logger = require("../middlewares/logger"); 6 | 7 | exports.load = (req, res, next, id) => { 8 | const tweet = req.tweet; 9 | utils.findByParam(tweet.comments, { id: id }, (err, comment) => { 10 | if (err) { 11 | return next(err); 12 | } 13 | req.comment = comment; 14 | next(); 15 | }); 16 | }; 17 | 18 | // ### Create Comment 19 | exports.create = (req, res) => { 20 | const tweet = req.tweet; 21 | const user = req.user; 22 | 23 | if (!req.body.body) { 24 | return res.redirect("/"); 25 | } 26 | tweet.addComment(user, req.body, err => { 27 | if (err) { 28 | logger.error(err); 29 | return res.render("pages/500"); 30 | } 31 | const activity = new Activity({ 32 | activityStream: "added a comment", 33 | activityKey: tweet.id, 34 | sender: user, 35 | receiver: req.tweet.user 36 | }); 37 | logger.info(activity); 38 | activity.save(err => { 39 | if (err) { 40 | logger.error(err); 41 | return res.render("pages/500"); 42 | } 43 | }); 44 | res.redirect("/"); 45 | }); 46 | }; 47 | 48 | // ### Delete Comment 49 | exports.destroy = (req, res) => { 50 | // delete a comment here. 51 | const comment = req.comment; 52 | comment.remove(err => { 53 | if (err) { 54 | res.send(400); 55 | } 56 | res.send(200); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | 3 | const request = require('supertest'); 4 | const app = require('../server'); 5 | const assert = require('assert'); 6 | 7 | describe('Test Homepage', function (done) { 8 | it('should return 302', function (done) { 9 | request(app) 10 | .get('/') 11 | .expect(302, done); 12 | }); 13 | }); 14 | 15 | describe('Test Login', function (done) { 16 | it('should return 200', function (done) { 17 | request(app) 18 | .get('/login') 19 | .expect(200, done); 20 | }); 21 | }); 22 | 23 | describe('Test Users API', function (done) { 24 | it('should return 200', function (done) { 25 | request(app) 26 | .get('/apiv1/users') 27 | .expect(200) 28 | .expect('Content-Type', /json/) 29 | .end(function(err, res) { 30 | if (err) return done(err); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('Test logout', function (done) { 37 | it('logout should redirect because there is no active session', function (done) { 38 | request(app) 39 | .get('/logout') 40 | .expect(302) 41 | .end(function (err, res) { 42 | if (err) return done(err); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('Test Tweets API', function (done) { 49 | it('should return 200', function (done) { 50 | request(app) 51 | .get('/apiv1/tweets') 52 | .expect(200) 53 | .expect('Content-Type', /json/) 54 | .end(function(err, res) { 55 | if (err) return done(err); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /app/views/components/profile-card.pug: -------------------------------------------------------------------------------- 1 | - var modalId = user._id + 'ChatModal'; 2 | - var name = user.name ? user.name : user.username 3 | - var currentUserId = req.user._id.toString() 4 | - var userId = user._id.toString() 5 | - var profileURL = "https://github.com/" + user.github.login; 6 | 7 | .profile 8 | - if (user.github !== undefined) 9 | img(class="profile__image", src=user.github.avatar_url) 10 | .profile__user-info 11 | span.profile__handle 12 | - if (user.github !== undefined) 13 | a(href=profileURL) 14 | span 15 | i(class="fab fa-github") #{user.github.login} 16 | .profile__messaging-options 17 | - if (currentUserId !== userId) 18 | a.btn(data-toggle='modal', href='#'+ modalId) Message #{name} 19 | a.btn.profile__follow-button(href="javascript:void(0)", data-userid=user._id, data-logged=req.user.id,title="follow") Follow 20 | if (req.isAuthenticated()) 21 | include modals/new-message-modal 22 | .row 23 | .col-12.user-information__stats 24 | ul 25 | li 26 | span.user-information__stat-title Tweets: 27 | span 28 | a(href="/users/" + user._id) #{tweetCount} 29 | li 30 | span.user-information__stat-title Following: 31 | span 32 | a(href="/users/" + user._id + "/following") #{followingCount} 33 | li 34 | span.user-information__stat-title Followers: 35 | span 36 | a(href="/users/" + user._id + "/followers") #{followerCount} 37 | 38 | 39 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const rootPath = path.normalize(__dirname + "/.."); 3 | 4 | const envPath = process.env.ENVPATH || ".env"; 5 | const dotenv = require("dotenv"); 6 | // Path to the file where environment variables 7 | dotenv.config({path: envPath }); 8 | 9 | module.exports = { 10 | development: { 11 | db: process.env.DB, 12 | port: process.env.PORT, 13 | root: rootPath, 14 | app: { 15 | name: "Node Twitter" 16 | }, 17 | github: { 18 | // GITHUB_CLIENT_SECRET and GITHUB_CLIENT_ID should be defined in .env file 19 | // which is stored locally on your computer or those variables values 20 | // can be passed from Docker container 21 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 22 | clientID: process.env.GITHUB_CLIENT_ID, 23 | callbackURL: "http://localhost:3000/auth/github/callback" 24 | } 25 | }, 26 | test: { 27 | //db: process.env.DB, 28 | // Hack to allow tests run 29 | db: "mongodb://root:volvo76@ds039078.mongolab.com:39078/ntwitter", 30 | root: rootPath, 31 | app: { 32 | name: "Nodejs Express Mongoose Demo" 33 | }, 34 | github: { 35 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 36 | clientID: process.env.GITHUB_CLIENT_ID, 37 | callbackURL: "http://localhost:3000/auth/github/callback" 38 | } 39 | }, 40 | production: { 41 | db: process.env.DB, 42 | root: rootPath, 43 | app: { 44 | name: "Nodejs Express Mongoose Demo" 45 | }, 46 | github: { 47 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 48 | clientID: process.env.GITHUB_CLIENT_ID, 49 | callbackURL: "http://nitter.herokuapp.com/auth/github/callback" 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /app/styles/components/_tweets.scss: -------------------------------------------------------------------------------- 1 | /* Tweets */ 2 | 3 | @import '../abstracts/placeholders'; 4 | @import '../abstracts/variables'; 5 | 6 | .tweet { 7 | @extend %block-content; 8 | color: $white; 9 | } 10 | 11 | .tweet::after { 12 | content: ""; 13 | clear: both; 14 | display: block; 15 | } 16 | 17 | .tweet__image { 18 | border-radius: 50%; 19 | height: 40px; 20 | border: 1px solid $black; 21 | } 22 | 23 | .logout-image { 24 | height: 20px; 25 | } 26 | 27 | .tweet__description { 28 | padding-left: 25px; 29 | } 30 | 31 | .tweet__content { 32 | margin: 5px 0 10px; 33 | word-wrap: break-word; 34 | } 35 | 36 | .tweet__form { 37 | .tweet__edit { 38 | background-color: $jungle-green; 39 | 40 | &:hover { 41 | background-color: lighten($jungle-green, 5%); 42 | } 43 | } 44 | .tweet__delete { 45 | margin-left: 10px; 46 | background-color: $go-red; 47 | 48 | &:hover { 49 | background-color: lighten($go-red, 5%); 50 | } 51 | } 52 | } 53 | 54 | .tweet__username a { 55 | color: $white; 56 | font-weight: bold; 57 | } 58 | 59 | .tweet__date, 60 | .tweet__date a { 61 | color: $grey-blue; 62 | } 63 | 64 | .tweet textarea.edit-tweet { 65 | margin: 10px 0 15px; 66 | } 67 | 68 | .tweet textarea { 69 | font-size: 13px; 70 | background-color: $dark-jungle-green; 71 | border-color: $black; 72 | display: block; 73 | padding: 6px 12px; 74 | color: $white; 75 | border-radius: 4px; 76 | width: 100%; 77 | } 78 | 79 | .tweet__comments .btn { 80 | margin: auto; 81 | display: block; 82 | margin-top: 10px; 83 | } 84 | 85 | .tweets__comment-image { 86 | border-radius: 50%; 87 | height: 20px; 88 | border: 1px solid $black; 89 | margin-right: 6px; 90 | } 91 | 92 | .notification-icon { 93 | margin-right: 10px; 94 | } -------------------------------------------------------------------------------- /app/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* General */ 2 | 3 | @import '../abstracts/variables'; 4 | 5 | body { 6 | font-size: 13px; 7 | min-height: 100vh; 8 | background-color: $dark-jungle-green; 9 | color: $grey-blue; 10 | } 11 | 12 | body > div.container { 13 | /* Calculate the minimum height by subtracting the header and footer height */ 14 | min-height: calc(100vh - 50px - 150px); 15 | padding-top: 30px; 16 | padding-bottom: 30px; 17 | } 18 | 19 | h4 { 20 | font-size: 1.2rem; 21 | } 22 | 23 | ul { 24 | list-style: none; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | a { 30 | color: $dodger-blue; 31 | 32 | &:hover { 33 | color: darken($dodger-blue, 10%); 34 | text-decoration: none; 35 | } 36 | } 37 | 38 | hr { 39 | border-color: $dark-jungle-green; 40 | } 41 | 42 | .btn { 43 | border-radius: 5px; 44 | font-size: 11px; 45 | padding: 3px 8px; 46 | color: $white; 47 | background-color: $dodger-blue; 48 | 49 | &:hover { 50 | background-color: lighten($dodger-blue, 5%); 51 | cursor: pointer; 52 | color: $white; 53 | } 54 | } 55 | 56 | .btn__delete{ 57 | background-color: firebrick; 58 | 59 | &:hover { 60 | background-color: #ff0000; 61 | cursor: pointer; 62 | color: $white; 63 | } 64 | } 65 | 66 | textarea { 67 | color: $grey-blue; 68 | padding: 5px 10px; 69 | width: 100%; 70 | background-color: $dark-jungle-green; 71 | border: solid 1px $black; 72 | border-radius: 8px; 73 | &::placeholder{ 74 | color: $grey-blue; 75 | } 76 | &:focus { 77 | color: $white; 78 | } 79 | } 80 | 81 | pre { 82 | white-space: pre-wrap; /* css-3 */ 83 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 84 | white-space: -pre-wrap; /* Opera 4-6 */ 85 | white-space: -o-pre-wrap; /* Opera 7 */ 86 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 87 | } 88 | 89 | pre code { 90 | color: black; 91 | } 92 | -------------------------------------------------------------------------------- /app/views/layouts/header.pug: -------------------------------------------------------------------------------- 1 | body 2 | header.navbar(role='banner') 3 | .container 4 | if (req.isAuthenticated()) 5 | .navbar__group.navbar__group_left 6 | ul.navbar__main-navigation 7 | li 8 | a(href="/" title="Home") 9 | i(class="fas fa-home", aria-hidden="true") 10 | span Home 11 | | 12 | li 13 | a(href="/users/"+req.user.id, title="Profile") 14 | i(class="fas fa-user", aria-hidden="true") 15 | span Profile 16 | | 17 | li 18 | a(href="/analytics/", title="analytics") 19 | i(class="fas fa-chart-line", aria-hidden="true") 20 | span Analytics 21 | li 22 | a(href='/chat/get/' + req.user._id, title="Inbox") 23 | i(class="far fa-envelope", aria-hidden="true") 24 | span Inbox 25 | li 26 | a(href="/chat/", title="chat") 27 | i(class="far fa-comments", aria-hidden="true") 28 | span Chat Users 29 | li 30 | a(href="/activities", title="activity") 31 | i(class="far fa-bell", aria-hidden="true") 32 | span.navbar__logo 33 | a(href='/') 34 | img(src='/img/bird.svg') 35 | if (req.isAuthenticated()) 36 | .navbar__group.navbar__group_right 37 | .navbar__new-tweet 38 | a.navbar__new-tweet-button.btn(data-toggle='modal', href='.new-tweet') 39 | span Tweet 40 | .navbar__profile 41 | a(href="/logout", title="logout", class="navbar__profile-logout") 42 | img(class='tweet__image chat-image logout-image', src=req.user.github.avatar_url) 43 | span Logout 44 | if (req.isAuthenticated()) 45 | include ../components/modals/new-tweet-modal 46 | -------------------------------------------------------------------------------- /config/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Analytics = mongoose.model('Analytics'); 3 | const logger = require('../../app/middlewares/logger'); 4 | 5 | 6 | exports.analytics = (req, res, next) => { 7 | // A lot of analytics is missed because users might have 8 | // malinformed IPs. Let's just get rid of the IP data altogether and log user irrepsective 9 | // of that. For backward compatiblity, we will just store a dummy IP for all future users. 10 | // This will also result in lesser code both in complexity and in line count. 11 | const url = req.protocol + '://' + req.get('host') + req.originalUrl; 12 | // cleanup IP to remove unwanted characters 13 | const cleanIp = '129.23.12.1'; 14 | Analytics.findOne({ user: req.user}).sort({ createdAt: -1 }).exec(function (err, analytics) { 15 | let date = new Date(); 16 | if (analytics !== null) { 17 | if (new Date(analytics.createdAt).getDate() !== date.getDate()) { 18 | if (req.get('host').split(':')[0] !== 'localhost') { 19 | const analytics = new Analytics({ 20 | ip: cleanIp, 21 | user: req.user, 22 | url: url 23 | }); 24 | analytics.save(err => { 25 | if (err) { 26 | logger.log(err); 27 | } 28 | }); 29 | } 30 | } else { 31 | logger.log('Not creating a new analytics entry on the same day'); 32 | } 33 | } else { 34 | // it means this user is a new user and do not have a analytics object yet 35 | if (req.get('host').split(':')[0] !== 'localhost') { 36 | const analytics = new Analytics({ 37 | ip: cleanIp, 38 | user: req.user, 39 | url: url 40 | }); 41 | analytics.save(err => { 42 | if (err) { 43 | logger.log(err); 44 | } 45 | }); 46 | } 47 | } 48 | }); 49 | next(); 50 | }; 51 | -------------------------------------------------------------------------------- /public/img/bird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 12 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/views/chat/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row.twitter-container 5 | .col-xl-12.col-lg-12.first-column 6 | .logged-user 7 | .row 8 | .container 9 | h1 10 | != title 11 | p Click on the name/image to send a message. 12 | div.row.chatgroup 13 | each user in users 14 | div.col-md-3.chat-bubble 15 | - var name = user.name ? user.name : user.username 16 | - var id = user._id + 'ChatModal'; 17 | a(data-toggle='modal', href='#'+ id) 18 | img(class='tweet__image chat-image', src=user.github.avatar_url) 19 | span #{name} 20 | - var currentUserId = req.user._id.toString() 21 | - var userId = user._id.toString() 22 | 23 | if (req.isAuthenticated()) 24 | div(id=id, class='modal fade') 25 | .modal-dialog(role='document') 26 | .modal-content 27 | .modal-header 28 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 29 | h4.modal-title Send new message to #{name} 30 | .modal-body 31 | form(method='post', action='/chats') 32 | .control-group 33 | label.control-label(for='chat') 34 | .controls 35 | textarea.flat.form-control(type='text', name='body', placeholder='Enter your Message here', maxlength='200') 36 | input(type="hidden", name='receiver', value=user._id) 37 | br 38 | .form-actions 39 | button.btn(type='submit') Send Message 40 | include ../components/pagination 41 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const cookieSession = require('cookie-session'); 4 | const fs = require('fs'); 5 | const passport = require('passport'); 6 | const env = process.env.NODE_ENV || 'development'; 7 | const config = require('./config/config')[env]; 8 | const auth = require('./config/middlewares/authorization'); 9 | const mongoose = require('mongoose'); 10 | const app = express(); 11 | const port = process.env.PORT || 3000; 12 | const cookieParserKey = process.env.COOKIE_KEY || "super55"; 13 | const sessionKey1 = process.env.SESSIONKEYONE || "key1"; 14 | const sessionKey2 = process.env.SESSIONKEYTWO || "key2"; 15 | const promiseRetry = require('promise-retry'); 16 | 17 | app.use(cookieParser(cookieParserKey)); 18 | app.use(cookieSession({ 19 | name: 'session', 20 | keys: [sessionKey1, sessionKey2] 21 | })); 22 | 23 | const options = { 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true, 26 | reconnectTries: 60, 27 | reconnectInterval: 1000, 28 | poolSize: 10, 29 | bufferMaxEntries: 0 // If not connected, return errors immediately rather than waiting for reconnect 30 | }; 31 | 32 | const promiseRetryOptions = { 33 | retries: options.reconnectTries, 34 | factor: 2, 35 | minTimeout: options.reconnectInterval, 36 | maxTimeout: 5000 37 | }; 38 | 39 | const connect = () => { 40 | return promiseRetry((retry, number) => { 41 | console.log(`MongoClient connecting to ${config.db} - retry number: ${number}`); 42 | return mongoose.connect(config.db, options).catch(retry) 43 | }, promiseRetryOptions); 44 | }; 45 | 46 | const models_path = __dirname+'/app/models'; 47 | fs.readdirSync(models_path).forEach(file => { 48 | require(models_path+'/'+file); 49 | }); 50 | 51 | require('./config/passport')(passport, config); 52 | require('./config/express')(app, config, passport); 53 | require('./config/routes')(app, passport, auth); 54 | 55 | app.listen(port); 56 | console.log('Express app started on port ' + port); 57 | 58 | connect(); 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /app/styles/pages/_home.scss: -------------------------------------------------------------------------------- 1 | /* Main Dashboard */ 2 | 3 | @import '../abstracts/variables'; 4 | @import '../abstracts/placeholders'; 5 | 6 | .user-information { 7 | @extend %block-content; 8 | } 9 | 10 | .user-information > .row { 11 | align-items: center; 12 | } 13 | 14 | .user-information__image { 15 | padding-right: 0; 16 | 17 | img { 18 | border-radius: 50%; 19 | width: 100px; 20 | max-width: 100%; 21 | float: right; 22 | } 23 | } 24 | 25 | .user-information__text { 26 | text-align: center; 27 | } 28 | 29 | .user-information h4, 30 | .useful-links h4, 31 | .recent-visits h4 { 32 | color: #fff; 33 | } 34 | 35 | .user-information__stats { 36 | ul { 37 | margin-top: 10px; 38 | text-align: center; 39 | display: flex; 40 | justify-content: space-around; 41 | font-weight: bold; 42 | width: 100%; 43 | } 44 | 45 | &_small { 46 | @media #{$large} { 47 | display: none; 48 | } 49 | } 50 | 51 | &_large { 52 | display: none; 53 | 54 | @media #{$large} { 55 | display: block; 56 | } 57 | } 58 | } 59 | 60 | .user-information__stat-title { 61 | display: block; 62 | } 63 | 64 | .user-information__stats span:not(.user-information__stat-title) { 65 | font-size: 18px; 66 | color: #1da1f2; 67 | } 68 | 69 | .useful-links a { 70 | font-weight: bold; 71 | } 72 | 73 | .useful-links, 74 | .recent-visits { 75 | @extend %block-content; 76 | text-align: center; 77 | } 78 | 79 | .first-column { 80 | .recent-visits, 81 | .useful-links { 82 | display: none; 83 | 84 | @media #{$large} { 85 | display: block; 86 | } 87 | } 88 | 89 | .recent-visits { 90 | @media #{$xlarge} { 91 | display: none; 92 | } 93 | } 94 | } 95 | 96 | .third-column { 97 | .recent-visits, 98 | .useful-links { 99 | @media #{$large} { 100 | display: none; 101 | } 102 | } 103 | .recent-visits { 104 | @media #{$xlarge} { 105 | display: block; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nwitter", 3 | "description": "A twitter clone written with Node.js, Express, and MongoDB", 4 | "version": "2.1.0", 5 | "repository": "https://github.com/vinitkumar/node-twitter", 6 | "private": false, 7 | "author": "Vinit Kumar (http://vinitkumar.me)", 8 | "scripts": { 9 | "start": "nodemon server.js", 10 | "test": "make test", 11 | "upgrade-interactive": "npm-check --update", 12 | "postinstall": "opencollective postinstall" 13 | }, 14 | "engines": { 15 | "node": "16.0.0", 16 | "npm": "7.10.0" 17 | }, 18 | "dependencies": { 19 | "async": "3.2.6", 20 | "bcrypt": "^5.1.1", 21 | "body-parser": "^1.20.3", 22 | "compression": "^1.7.1", 23 | "connect-flash": "latest", 24 | "connect-mongo": "latest", 25 | "cookie-parser": "^1.4.7", 26 | "cookie-session": "^1.3.3", 27 | "csurf": "^1.10.0", 28 | "dateformat": "^3.0.3", 29 | "dotenv": "^8.6.0", 30 | "errorhandler": "^1.5.0", 31 | "eslint": "^9.9.1", 32 | "express": "^4.21.2", 33 | "express-session": "^1.18.1", 34 | "forever": "latest", 35 | "method-override": "^3.0.0", 36 | "moment": "^2.30.1", 37 | "mongoose": "^8.9.5", 38 | "morgan": "^1.9.0", 39 | "opencollective": "^1.0.3", 40 | "passport": "^0.7.0", 41 | "passport-github": "latest", 42 | "passport-github2": "^0.1.9", 43 | "passport-google-oauth": "latest", 44 | "passport-local": "latest", 45 | "passport-twitter": "~1.0.2", 46 | "promise-retry": "^2.0.1", 47 | "pug": "^3.0.3", 48 | "raven": "^2.1.2", 49 | "serve-favicon": "^2.4.5", 50 | "underscore": "latest", 51 | "view-helpers": "latest", 52 | "winston": "^3.14.2" 53 | }, 54 | "devDependencies": { 55 | "ava": "latest", 56 | "braces": "^3.0.2", 57 | "chai": "^4.2.0", 58 | "grunt": "^1.3.0", 59 | "grunt-contrib-sass": "latest", 60 | "grunt-contrib-watch": "^1.1.0", 61 | "grunt-sass": "latest", 62 | "js-yaml": "^3.14.0", 63 | "mocha": "^10.2.0", 64 | "nodemon": "latest", 65 | "npm-check": "^5.9.2", 66 | "should": "latest", 67 | "supertest": "^6.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/views/pages/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row 5 | .col-xl-3.col-lg-4.first-column 6 | .user-information 7 | .row 8 | .col-4.user-information__image 9 | img(src=req.user.github.avatar_url) 10 | .col-8.user-information__text 11 | a(href="/users/" + req.user.id) 12 | h4 #{req.user.name} 13 | .user-information__stats.user-information__stats_small 14 | ul 15 | li 16 | span.user-information__stat-title Tweets: 17 | span 18 | a(href="/users/" + req.user.id) #{tweetCount} 19 | li 20 | span.user-information__stat-title Following: 21 | span 22 | a(href="/users/" + req.user._id + "/following") #{followingCount} 23 | li 24 | span.user-information__stat-title Followers: 25 | span 26 | a(href="/users/" + req.user._id + "/followers") #{followerCount} 27 | .col-md-12.user-information__stats.user-information__stats_large 28 | ul 29 | li 30 | span.user-information__stat-title Tweets: 31 | span 32 | a(href="/users/" + req.user.id) #{tweetCount} 33 | li 34 | span.user-information__stat-title Following: 35 | span 36 | a(href="/users/" + req.user._id + "/following") #{followingCount} 37 | li 38 | span.user-information__stat-title Followers: 39 | span 40 | a(href="/users/" + req.user._id + "/followers") #{followerCount} 41 | .useful-links 42 | include ../components/useful-links 43 | .recent-visits 44 | include ../components/recent-visits 45 | .col-xl-6.col-lg-8.second-column 46 | include ../components/tweets 47 | include ../components/pagination 48 | 49 | 50 | .col-xl-3.col-lg-4.third-column 51 | .recent-visits.col-xs-6.col-md-12 52 | include ../components/recent-visits 53 | .useful-links.col-xs-6.col-md-12 54 | include ../components/useful-links 55 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const LocalStrategy = require("passport-local").Strategy; 3 | const GitHubStrategy = require("passport-github").Strategy; 4 | const User = mongoose.model("User"); 5 | 6 | module.exports = (passport, config) => { 7 | // require('./initializer') 8 | 9 | // serialize sessions 10 | passport.serializeUser((user, done) => { 11 | done(null, user.id); 12 | }); 13 | 14 | passport.deserializeUser((id, done) => { 15 | User.findOne({ _id: id }, (err, user) => { 16 | done(err, user); 17 | }); 18 | }); 19 | 20 | // use local strategy 21 | passport.use( 22 | new LocalStrategy( 23 | { 24 | usernameField: "email", 25 | passwordField: "password" 26 | }, 27 | (email, password, done) => { 28 | User.findOne({ email: email }, (err, user) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | if (!user) { 33 | return done(null, false, { message: "Unknown user" }); 34 | } 35 | if (!user.authenticate(password)) { 36 | return done(null, false, { message: "Invalid password" }); 37 | } 38 | return done(null, user); 39 | }); 40 | } 41 | ) 42 | ); 43 | 44 | // use github strategy 45 | passport.use( 46 | new GitHubStrategy( 47 | { 48 | clientID: config.github.clientID, 49 | clientSecret: config.github.clientSecret, 50 | callbackURL: config.github.callbackURL 51 | }, 52 | (accessToken, refreshToken, profile, done) => { 53 | const options = { 54 | criteria: { "github.id": parseInt(profile.id) } 55 | }; 56 | User.load(options, (err, user) => { 57 | if (!user) { 58 | user = new User({ 59 | name: profile.displayName, 60 | // email: profile.emails[0].value, 61 | username: profile.username, 62 | provider: "github", 63 | github: profile._json 64 | }); 65 | user.save(err => { 66 | if (err) console.log(err); 67 | return done(err, user); 68 | }); 69 | } else { 70 | User.findOne({ username: profile.username }, function(err, user) { 71 | user.github = profile._json; 72 | user.save(); 73 | return done(err, user); 74 | }); 75 | } 76 | }); 77 | } 78 | ) 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /app/controllers/chat.js: -------------------------------------------------------------------------------- 1 | const createPagination = require("./analytics").createPagination; 2 | const mongoose = require("mongoose"); 3 | const Activity = mongoose.model("Activity"); 4 | const Chat = mongoose.model("Chat"); 5 | const User = mongoose.model("User"); 6 | const logger = require("../middlewares/logger"); 7 | 8 | exports.chat = (req, res, next, id) => { 9 | Chat.load(id, (err, chat) => { 10 | if (err) { 11 | return next(err); 12 | } 13 | if (!chat) { 14 | return next(new Error("Failed to load tweet" + id)); 15 | } 16 | req.chat = chat; 17 | next(); 18 | }); 19 | }; 20 | 21 | exports.index = (req, res) => { 22 | // so basically this is going to be a list of all chats the user had till date. 23 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 24 | const perPage = 10; 25 | const options = { 26 | perPage: perPage, 27 | page: page, 28 | criteria: { github: { $exists: true } } 29 | }; 30 | let users, count, pagination; 31 | User.list(options) 32 | .then(result => { 33 | users = result; 34 | return User.countDocuments() 35 | }) 36 | .then(result => { 37 | count = result; 38 | pagination = createPagination(req, Math.ceil(result / perPage), page + 1); 39 | res.render("chat/index", { 40 | title: "Chat User List", 41 | users: users, 42 | page: page + 1, 43 | pagination: pagination, 44 | pages: Math.ceil(count / perPage) 45 | }); 46 | }) 47 | .catch(error => { 48 | return res.render("pages/500", { errors: error.errors }); 49 | }); 50 | }; 51 | 52 | exports.show = (req, res) => { 53 | res.send(req.chat); 54 | }; 55 | 56 | exports.getChat = (req, res) => { 57 | const options = { 58 | criteria: { receiver: req.params.userid } 59 | }; 60 | let chats; 61 | Chat.list(options).then(result => { 62 | chats = result; 63 | res.render("chat/chat", { chats: chats }); 64 | }); 65 | }; 66 | 67 | exports.create = (req, res) => { 68 | const chat = new Chat({ 69 | message: req.body.body, 70 | receiver: req.body.receiver, 71 | sender: req.user.id 72 | }); 73 | logger.info("chat instance", chat); 74 | chat.save(err => { 75 | const activity = new Activity({ 76 | activityStream: "sent a message to", 77 | activityKey: chat.id, 78 | receiver: req.body.receiver, 79 | sender: req.user.id 80 | }); 81 | activity.save(err => { 82 | if (err) { 83 | logger.error(err); 84 | res.render("pages/500"); 85 | } 86 | }); 87 | logger.error(err); 88 | if (!err) { 89 | res.redirect(req.header("Referrer")); 90 | } 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /app/styles/layout/_navigation.scss: -------------------------------------------------------------------------------- 1 | /* Navbar */ 2 | 3 | @import '../abstracts/variables'; 4 | 5 | // Initially don't display these elements and have the media queries decide if they get displayed 6 | .navbar__main-navigation span, 7 | .navbar__logo { 8 | display: none; 9 | 10 | @media #{$large} { 11 | display: inline-block; 12 | text-align: center; 13 | } 14 | } 15 | 16 | .navbar { 17 | background-color: lighten($yankees-blue, 2.5%); 18 | height: 50px; 19 | margin: 0; 20 | padding: 0; 21 | font-size: 14px; 22 | border-bottom: 1px solid $black; 23 | 24 | .container { 25 | display: flex; 26 | align-items: center; 27 | height: 100%; 28 | 29 | > * { 30 | flex-grow: 1; 31 | } 32 | } 33 | 34 | a { 35 | color: $grey-blue; 36 | } 37 | } 38 | 39 | .navbar__group { 40 | height: 100%; 41 | 42 | &_right { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: flex-end; 46 | align-items: center; 47 | } 48 | } 49 | 50 | .navbar__main-navigation { 51 | display: flex; 52 | align-items: center; 53 | height: 100%; 54 | margin-right: auto; 55 | 56 | li { 57 | margin-right: 5px; 58 | display: flex; 59 | height: 100%; 60 | align-items: center; 61 | 62 | &:hover { 63 | color: $dodger-blue !important; 64 | } 65 | } 66 | 67 | a { 68 | display: flex; 69 | align-items: center; 70 | height: 100%; 71 | padding-left: 8px; 72 | padding-right: 8px; 73 | 74 | &:hover { 75 | color: $dodger-blue !important; 76 | border-bottom: 2px solid $dodger-blue; 77 | 78 | > * { 79 | margin-bottom: -2px; 80 | } 81 | } 82 | i { 83 | margin-right: 4px; 84 | } 85 | } 86 | 87 | .fa { 88 | font-size: 22px; 89 | display: inline-block; 90 | padding: 0; 91 | padding-right: 10px; 92 | padding-left: 10px; 93 | 94 | @media #{$large} { 95 | padding-left: 0; 96 | } 97 | } 98 | } 99 | 100 | .navbar__logo { 101 | position: absolute; 102 | left: 50%; 103 | margin-left: -13px; 104 | 105 | img { 106 | height: 26px; 107 | } 108 | } 109 | 110 | .navbar__profile { 111 | display: flex; 112 | align-items: center; 113 | margin-right: 20px; 114 | 115 | .navbar__profile-logout { 116 | display: flex; 117 | } 118 | 119 | .fa { 120 | font-size: 20px; 121 | padding-right: 10px; 122 | } 123 | } 124 | 125 | .navbar__new-tweet { 126 | text-align: right; 127 | 128 | .navbar__new-tweet-button{ 129 | color: $white; 130 | font-size: 14px; 131 | padding: 7px 14px; 132 | border-radius: 20px; 133 | 134 | &:hover { 135 | color: $white; 136 | } 137 | } 138 | } 139 | 140 | 141 | .navbar__notifications { 142 | color: red !important; 143 | } 144 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | const express = require("express"); 5 | const session = require("express-session"); 6 | const compression = require("compression"); 7 | // const favicon = require('serve-favicon'); 8 | const errorHandler = require("errorhandler"); 9 | const mongoStore = require("connect-mongo")(session); 10 | const flash = require("connect-flash"); 11 | const helpers = require("view-helpers"); 12 | const bodyParser = require("body-parser"); 13 | const methodOverride = require("method-override"); 14 | const cookieParser = require("cookie-parser"); 15 | 16 | const Raven = require("raven"); 17 | // Disable Raven console alerts 18 | Raven.disableConsoleAlerts(); 19 | 20 | const moment = require("moment"); 21 | const morgan = require("morgan"); 22 | 23 | module.exports = (app, config, passport) => { 24 | app.set("showStackError", true); 25 | app.locals.moment = moment; 26 | 27 | // use morgan for logging 28 | app.use( 29 | morgan("dev", { 30 | skip: function(req, res) { 31 | return res.statusCode < 400; 32 | }, 33 | stream: process.stderr 34 | }) 35 | ); 36 | 37 | // use morgan for logging 38 | app.use( 39 | morgan("dev", { 40 | skip: function(req, res) { 41 | return res.statusCode >= 400; 42 | }, 43 | stream: process.stdout 44 | }) 45 | ); 46 | // setup Sentry to get any crashes 47 | if (process.env.SENTRY_DSN !== null) { 48 | Raven.config(process.env.SENTRY_DSN).install(); 49 | app.use(Raven.requestHandler()); 50 | app.use(Raven.errorHandler()); 51 | } 52 | app.use( 53 | compression({ 54 | filter: function(req, res) { 55 | return /json|text|javascript|css/.test(res.getHeader("Content-Type")); 56 | }, 57 | level: 9 58 | }) 59 | ); 60 | // app.use(favicon()); 61 | app.use(express.static(config.root + "/public")); 62 | 63 | if (process.env.NODE_ENV === "development") { 64 | app.use(errorHandler()); 65 | app.locals.pretty = true; 66 | } 67 | 68 | app.set("views", config.root + "/app/views"); 69 | app.set("view engine", "pug"); 70 | 71 | app.use(helpers(config.app.name)); 72 | app.use(cookieParser()); 73 | app.use( 74 | bodyParser.urlencoded({ 75 | extended: true 76 | }) 77 | ); 78 | app.use(bodyParser.json()); 79 | app.use(methodOverride("_method")); 80 | app.use( 81 | session({ 82 | secret: process.env.SECRET, 83 | resave: false, 84 | saveUninitialized: false, 85 | store: new mongoStore({ 86 | url: config.db, 87 | collection: "sessions" 88 | }) 89 | }) 90 | ); 91 | 92 | app.use(flash()); 93 | app.use(passport.initialize()); 94 | app.use(passport.session()); 95 | app.disable('view cache'); 96 | app.use((err, req, res, next) => { 97 | if (err.message.indexOf("not found") !== -1) { 98 | return next(); 99 | } 100 | console.log(err.stack); 101 | 102 | res.status(500).render("pages/500", { error: err.stack }); 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /public/css/pygments-manni.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | /*{ background: #f0f3f3; }*/ 3 | .c { color: #999; } /* Comment */ 4 | .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 5 | .k { color: #006699; } /* Keyword */ 6 | .o { color: #555555 } /* Operator */ 7 | .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 8 | .cp { color: #009999 } /* Comment.Preproc */ 9 | .c1 { color: #999; } /* Comment.Single */ 10 | .cs { color: #999; } /* Comment.Special */ 11 | .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 12 | .ge { font-style: italic } /* Generic.Emph */ 13 | .gr { color: #FF0000 } /* Generic.Error */ 14 | .gh { color: #003300; } /* Generic.Heading */ 15 | .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 16 | .go { color: #AAAAAA } /* Generic.Output */ 17 | .gp { color: #000099; } /* Generic.Prompt */ 18 | .gs { } /* Generic.Strong */ 19 | .gu { color: #003300; } /* Generic.Subheading */ 20 | .gt { color: #99CC66 } /* Generic.Traceback */ 21 | .kc { color: #006699; } /* Keyword.Constant */ 22 | .kd { color: #006699; } /* Keyword.Declaration */ 23 | .kn { color: #006699; } /* Keyword.Namespace */ 24 | .kp { color: #006699 } /* Keyword.Pseudo */ 25 | .kr { color: #006699; } /* Keyword.Reserved */ 26 | .kt { color: #007788; } /* Keyword.Type */ 27 | .m { color: #FF6600 } /* Literal.Number */ 28 | .s { color: #d44950 } /* Literal.String */ 29 | .na { color: #4f9fcf } /* Name.Attribute */ 30 | .nb { color: #336666 } /* Name.Builtin */ 31 | .nc { color: #00AA88; } /* Name.Class */ 32 | .no { color: #336600 } /* Name.Constant */ 33 | .nd { color: #9999FF } /* Name.Decorator */ 34 | .ni { color: #999999; } /* Name.Entity */ 35 | .ne { color: #CC0000; } /* Name.Exception */ 36 | .nf { color: #CC00FF } /* Name.Function */ 37 | .nl { color: #9999FF } /* Name.Label */ 38 | .nn { color: #00CCFF; } /* Name.Namespace */ 39 | .nt { color: #2f6f9f; } /* Name.Tag */ 40 | .nv { color: #003333 } /* Name.Variable */ 41 | .ow { color: #000000; } /* Operator.Word */ 42 | .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .mf { color: #FF6600 } /* Literal.Number.Float */ 44 | .mh { color: #FF6600 } /* Literal.Number.Hex */ 45 | .mi { color: #FF6600 } /* Literal.Number.Integer */ 46 | .mo { color: #FF6600 } /* Literal.Number.Oct */ 47 | .sb { color: #CC3300 } /* Literal.String.Backtick */ 48 | .sc { color: #CC3300 } /* Literal.String.Char */ 49 | .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 50 | .s2 { color: #CC3300 } /* Literal.String.Double */ 51 | .se { color: #CC3300; } /* Literal.String.Escape */ 52 | .sh { color: #CC3300 } /* Literal.String.Heredoc */ 53 | .si { color: #AA0000 } /* Literal.String.Interpol */ 54 | .sx { color: #CC3300 } /* Literal.String.Other */ 55 | .sr { color: #33AAAA } /* Literal.String.Regex */ 56 | .s1 { color: #CC3300 } /* Literal.String.Single */ 57 | .ss { color: #FFCC33 } /* Literal.String.Symbol */ 58 | .bp { color: #336666 } /* Name.Builtin.Pseudo */ 59 | .vc { color: #003333 } /* Name.Variable.Class */ 60 | .vg { color: #003333 } /* Name.Variable.Global */ 61 | .vi { color: #003333 } /* Name.Variable.Instance */ 62 | .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 63 | 64 | .css .o, 65 | .css .o + .nt, 66 | .css .nt + .nt { color: #999; } 67 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('.favorite').on('click', function(e) { 3 | const tweetID = $(e.currentTarget).data('tweetid'); 4 | const url = 'tweets/' + tweetID + '/favorites'; 5 | $.ajax({ 6 | type: 'POST', 7 | url: url, 8 | success: function(data) { 9 | console.log('send a favorite'); 10 | }, 11 | error: function(data) { 12 | console.log('not sent'); 13 | }, 14 | }); 15 | }); 16 | 17 | $('.profile__follow-button').on('click', function(e) { 18 | const userID = $(e.currentTarget).data('userid'); 19 | const url = '/users/' + userID + '/follow'; 20 | if ($(this).hasClass('following')) { 21 | $(this).text('Follow'); 22 | $(this).removeClass('following'); 23 | } else { 24 | $(this).text('Unfollow'); 25 | $(this).addClass('following'); 26 | } 27 | $.ajax({ 28 | type: 'POST', 29 | url: url, 30 | success: function(data) { 31 | console.log('Followed the user'); 32 | }, 33 | error: function(data) { 34 | console.log('not sent'); 35 | }, 36 | }); 37 | }); 38 | 39 | $('.tweet__edit').on('click', function(e) { 40 | e.preventDefault(); 41 | let $editButton = $(e.target); 42 | if ($editButton.hasClass('tweet__edit')) { 43 | // Change "edit" to "save" on the button 44 | $editButton 45 | .text('Save') 46 | .removeClass('tweet__edit') 47 | .addClass('tweet__save'); 48 | // Get the tweet content text 49 | let $originalTweet = $editButton 50 | .parent() 51 | .siblings('.tweet__content'); 52 | let tweetText = $originalTweet.text(); 53 | // Replace the tweet text element with a textarea element 54 | let $modifiedText = $('') 55 | .addClass('edit-tweet') 56 | .val(tweetText) 57 | .attr('placeholder', tweetText); 58 | $originalTweet.after($modifiedText).remove(); 59 | } else if ($editButton.hasClass('tweet__save')) { 60 | // Change "save" to "edit" on the button 61 | $editButton 62 | .text('Edit') 63 | .removeClass('tweet__save') 64 | .addClass('tweet__edit'); 65 | let $modifiedTweet = $(e.target) 66 | .parent() 67 | .siblings('textarea'); 68 | let originalText = $modifiedTweet.attr('placeholder'); 69 | let modifiedText = $modifiedTweet.val(); 70 | if (modifiedText !== originalText) { 71 | // Make a PUT request to /tweets/:id 72 | let tweetId = $editButton.closest('.tweet').attr('data-tweetId'); 73 | $.ajax($editButton.attr('href'), { 74 | method: 'POST', 75 | data: {id: tweetId, tweet: modifiedText}, 76 | success: function(data) {}, 77 | error: function(data) {}, 78 | }); 79 | } 80 | 81 | // The same replace method in tweeet.pug file 82 | // TODO: find solution to reuse the same code 83 | modifiedText = modifiedText.replace(/#(\w+)/g, '#$1'); 84 | let $tweetElement = $('') 85 | .addClass('tweet__content'); 86 | //.text(modifiedText); 87 | $tweetElement.append(modifiedText); 88 | $modifiedTweet.after($tweetElement).remove(); 89 | } 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vinit1414.08@bitmesra.ac.in. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Tweet = mongoose.model("Tweet"); 3 | const Schema = mongoose.Schema; 4 | const bcrypt = require('bcrypt'); 5 | const authTypes = ['github']; 6 | 7 | // ## Define UserSchema 8 | const UserSchema = new Schema( 9 | { 10 | name: String, 11 | email: String, 12 | username: String, 13 | provider: String, 14 | hashedPassword: String, 15 | salt: String, 16 | github: {}, 17 | followers: [{ type: Schema.ObjectId, ref: "User" }], 18 | following: [{ type: Schema.ObjectId, ref: "User" }], 19 | tweets: Number 20 | }, 21 | { usePushEach: true } 22 | ); 23 | 24 | UserSchema.virtual("password") 25 | .set(function(password) { 26 | this._password = password; 27 | this.salt = this.makeSalt(); 28 | this.hashedPassword = this.encryptPassword(password); 29 | }) 30 | .get(function() { 31 | return this._password; 32 | }); 33 | 34 | const validatePresenceOf = value => value && value.length; 35 | 36 | UserSchema.path("name").validate(function(name) { 37 | if (authTypes.indexOf(this.provider) !== -1) { 38 | return true; 39 | } 40 | return name.length; 41 | }, "Name cannot be blank"); 42 | 43 | UserSchema.path("email").validate(function(email) { 44 | if (authTypes.indexOf(this.provider) !== -1) { 45 | return true; 46 | } 47 | return email.length; 48 | }, "Email cannot be blank"); 49 | 50 | UserSchema.path("username").validate(function(username) { 51 | if (authTypes.indexOf(this.provider) !== -1) { 52 | return true; 53 | } 54 | return username.length; 55 | }, "username cannot be blank"); 56 | 57 | UserSchema.path("hashedPassword").validate(function(hashedPassword) { 58 | if (authTypes.indexOf(this.provider) !== -1) { 59 | return true; 60 | } 61 | return hashedPassword.length; 62 | }, "Password cannot be blank"); 63 | 64 | UserSchema.pre("save", function(next) { 65 | if ( 66 | !validatePresenceOf(this.password) && 67 | authTypes.indexOf(this.provider) === -1 68 | ) { 69 | next(new Error("Invalid password")); 70 | } else { 71 | next(); 72 | } 73 | }); 74 | 75 | UserSchema.methods = { 76 | authenticate: function(plainText) { 77 | return this.encryptPassword(plainText) === this.hashedPassword; 78 | }, 79 | 80 | makeSalt: function() { 81 | return Math.round(new Date().valueOf() * Math.random()); 82 | }, 83 | 84 | encryptPassword: function(password) { 85 | if (!password) { 86 | return ""; 87 | } 88 | let salt = this.makeSalt(); 89 | return bcrypt.hashSync(password, salt) 90 | }, 91 | }; 92 | 93 | UserSchema.statics = { 94 | addfollow: function(id, cb) { 95 | this.findOne({ _id: id }) 96 | .populate("followers") 97 | .exec(cb); 98 | }, 99 | countUserTweets: function(id, cb) { 100 | return Tweet.find({ user: id }) 101 | .countDocuments() 102 | .exec(cb); 103 | }, 104 | load: function(options, cb) { 105 | options.select = options.select || "name username github"; 106 | return this.findOne(options.criteria) 107 | .select(options.select) 108 | .exec(cb); 109 | }, 110 | list: function(options) { 111 | const criteria = options.criteria || {}; 112 | return this.find(criteria) 113 | .populate("user", "name username") 114 | .limit(options.perPage) 115 | .skip(options.perPage * options.page); 116 | }, 117 | countTotalUsers: function() { 118 | return this.find({}).countDocuments(); 119 | } 120 | }; 121 | 122 | mongoose.model("User", UserSchema); 123 | -------------------------------------------------------------------------------- /app/controllers/tweets.js: -------------------------------------------------------------------------------- 1 | // ## Tweet Controller 2 | const createPagination = require("./analytics").createPagination; 3 | const mongoose = require("mongoose"); 4 | const Tweet = mongoose.model("Tweet"); 5 | const User = mongoose.model("User"); 6 | const Analytics = mongoose.model("Analytics"); 7 | const _ = require("underscore"); 8 | const logger = require("../middlewares/logger"); 9 | 10 | exports.tweet = (req, res, next, id) => { 11 | Tweet.load(id, (err, tweet) => { 12 | if (err) { 13 | return next(err); 14 | } 15 | if (!tweet) { 16 | return next(new Error("Failed to load tweet" + id)); 17 | } 18 | req.tweet = tweet; 19 | next(); 20 | }); 21 | }; 22 | 23 | // ### Create a Tweet 24 | exports.create = (req, res) => { 25 | const tweet = new Tweet(req.body); 26 | tweet.user = req.user; 27 | tweet.tags = parseHashtag(req.body.body); 28 | 29 | tweet.uploadAndSave({}, err => { 30 | if (err) { 31 | res.render("pages/500", { error: err }); 32 | } else { 33 | res.redirect("/"); 34 | } 35 | }); 36 | }; 37 | 38 | // ### Update a tweet 39 | exports.update = (req, res) => { 40 | let tweet = req.tweet; 41 | tweet = _.extend(tweet, { body: req.body.tweet }); 42 | tweet.uploadAndSave({}, err => { 43 | if (err) { 44 | return res.render("pages/500", { error: err }); 45 | } 46 | res.redirect("/"); 47 | }); 48 | }; 49 | 50 | // ### Delete a tweet 51 | exports.destroy = (req, res) => { 52 | const tweet = req.tweet; 53 | tweet.remove(err => { 54 | if (err) { 55 | return res.render("pages/500"); 56 | } 57 | res.redirect("/"); 58 | }); 59 | }; 60 | 61 | // ### Parse a hashtag 62 | 63 | function parseHashtag(inputText) { 64 | var regex = /(?:^|\s)(?:#)([a-zA-Z\d]+)/g; 65 | var matches = []; 66 | var match; 67 | while ((match = regex.exec(inputText)) !== null) { 68 | matches.push(match[1]); 69 | } 70 | return matches; 71 | } 72 | 73 | exports.parseHashtag = parseHashtag; 74 | 75 | let showTweets = (req, res, criteria) => { 76 | const findCriteria = criteria || {}; 77 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 78 | const perPage = 10; 79 | const options = { 80 | perPage: perPage, 81 | page: page, 82 | criteria: findCriteria 83 | }; 84 | let followingCount = req.user.following.length; 85 | let followerCount = req.user.followers.length; 86 | let tweets, tweetCount, pageViews, analytics, pagination; 87 | User.countUserTweets(req.user._id).then(result => { 88 | tweetCount = result; 89 | }); 90 | Tweet.list(options) 91 | .then(result => { 92 | tweets = result; 93 | return Tweet.countTweets(findCriteria); 94 | }) 95 | .then(result => { 96 | pageViews = result; 97 | pagination = createPagination( 98 | req, 99 | Math.ceil(pageViews / perPage), 100 | page + 1 101 | ); 102 | return Analytics.list({ perPage: 15 }); 103 | }) 104 | .then(result => { 105 | analytics = result; 106 | res.render("pages/index", { 107 | title: "List of Tweets", 108 | tweets: tweets, 109 | analytics: analytics, 110 | page: page + 1, 111 | tweetCount: tweetCount, 112 | pagination: pagination, 113 | followerCount: followerCount, 114 | followingCount: followingCount, 115 | pages: Math.ceil(pageViews / perPage) 116 | }); 117 | }) 118 | .catch(error => { 119 | logger.error(error); 120 | res.render("pages/500"); 121 | }); 122 | }; 123 | 124 | // ### Find a tag 125 | exports.findTag = (req, res) => { 126 | let tag = req.params.tag; 127 | showTweets(req, res, { tags: tag.toLowerCase() }); 128 | }; 129 | 130 | exports.index = (req, res) => { 131 | showTweets(req, res); 132 | }; 133 | -------------------------------------------------------------------------------- /public/css/bootstrap/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const log = require("./middlewares/logger"); 4 | 5 | const users = require("../app/controllers/users"); 6 | const apiv1 = require("../app/controllers/apiv1"); 7 | const chat = require("../app/controllers/chat"); 8 | const analytics = require("../app/controllers/analytics"); 9 | const tweets = require("../app/controllers/tweets"); 10 | const comments = require("../app/controllers/comments"); 11 | const favorites = require("../app/controllers/favorites"); 12 | const follows = require("../app/controllers/follows"); 13 | const activity = require("../app/controllers/activity"); 14 | 15 | module.exports = (app, passport, auth) => { 16 | app.use("/", router); 17 | /** 18 | * Main unauthenticated routes 19 | */ 20 | router.get("/login", users.login); 21 | router.get("/signup", users.signup); 22 | router.get("/logout", users.logout); 23 | 24 | /** 25 | * Authentication routes 26 | */ 27 | router.get( 28 | "/auth/github", 29 | passport.authenticate("github", { failureRedirect: "/login" }), 30 | users.signin 31 | ); 32 | router.get( 33 | "/auth/github/callback", 34 | passport.authenticate("github", { failureRedirect: "/login" }), 35 | users.authCallback 36 | ); 37 | 38 | /** 39 | * API routes 40 | */ 41 | router.get("/apiv1/tweets", apiv1.tweetList); 42 | router.get("/apiv1/users", apiv1.usersList); 43 | 44 | /** 45 | * Authentication middleware 46 | * All routes specified after this middleware require authentication in order 47 | * to access 48 | */ 49 | router.use(auth.requiresLogin); 50 | /** 51 | * Analytics logging middleware 52 | * Anytime an authorized user makes a get request, it will be logged into 53 | * analytics 54 | */ 55 | router.get("/*", log.analytics); 56 | 57 | /** 58 | * Acivity routes 59 | */ 60 | router.get("/activities", activity.index); 61 | /** 62 | * Home route 63 | */ 64 | router.get("/", tweets.index); 65 | /** 66 | * User routes 67 | */ 68 | router.get("/users/:userId", users.show); 69 | router.get("/users/:userId/followers", users.showFollowers); 70 | router.get("/users/:userId/following", users.showFollowing); 71 | router.post("/users", users.create); 72 | router.post( 73 | "/users/sessions", 74 | passport.authenticate("local", { 75 | failureRedirect: "/login", 76 | failureFlash: "Invalid email or password" 77 | }), 78 | users.session 79 | ); 80 | router.post("/users/:userId/follow", follows.follow); 81 | router.post("/users/:userId/delete", users.delete); 82 | router.param("userId", users.user); 83 | 84 | /** 85 | * Chat routes 86 | */ 87 | router.get("/chat", chat.index); 88 | router.get("/chat/:id", chat.show); 89 | router.get("/chat/get/:userid", chat.getChat); 90 | router.post("/chats", chat.create); 91 | /** 92 | * Analytics routes 93 | */ 94 | router.get("/analytics", analytics.index); 95 | 96 | /** 97 | * Tweet routes 98 | */ 99 | router 100 | .route("/tweets") 101 | .get(tweets.index) 102 | .post(tweets.create); 103 | 104 | router 105 | .route("/tweets/:id") 106 | .post(auth.tweet.hasAuthorization, tweets.update) 107 | .delete(auth.tweet.hasAuthorization, tweets.destroy); 108 | 109 | router.param("id", tweets.tweet); 110 | 111 | /** 112 | * Comment routes 113 | */ 114 | router 115 | .route("/tweets/:id/comments") 116 | .get(comments.create) 117 | .post(comments.create) 118 | .delete(comments.destroy); 119 | 120 | /** 121 | * Favorite routes 122 | */ 123 | router 124 | .route("/tweets/:id/favorites") 125 | .post(favorites.create) 126 | .delete(favorites.destroy); 127 | 128 | /** 129 | * Find tags 130 | */ 131 | router 132 | .route("/tweets/hashtag/:tag") 133 | .get(tweets.findTag); 134 | 135 | /** 136 | * Page not found route (must be at the end of all routes) 137 | */ 138 | router.use((req, res) => { 139 | res.status(404).render("pages/404", { 140 | url: req.originalUrl, 141 | error: "Not found" 142 | }); 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Introduction 4 | 5 | First, thank you for considering contributing to node-twitter! It's people like you that make the open source community such a great community! 😊 6 | 7 | We welcome any type of contribution, not only code. You can help with 8 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 9 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 10 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 11 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 12 | - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-twitter). 13 | 14 | ## Your First Contribution 15 | 16 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 17 | 18 | ## Submitting code 19 | 20 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 21 | 22 | ## Code review process 23 | 24 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 25 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 26 | 27 | ## Financial contributions 28 | 29 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-twitter). 30 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 31 | 32 | ## Questions 33 | 34 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 35 | You can also reach us at hello@node-twitter.opencollective.com. 36 | 37 | ## Credits 38 | 39 | ### Contributors 40 | 41 | Thank you to all the people who have already contributed to node-twitter! 42 | 43 | 44 | 45 | ### Backers 46 | 47 | Thank you to all our backers! [[Become a backer](https://opencollective.com/node-twitter#backer)] 48 | 49 | 50 | 51 | 52 | ### Sponsors 53 | 54 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/node-twitter#sponsor)) 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/models/tweets.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const utils = require("../../lib/utils"); 4 | 5 | // Getters and Setters 6 | const setTags = tags => tags.map(t => t.toLowerCase()); 7 | 8 | // Tweet Schema 9 | const TweetSchema = new Schema( 10 | { 11 | body: { type: String, default: "", trim: true, maxlength: 280 }, 12 | user: { type: Schema.ObjectId, ref: "User" }, 13 | comments: [ 14 | { 15 | body: { type: String, default: "", maxlength: 280 }, 16 | user: { type: Schema.ObjectId, ref: "User" }, 17 | commenterName: { type: String, default: "" }, 18 | commenterPicture: { type: String, default: "" }, 19 | createdAt: { type: Date, default: Date.now } 20 | } 21 | ], 22 | tags: { type: [String], set: setTags }, 23 | favorites: [{ type: Schema.ObjectId, ref: "User" }], 24 | favoriters: [{ type: Schema.ObjectId, ref: "User" }], // same as favorites 25 | favoritesCount: Number, 26 | createdAt: { type: Date, default: Date.now } 27 | }, 28 | { usePushEach: true } 29 | ); 30 | 31 | // Pre save hook 32 | TweetSchema.pre("save", function(next) { 33 | if (this.favorites) { 34 | this.favoritesCount = this.favorites.length; 35 | } 36 | if (this.favorites) { 37 | this.favoriters = this.favorites; 38 | } 39 | next(); 40 | }); 41 | 42 | // Validations in the schema 43 | TweetSchema.path("body").validate( 44 | body => body.length > 0, 45 | "Tweet body cannot be blank" 46 | ); 47 | 48 | TweetSchema.virtual("_favorites").set(function(user) { 49 | if (this.favorites.indexOf(user._id) === -1) { 50 | this.favorites.push(user._id); 51 | } else { 52 | this.favorites.splice(this.favorites.indexOf(user._id), 1); 53 | } 54 | }); 55 | 56 | TweetSchema.methods = { 57 | uploadAndSave: function(images, callback) { 58 | // const imager = new Imager(imagerConfig, "S3"); 59 | const self = this; 60 | if (!images || !images.length) { 61 | return this.save(callback); 62 | } 63 | imager.upload( 64 | images, 65 | (err, cdnUri, files) => { 66 | if (err) { 67 | return callback(err); 68 | } 69 | if (files.length) { 70 | self.image = { cdnUri: cdnUri, files: files }; 71 | } 72 | self.save(callback); 73 | }, 74 | "article" 75 | ); 76 | }, 77 | addComment: function(user, comment, cb) { 78 | if (user.name) { 79 | this.comments.push({ 80 | body: comment.body, 81 | user: user._id, 82 | commenterName: user.name, 83 | commenterPicture: user.github.avatar_url 84 | }); 85 | this.save(cb); 86 | } else { 87 | this.comments.push({ 88 | body: comment.body, 89 | user: user._id, 90 | commenterName: user.username, 91 | commenterPicture: user.github.avatar_url 92 | }); 93 | 94 | this.save(cb); 95 | } 96 | }, 97 | 98 | removeComment: function(commentId, cb) { 99 | let index = utils.indexof(this.comments, { id: commentId }); 100 | if (~index) { 101 | this.comments.splice(index, 1); 102 | } else { 103 | return cb("not found"); 104 | } 105 | this.save(cb); 106 | } 107 | }; 108 | 109 | // ## Static Methods in the TweetSchema 110 | TweetSchema.statics = { 111 | // Load tweets 112 | load: function(id, callback) { 113 | this.findOne({ _id: id }) 114 | .populate("user", "name username provider github") 115 | .populate("comments.user") 116 | .exec(callback); 117 | }, 118 | // List tweets 119 | list: function(options) { 120 | const criteria = options.criteria || {}; 121 | return this.find(criteria) 122 | .populate("user", "name username provider github") 123 | .sort({ createdAt: -1 }) 124 | .limit(options.perPage) 125 | .skip(options.perPage * options.page); 126 | }, 127 | // List tweets 128 | limitedList: function(options) { 129 | const criteria = options.criteria || {}; 130 | return this.find(criteria) 131 | .populate("user", "name username") 132 | .sort({ createdAt: -1 }) 133 | .limit(options.perPage) 134 | .skip(options.perPage * options.page); 135 | }, 136 | // Tweets of User 137 | userTweets: function(id, callback) { 138 | this.find({ user: ObjectId(id) }) 139 | .toArray() 140 | .exec(callback); 141 | }, 142 | 143 | // Count the number of tweets for a specific user 144 | countUserTweets: function(id, callback) { 145 | return this.find({ user: id }) 146 | .countDocuments() 147 | .exec(callback); 148 | }, 149 | 150 | // Count the app tweets by criteria 151 | countTweets: function(criteria) { 152 | return this.find(criteria).countDocuments(); 153 | } 154 | }; 155 | 156 | mongoose.model("Tweet", TweetSchema); 157 | -------------------------------------------------------------------------------- /app/controllers/analytics.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Analytics = mongoose.model("Analytics"); 3 | const Tweet = mongoose.model("Tweet"); 4 | const User = mongoose.model("User"); 5 | const qs = require("querystring"); 6 | const url = require("url"); 7 | const logger = require("../middlewares/logger"); 8 | 9 | exports.createPagination = (req, pages, page) => { 10 | let params = qs.parse(url.parse(req.url).query); 11 | let str = ""; 12 | let pageNumberClass; 13 | let pageCutLow = page - 1; 14 | let pageCutHigh = page + 1; 15 | // Show the Previous button only if you are on a page other than the first 16 | if (page > 1) { 17 | str += 18 | 'Previous'; 21 | } 22 | // Show all the pagination elements if there are less than 6 pages total 23 | if (pages < 6) { 24 | for (let p = 1; p <= pages; p++) { 25 | params.page = p; 26 | pageNumberClass = page === p ? "active" : "no"; 27 | let href = "?" + qs.stringify(params); 28 | str += 29 | '' + 34 | p + 35 | ""; 36 | } 37 | } 38 | // Use "..." to collapse pages outside of a certain range 39 | else { 40 | // Show the very first page followed by a "..." at the beginning of the 41 | // pagination section (after the Previous button) 42 | if (page > 2) { 43 | str += 44 | '1'; 45 | if (page > 3) { 46 | str += '...'; 47 | } 48 | } 49 | // Determine how many pages to show after the current page index 50 | if (page === 1) { 51 | pageCutHigh += 2; 52 | } else if (page === 2) { 53 | pageCutHigh += 1; 54 | } 55 | // Determine how many pages to show before the current page index 56 | if (page === pages) { 57 | pageCutLow -= 2; 58 | } else if (page === pages - 1) { 59 | pageCutLow -= 1; 60 | } 61 | // Output the indexes for pages that fall inside the range of pageCutLow 62 | // and pageCutHigh 63 | for (let p = pageCutLow; p <= pageCutHigh; p++) { 64 | if (p === 0) { 65 | p += 1; 66 | } 67 | if (p > pages) { 68 | continue; 69 | } 70 | params.page = p; 71 | pageNumberClass = page === p ? "active" : "no"; 72 | let href = "?" + qs.stringify(params); 73 | str += 74 | '' + 79 | p + 80 | ""; 81 | } 82 | // Show the very last page preceded by a "..." at the end of the pagination 83 | // section (before the Next button) 84 | if (page < pages - 1) { 85 | if (page < pages - 2) { 86 | str += '...'; 87 | } 88 | str += 89 | '' + 92 | pages + 93 | ""; 94 | } 95 | } 96 | // Show the Next button only if you are on a page other than the last 97 | if (page < pages) { 98 | str += 99 | 'Next'; 102 | } 103 | // Return the pagination string to be outputted in the pug templates 104 | return str; 105 | }; 106 | 107 | exports.index = (req, res) => { 108 | let createPagination = exports.createPagination; 109 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 110 | const perPage = 10; 111 | const options = { 112 | perPage: perPage, 113 | page: page 114 | }; 115 | 116 | let analytics, pageViews, tweetCount, pagination, userCount; 117 | 118 | Analytics.list(options) 119 | .then(result => { 120 | analytics = result; 121 | return Analytics.countDocuments(); 122 | }) 123 | .then(result => { 124 | pageViews = result; 125 | pagination = createPagination( 126 | req, 127 | Math.ceil(pageViews / perPage), 128 | page + 1 129 | ); 130 | return Tweet.countTweets(); 131 | }) 132 | .then(result => { 133 | tweetCount = result; 134 | return User.countTotalUsers(); 135 | }) 136 | .then(result => { 137 | userCount = result; 138 | res.render("pages/analytics", { 139 | title: "List of users", 140 | analytics: analytics, 141 | pageViews: pageViews, 142 | userCount: userCount, 143 | tweetCount: tweetCount, 144 | pagination: pagination, 145 | pages: Math.ceil(pageViews / perPage) 146 | }); 147 | }) 148 | .catch(error => { 149 | logger.error(error); 150 | return res.render("pages/500"); 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /app/controllers/users.js: -------------------------------------------------------------------------------- 1 | const Mongoose = require("mongoose"); 2 | const Tweet = Mongoose.model("Tweet"); 3 | const User = Mongoose.model("User"); 4 | const Analytics = Mongoose.model("Analytics"); 5 | const logger = require("../middlewares/logger"); 6 | 7 | exports.signin = (req, res) => {}; 8 | 9 | exports.authCallback = (req, res) => { 10 | res.redirect("/"); 11 | }; 12 | 13 | exports.login = (req, res) => { 14 | let tweetCount, userCount, analyticsCount; 15 | let options = {}; 16 | Analytics.list(options) 17 | .then(() => { 18 | return Analytics.countDocuments(); 19 | }) 20 | .then(result => { 21 | analyticsCount = result; 22 | return Tweet.countTweets(); 23 | }) 24 | .then(result => { 25 | tweetCount = result; 26 | return User.countTotalUsers(); 27 | }) 28 | .then(result => { 29 | userCount = result; 30 | logger.info(tweetCount); 31 | logger.info(userCount); 32 | logger.info(tweetCount); 33 | res.render("pages/login", { 34 | title: "Login", 35 | message: req.flash("error"), 36 | userCount: userCount, 37 | tweetCount: tweetCount, 38 | analyticsCount: analyticsCount 39 | }); 40 | }); 41 | }; 42 | 43 | exports.signup = (req, res) => { 44 | res.render("pages/login", { 45 | title: "Sign up", 46 | user: new User() 47 | }); 48 | }; 49 | 50 | exports.logout = (req, res) => { 51 | req.logout(); 52 | res.redirect("/login"); 53 | }; 54 | 55 | exports.session = (req, res) => { 56 | res.redirect("/"); 57 | }; 58 | 59 | exports.create = (req, res, next) => { 60 | const user = new User(req.body); 61 | user.provider = "local"; 62 | user 63 | .save() 64 | .catch(error => { 65 | return res.render("pages/login", { errors: error.errors, user: user }); 66 | }) 67 | .then(() => { 68 | return req.login(user); 69 | }) 70 | .then(() => { 71 | return res.redirect("/"); 72 | }) 73 | .catch(error => { 74 | return next(error); 75 | }); 76 | }; 77 | 78 | exports.list = (req, res) => { 79 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 80 | const perPage = 5; 81 | const options = { 82 | perPage: perPage, 83 | page: page, 84 | criteria: { github: { $exists: true } } 85 | }; 86 | let users, count; 87 | User.list(options) 88 | .then(result => { 89 | users = result; 90 | return User.countDocuments(); 91 | }) 92 | .then(result => { 93 | count = result; 94 | res.render("pages/user-list", { 95 | title: "List of Users", 96 | users: users, 97 | page: page + 1, 98 | pages: Math.ceil(count / perPage) 99 | }); 100 | }) 101 | .catch(error => { 102 | return res.render("pages/500", { errors: error.errors }); 103 | }); 104 | }; 105 | 106 | exports.show = (req, res) => { 107 | const user = req.profile; 108 | const reqUserId = user._id; 109 | const userId = reqUserId.toString(); 110 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 111 | const options = { 112 | perPage: 100, 113 | page: page, 114 | criteria: { user: userId } 115 | }; 116 | let tweets, tweetCount; 117 | let followingCount = user.following.length; 118 | let followerCount = user.followers.length; 119 | 120 | Tweet.list(options) 121 | .then(result => { 122 | tweets = result; 123 | return Tweet.countUserTweets(reqUserId); 124 | }) 125 | .then(result => { 126 | tweetCount = result; 127 | res.render("pages/profile", { 128 | title: "Tweets from " + user.name, 129 | user: user, 130 | tweets: tweets, 131 | tweetCount: tweetCount, 132 | followerCount: followerCount, 133 | followingCount: followingCount 134 | }); 135 | }) 136 | .catch(error => { 137 | return res.render("pages/500", { errors: error.errors }); 138 | }); 139 | }; 140 | 141 | exports.user = (req, res, next, id) => { 142 | User.findOne({ _id: id }).exec((err, user) => { 143 | if (err) { 144 | return next(err); 145 | } 146 | if (!user) { 147 | return next(new Error("failed to load user " + id)); 148 | } 149 | req.profile = user; 150 | next(); 151 | }); 152 | }; 153 | 154 | exports.showFollowers = (req, res) => { 155 | showFollowers(req, res, "followers"); 156 | }; 157 | 158 | exports.showFollowing = (req, res) => { 159 | showFollowers(req, res, "following"); 160 | }; 161 | 162 | exports.delete = (req, res) => { 163 | Tweet.remove({ user: req.user._id }) 164 | .then(() => { 165 | User.findByIdAndRemove(req.user._id) 166 | .then(() => { 167 | return res.redirect("/login"); 168 | }) 169 | .catch(() => { 170 | res.render("pages/500"); 171 | }); 172 | }) 173 | .catch(() => { 174 | res.render("pages/500"); 175 | }); 176 | }; 177 | 178 | function showFollowers(req, res, type) { 179 | let user = req.profile; 180 | let followers = user[type]; 181 | let tweetCount; 182 | let followingCount = user.following.length; 183 | let followerCount = user.followers.length; 184 | let userFollowers = User.find({ _id: { $in: followers } }).populate( 185 | "user", 186 | "_id name username github" 187 | ); 188 | 189 | Tweet.countUserTweets(user._id).then(result => { 190 | tweetCount = result; 191 | userFollowers.exec((err, users) => { 192 | if (err) { 193 | return res.render("pages/500"); 194 | } 195 | res.render("pages/followers", { 196 | user: user, 197 | followers: users, 198 | tweetCount: tweetCount, 199 | followerCount: followerCount, 200 | followingCount: followingCount 201 | }); 202 | }); 203 | }); 204 | } 205 | -------------------------------------------------------------------------------- /public/css/bootstrap/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | text-align: left; 39 | background-color: #fff; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: 0 !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | text-decoration: underline; 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | border-bottom: 0; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | font-size: 75%; 121 | line-height: 0; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | color: #007bff; 135 | text-decoration: none; 136 | background-color: transparent; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-family: monospace, monospace; 164 | font-size: 1em; 165 | } 166 | 167 | pre { 168 | margin-top: 0; 169 | margin-bottom: 1rem; 170 | overflow: auto; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | vertical-align: middle; 180 | border-style: none; 181 | } 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | table { 188 | border-collapse: collapse; 189 | } 190 | 191 | caption { 192 | padding-top: 0.75rem; 193 | padding-bottom: 0.75rem; 194 | color: #6c757d; 195 | text-align: left; 196 | caption-side: bottom; 197 | } 198 | 199 | th { 200 | text-align: inherit; 201 | } 202 | 203 | label { 204 | display: inline-block; 205 | margin-bottom: .5rem; 206 | } 207 | 208 | button { 209 | border-radius: 0; 210 | } 211 | 212 | button:focus { 213 | outline: 1px dotted; 214 | outline: 5px auto -webkit-focus-ring-color; 215 | } 216 | 217 | input, 218 | button, 219 | select, 220 | optgroup, 221 | textarea { 222 | margin: 0; 223 | font-family: inherit; 224 | font-size: inherit; 225 | line-height: inherit; 226 | } 227 | 228 | button, 229 | input { 230 | overflow: visible; 231 | } 232 | 233 | button, 234 | select { 235 | text-transform: none; 236 | } 237 | 238 | button, 239 | html [type="button"], 240 | [type="reset"], 241 | [type="submit"] { 242 | -webkit-appearance: button; 243 | } 244 | 245 | button::-moz-focus-inner, 246 | [type="button"]::-moz-focus-inner, 247 | [type="reset"]::-moz-focus-inner, 248 | [type="submit"]::-moz-focus-inner { 249 | padding: 0; 250 | border-style: none; 251 | } 252 | 253 | input[type="radio"], 254 | input[type="checkbox"] { 255 | box-sizing: border-box; 256 | padding: 0; 257 | } 258 | 259 | input[type="date"], 260 | input[type="time"], 261 | input[type="datetime-local"], 262 | input[type="month"] { 263 | -webkit-appearance: listbox; 264 | } 265 | 266 | textarea { 267 | overflow: auto; 268 | resize: vertical; 269 | } 270 | 271 | fieldset { 272 | min-width: 0; 273 | padding: 0; 274 | margin: 0; 275 | border: 0; 276 | } 277 | 278 | legend { 279 | display: block; 280 | width: 100%; 281 | max-width: 100%; 282 | padding: 0; 283 | margin-bottom: .5rem; 284 | font-size: 1.5rem; 285 | line-height: inherit; 286 | color: inherit; 287 | white-space: normal; 288 | } 289 | 290 | progress { 291 | vertical-align: baseline; 292 | } 293 | 294 | [type="number"]::-webkit-inner-spin-button, 295 | [type="number"]::-webkit-outer-spin-button { 296 | height: auto; 297 | } 298 | 299 | [type="search"] { 300 | outline-offset: -2px; 301 | -webkit-appearance: none; 302 | } 303 | 304 | [type="search"]::-webkit-search-cancel-button, 305 | [type="search"]::-webkit-search-decoration { 306 | -webkit-appearance: none; 307 | } 308 | 309 | ::-webkit-file-upload-button { 310 | font: inherit; 311 | -webkit-appearance: button; 312 | } 313 | 314 | output { 315 | display: inline-block; 316 | } 317 | 318 | summary { 319 | display: list-item; 320 | cursor: pointer; 321 | } 322 | 323 | template { 324 | display: none; 325 | } 326 | 327 | [hidden] { 328 | display: none !important; 329 | } 330 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Node Twitter 2 | 3 | [](https://travis-ci.org/vinitkumar/node-twitter) 4 | 5 | 6 | 7 | Node twitter is an effort to rewrite some of Twitter's functionality using modern 8 | javascript based toolchain. It was mostly an effort to learn Node.js and trying to reverse 9 | engineer some of twitter's feature. 10 | 11 | It has support for tweeting, commenting and following with analytics 12 | 13 | You can support the development here by becoming a backer: https://opencollective.com/nodetwitter/ 14 | 15 | 16 | 17 | 18 | ## Prerequisites 19 | 20 | You are required to have Node.js and MongoDB installed if you'd like to run the app locally. 21 | 22 | - [Node.js](http://nodejs.org) 23 | - [Mongodb](http://docs.mongodb.org/manual/installation/) 24 | 25 | Install sass and grunt too to compile the CSS files 26 | 27 | ``` 28 | sudo npm install -g grunt-cli 29 | sudo npm install -g sass 30 | 31 | ``` 32 | 33 | The configuration is in `config/config.js`. Please create your own `.env` file. You can find an example of `.env` file in `.env.example`. 34 | 35 | Create a [github application](https://github.com/settings/apps) and copy cliend id and secret to .env file: 36 | 37 | ``` 38 | GITHUB_CLIENT_SECRET="your_github_client_secret" 39 | GITHUB_CLIENT_ID="your_github_client_id" 40 | ``` 41 | ## Usage via Docker 42 | 43 | 44 | The fastest way to get this running is with docker. Docker bootstrap all dependencies and 45 | you can just run these couple of commands to get it up and running. 46 | 47 | ``` 48 | # first build the container 49 | docker-compose --log-level DEBUG build 50 | 51 | # then run the server and mongodb like this: 52 | 53 | docker-compose up 54 | ``` 55 | Now, open the website on http://localhost:3000 and it should just work. 56 | 57 | Before building Docker container change DB link to: `mongodb://mongodb/ntwitter` in `.env` file. 58 | 59 | 60 | ## Usage 61 | 62 | ```sh 63 | # First install all the project dependencies. 64 | # run mongodb server 65 | ~/ mongod 66 | ~/node-twitter/ npm install 67 | # Now run the app 68 | ~/node-twitter/ npm start 69 | 70 | > node-twitter@1.1.0 start ~/node-twitter 71 | > node server.js 72 | 73 | Express app started on port 3000 74 | ``` 75 | 76 | # Contribute 77 | 78 | ## Introduction 79 | 80 | First, thank you for considering contributing to Node Twitter 81 | 82 | 83 | ! It's people like you that make the open source community such a great community! 😊 84 | 85 | We welcome any type of contribution, not only code. You can help with 86 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 87 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 88 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 89 | - **Code**: take a look at the [open issues](https://github.com/vinitkumar/node-twitter/issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 90 | - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/nodetwitter/). 91 | 92 | ## Your First Contribution 93 | 94 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 95 | 96 | ## Submitting code 97 | 98 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 99 | 100 | ## Code review process 101 | 102 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 103 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 104 | 105 | ## Financial contributions 106 | 107 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/nodetwitter/). 108 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 109 | 110 | ## Questions 111 | 112 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 113 | You can also reach us at mail@vinitkumar.me 114 | 115 | ## Credits 116 | 117 | ### Contributors 118 | 119 | Thank you to all the people who have already contributed to node-twitter! 120 | 121 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/0) 122 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/1) 123 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/2) 124 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/3) 125 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/4) 126 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/5) 127 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/6) 128 | [](https://sourcerer.io/fame/vinitkumar/vinitkumar/node-twitter/links/7) 129 | 130 | 131 | ### Backers 132 | 133 | Thank you to all our backers! [[Become a backer](https://opencollective.com/nodetwitter/#backer)] 134 | 135 | 136 | 137 | 138 | ### Sponsors 139 | 140 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/nodetwitter/#sponsor)) 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | ## License 155 | [Apache License 2.0](https://github.com/vinitkumar/node-twitter/blob/master/License) 156 | 157 | 158 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fvinitkumar%2Fnode-twitter?ref=badge_large) 159 | 160 | ## Important 161 | 162 | Twitter is a registered trademark of Twitter Inc. This project is just for learning purposes and should be treated as such. 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /public/css/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "style.css", 4 | "sources": [ 5 | "../../app/styles/main.scss", 6 | "../../app/styles/base/_reset.scss", 7 | "../../app/styles/abstracts/_variables.scss", 8 | "../../app/styles/layout/_navigation.scss", 9 | "../../app/styles/components/_followers.scss", 10 | "../../app/styles/abstracts/_placeholders.scss", 11 | "../../app/styles/components/_tweets.scss", 12 | "../../app/styles/components/_pagination.scss", 13 | "../../app/styles/components/_modals.scss", 14 | "../../app/styles/components/_profile-card.scss", 15 | "../../app/styles/pages/_login.scss", 16 | "../../app/styles/pages/_home.scss", 17 | "../../app/styles/pages/_analytics.scss", 18 | "../../app/styles/pages/_messaging.scss", 19 | "../../app/styles/layout/_footer.scss" 20 | ], 21 | "names": [], 22 | "mappings": "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" 23 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | body { 3 | font-size: 13px; 4 | min-height: 100vh; 5 | background-color: #141D26; 6 | color: #8899A6; } 7 | 8 | body > div.container { 9 | /* Calculate the minimum height by subtracting the header and footer height */ 10 | min-height: calc(100vh - 50px - 150px); 11 | padding-top: 30px; 12 | padding-bottom: 30px; } 13 | 14 | h4 { 15 | font-size: 1.2rem; } 16 | 17 | ul { 18 | list-style: none; 19 | margin: 0; 20 | padding: 0; } 21 | 22 | a { 23 | color: #1DA1F2; } 24 | a:hover { 25 | color: #0c85d0; 26 | text-decoration: none; } 27 | 28 | hr { 29 | border-color: #141D26; } 30 | 31 | .btn { 32 | border-radius: 5px; 33 | font-size: 11px; 34 | padding: 3px 8px; 35 | color: #ffffff; 36 | background-color: #1DA1F2; } 37 | .btn:hover { 38 | background-color: #35abf3; 39 | cursor: pointer; 40 | color: #ffffff; } 41 | 42 | .login-btn { 43 | margin-top: 40px; } 44 | 45 | textarea { 46 | color: #8899A6; 47 | padding: 5px 10px; 48 | width: 100%; 49 | background-color: #141D26; 50 | border: solid 1px #000; 51 | border-radius: 8px; } 52 | textarea::placeholder { 53 | color: #8899A6; } 54 | textarea:focus { 55 | color: #ffffff; } 56 | 57 | pre { 58 | white-space: pre-wrap; 59 | /* css-3 */ 60 | white-space: -moz-pre-wrap; 61 | /* Mozilla, since 1999 */ 62 | white-space: -pre-wrap; 63 | /* Opera 4-6 */ 64 | white-space: -o-pre-wrap; 65 | /* Opera 7 */ 66 | word-wrap: break-word; 67 | /* Internet Explorer 5.5+ */ } 68 | 69 | pre code { 70 | color: black; } 71 | 72 | /* Navbar */ 73 | .navbar__main-navigation span, 74 | .navbar__logo { 75 | display: none; } 76 | @media (min-width: 992px) { 77 | .navbar__main-navigation span, 78 | .navbar__logo { 79 | display: inline-block; 80 | text-align: center; } } 81 | 82 | .navbar { 83 | background-color: #1f2e3f; 84 | height: 50px; 85 | margin: 0; 86 | padding: 0; 87 | font-size: 14px; 88 | border-bottom: 1px solid #000; } 89 | .navbar .container { 90 | display: flex; 91 | align-items: center; 92 | height: 100%; } 93 | .navbar .container > * { 94 | flex-grow: 1; } 95 | .navbar a { 96 | color: #8899A6; } 97 | 98 | .navbar__group { 99 | height: 100%; } 100 | .navbar__group_right { 101 | display: flex; 102 | flex-direction: row; 103 | justify-content: flex-end; 104 | align-items: center; } 105 | 106 | .navbar__main-navigation { 107 | display: flex; 108 | align-items: center; 109 | height: 100%; 110 | margin-right: auto; } 111 | .navbar__main-navigation li { 112 | margin-right: 5px; 113 | display: flex; 114 | height: 100%; 115 | align-items: center; } 116 | .navbar__main-navigation li:hover { 117 | color: #1DA1F2 !important; } 118 | .navbar__main-navigation a { 119 | display: flex; 120 | align-items: center; 121 | height: 100%; 122 | padding-left: 8px; 123 | padding-right: 8px; } 124 | .navbar__main-navigation a:hover { 125 | color: #1DA1F2 !important; 126 | border-bottom: 2px solid #1DA1F2; } 127 | .navbar__main-navigation a:hover > * { 128 | margin-bottom: -2px; } 129 | .navbar__main-navigation a i { 130 | margin-right: 4px; } 131 | .navbar__main-navigation .fa { 132 | font-size: 22px; 133 | display: inline-block; 134 | padding: 0; 135 | padding-right: 10px; 136 | padding-left: 10px; } 137 | @media (min-width: 992px) { 138 | .navbar__main-navigation .fa { 139 | padding-left: 0; } } 140 | 141 | .navbar__logo { 142 | position: absolute; 143 | left: 50%; 144 | margin-left: -13px; } 145 | .navbar__logo img { 146 | height: 26px; } 147 | 148 | .navbar__profile { 149 | display: flex; 150 | align-items: center; 151 | margin-right: 20px; } 152 | .navbar__profile .navbar__profile-logout { 153 | display: flex; } 154 | .navbar__profile .fa { 155 | font-size: 20px; 156 | padding-right: 10px; } 157 | 158 | .navbar__new-tweet { 159 | text-align: right; } 160 | .navbar__new-tweet .navbar__new-tweet-button { 161 | color: #ffffff; 162 | font-size: 14px; 163 | padding: 7px 14px; 164 | border-radius: 20px; } 165 | .navbar__new-tweet .navbar__new-tweet-button:hover { 166 | color: #ffffff; } 167 | 168 | .navbar__notifications { 169 | color: red !important; } 170 | 171 | .follower, .tweet, .profile, .user-information, .useful-links, 172 | .recent-visits, .analytics { 173 | background-color: #1B2836; 174 | padding: 15px; 175 | border-radius: 2px; 176 | border: 1px solid #000; 177 | margin-bottom: 20px; } 178 | 179 | .follower { 180 | text-align: center; } 181 | 182 | .follower__user-info { 183 | margin-top: 10px; } 184 | 185 | .follower__image { 186 | border-radius: 50%; 187 | width: 100px; 188 | max-width: 100%; } 189 | 190 | /* Tweets */ 191 | .follower, .tweet, .profile, .user-information, .useful-links, 192 | .recent-visits, .analytics { 193 | background-color: #1B2836; 194 | padding: 15px; 195 | border-radius: 2px; 196 | border: 1px solid #000; 197 | margin-bottom: 20px; } 198 | 199 | .tweet { 200 | color: #ffffff; } 201 | 202 | .tweet::after { 203 | content: ""; 204 | clear: both; 205 | display: block; } 206 | 207 | .tweet__image { 208 | border-radius: 50%; 209 | height: 40px; 210 | border: 1px solid #000; } 211 | 212 | .logout-image { 213 | height: 20px; } 214 | 215 | .tweet__description { 216 | padding-left: 25px; } 217 | 218 | .tweet__content { 219 | margin: 5px 0 10px; 220 | word-wrap: break-word; } 221 | 222 | .tweet__form .tweet__edit { 223 | background-color: #2AAF81; } 224 | .tweet__form .tweet__edit:hover { 225 | background-color: #2fc490; } 226 | 227 | .tweet__form .tweet__delete { 228 | margin-top: -10%; 229 | margin-left: 10%; 230 | background-color: #E53939; } 231 | .tweet__form .tweet__delete:hover { 232 | background-color: #e85050; } 233 | 234 | .tweet__username a { 235 | color: #ffffff; 236 | font-weight: bold; } 237 | 238 | .tweet__date, 239 | .tweet__date a { 240 | color: #8899A6; } 241 | 242 | .tweet textarea.edit-tweet { 243 | margin: 10px 0 15px; } 244 | 245 | .tweet textarea { 246 | font-size: 13px; 247 | background-color: #141D26; 248 | border-color: #000; 249 | display: block; 250 | padding: 6px 12px; 251 | color: #ffffff; 252 | border-radius: 4px; 253 | width: 100%; } 254 | 255 | .tweet__comments .btn { 256 | margin: auto; 257 | display: block; 258 | margin-top: 10px; } 259 | 260 | .tweets__comment-image { 261 | border-radius: 50%; 262 | height: 20px; 263 | border: 1px solid #000; 264 | margin-right: 6px; } 265 | 266 | .notification-icon { 267 | margin-right: 10px; } 268 | 269 | .pagination { 270 | text-align: center; } 271 | .pagination a, 272 | .pagination li.out-of-range { 273 | padding: 6px 12px; } 274 | 275 | .pagination { 276 | justify-content: center; } 277 | 278 | .pagination__list { 279 | display: flex; } 280 | .pagination__list li a { 281 | color: #ffffff; 282 | border-color: #000 !important; } 283 | .pagination__list .no a { 284 | background-color: #1B2836; 285 | color: #ffffff; } 286 | .pagination__list .active a { 287 | background-color: #1DA1F2 !important; } 288 | 289 | .modal-dialog { 290 | margin-top: 60px; } 291 | 292 | .modal-content { 293 | background: none; } 294 | 295 | .modal-header { 296 | background-color: #141D26; 297 | justify-content: center; 298 | border-bottom: 0; } 299 | .modal-header button.close { 300 | color: #8899A6; 301 | opacity: 1; 302 | text-shadow: none; 303 | position: absolute; 304 | right: 15px; } 305 | .modal-header button.close:hover { 306 | cursor: pointer; } 307 | 308 | .modal-body { 309 | background-color: #1B2836; 310 | padding: 20px 15px; } 311 | 312 | .modal { 313 | padding: 0; } 314 | 315 | .new-tweet__button, 316 | .new-message__button { 317 | font-size: 14px; 318 | padding: 4px 10px; 319 | background-color: #1da1f2; 320 | color: #fff; 321 | margin: auto; 322 | margin-top: 10px; 323 | display: block; } 324 | 325 | .delete-confirmation__button{ 326 | font-size: 14px; 327 | padding: 4px 10px; 328 | background-color: firebrick; 329 | color: #fff; 330 | margin: auto; 331 | margin-top: 10px; 332 | display: block; } 333 | .delete-confirmation__button:hover { 334 | background-color: #ff0000; } 335 | 336 | .btn__delete{ 337 | background-color: firebrick; } 338 | 339 | .btn__delete:hover{ 340 | background-color: #ff0000;} 341 | 342 | 343 | 344 | /* Profile Card */ 345 | .follower, .tweet, .profile, .user-information, .useful-links, 346 | .recent-visits, .analytics { 347 | background-color: #1B2836; 348 | padding: 15px; 349 | border-radius: 2px; 350 | border: 1px solid #000; 351 | margin-bottom: 20px; } 352 | 353 | .profile { 354 | text-align: center; } 355 | 356 | .profile__handle { 357 | font-size: 18px; 358 | margin-top: 10px; 359 | margin-bottom: 5px; 360 | display: inline-block; } 361 | 362 | .profile__image { 363 | border: 0; 364 | border-radius: 50%; 365 | width: 150px; 366 | max-width: 100%; } 367 | 368 | .profile__messaging-options .btn { 369 | margin: 10px; } 370 | 371 | .profile__follow { 372 | margin: 10px 0; } 373 | 374 | .profile__follow-button.following { 375 | background-color: #E53939; } 376 | .profile__follow-button.following:hover { 377 | background-color: #e85050; } 378 | 379 | /* Login Page */ 380 | .login { 381 | padding-top: 36px; } 382 | 383 | .login-container { 384 | text-align: center; 385 | margin: auto; } 386 | 387 | .github-btn { 388 | margin-left: 20px; } 389 | 390 | .icon-container { 391 | margin-left: 6px; } 392 | 393 | .stats-parent-container { 394 | margin-top: 60px; } 395 | 396 | .login-using-github { 397 | font-size: 20px; } 398 | 399 | /* Main Dashboard */ 400 | .follower, .tweet, .profile, .user-information, .useful-links, 401 | .recent-visits, .analytics { 402 | background-color: #1B2836; 403 | padding: 15px; 404 | border-radius: 2px; 405 | border: 1px solid #000; 406 | margin-bottom: 20px; } 407 | 408 | .user-information > .row { 409 | align-items: center; } 410 | 411 | .user-information__image { 412 | padding-right: 0; } 413 | .user-information__image img { 414 | border-radius: 50%; 415 | width: 100px; 416 | max-width: 100%; 417 | float: right; } 418 | 419 | .user-information__text { 420 | text-align: center; } 421 | 422 | .user-information h4, 423 | .useful-links h4, 424 | .recent-visits h4 { 425 | color: #fff; } 426 | 427 | .user-information__stats ul { 428 | margin-top: 10px; 429 | text-align: center; 430 | display: flex; 431 | justify-content: space-around; 432 | font-weight: bold; 433 | width: 100%; } 434 | 435 | @media (min-width: 992px) { 436 | .user-information__stats_small { 437 | display: none; } } 438 | 439 | .user-information__stats_large { 440 | display: none; } 441 | @media (min-width: 992px) { 442 | .user-information__stats_large { 443 | display: block; } } 444 | 445 | .user-information__stat-title { 446 | display: block; } 447 | 448 | .user-information__stats span:not(.user-information__stat-title) { 449 | font-size: 18px; 450 | color: #1da1f2; } 451 | 452 | .useful-links a { 453 | font-weight: bold; } 454 | 455 | .useful-links, 456 | .recent-visits { 457 | text-align: center; } 458 | 459 | .first-column .recent-visits, 460 | .first-column .useful-links { 461 | display: none; } 462 | @media (min-width: 992px) { 463 | .first-column .recent-visits, 464 | .first-column .useful-links { 465 | display: block; } } 466 | 467 | @media (min-width: 1200px) { 468 | .first-column .recent-visits { 469 | display: none; } } 470 | 471 | @media (min-width: 992px) { 472 | .third-column .recent-visits, 473 | .third-column .useful-links { 474 | display: none; } } 475 | 476 | @media (min-width: 1200px) { 477 | .third-column .recent-visits { 478 | display: block; } } 479 | 480 | /* Analytics Page */ 481 | .follower, .tweet, .profile, .user-information, .useful-links, 482 | .recent-visits, .analytics { 483 | background-color: #1B2836; 484 | padding: 15px; 485 | border-radius: 2px; 486 | border: 1px solid #000; 487 | margin-bottom: 20px; } 488 | 489 | .analytics { 490 | padding-top: 30px; 491 | padding-bottom: 30px; } 492 | .analytics h1 { 493 | color: #fff; 494 | text-align: center; } 495 | 496 | .analytics__table { 497 | table-layout: fixed; 498 | width: 100%; } 499 | .analytics__table th, 500 | .analytics__table td { 501 | border: 1px solid #fff; 502 | padding: .25rem .75rem; } 503 | .analytics__table th { 504 | font-weight: bold; 505 | color: #fff; 506 | text-align: center; } 507 | .analytics__table td { 508 | word-wrap: break-word; 509 | vertical-align: middle; } 510 | 511 | .analytics__user-column { 512 | width: 25%; } 513 | 514 | .analytics__stats { 515 | margin: 20px 0; } 516 | .analytics__stats ul { 517 | display: flex; 518 | justify-content: space-evenly; 519 | font-weight: bold; } 520 | .analytics__stats ul li { 521 | text-align: center; } 522 | 523 | .analytics__stats-title { 524 | font-size: 16px; } 525 | 526 | .analytics__stats-value { 527 | font-size: 18px; 528 | color: #1da1f2; 529 | display: block; } 530 | 531 | .analytics__user { 532 | font-weight: bold; 533 | text-align: center; } 534 | 535 | .chatbox { 536 | padding: 20px; } 537 | 538 | .chatMessage { 539 | display: inline; } 540 | 541 | span.sender { 542 | color: red; 543 | margin-left: 5px; 544 | margin-right: 5px; } 545 | 546 | span.message { 547 | width: fit-content; 548 | padding: 6px; } 549 | 550 | .msgtime { 551 | font-size: 10px; } 552 | 553 | .chatlist { 554 | margin-left: 20px; } 555 | 556 | .chat-image { 557 | margin-left: 5px; 558 | margin-right: 5px; } 559 | 560 | .chatrow { 561 | margin-bottom: 10px; 562 | padding: 4px; 563 | border-radius: 7px; } 564 | 565 | .inboxlink { 566 | margin-left: 40px; } 567 | 568 | .message-row { 569 | margin-top: 6px; } 570 | 571 | .following-image, .follower-image { 572 | margin-right: 6px; } 573 | 574 | a.logout { 575 | color: red !important; } 576 | 577 | .btn-message { 578 | margin-right: 12px; } 579 | 580 | .chatgroup { 581 | margin-bottom: 20px; } 582 | 583 | .chat-bubble { 584 | margin-bottom: 10px; } 585 | 586 | /* Footer */ 587 | footer { 588 | text-align: center; 589 | background-color: #243447; 590 | padding-top: 30px; 591 | height: 150px; } 592 | 593 | /* Media Queries */ 594 | #error-stack-trace { 595 | background: yellow; 596 | padding: 20px; 597 | margin-top: 20px; } 598 | 599 | /*# sourceMappingURL=style.css.map */ -------------------------------------------------------------------------------- /public/css/bootstrap/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACoBA,ECXA,QADA,SDeE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,qBAAA,KACA,mBAAA,UACA,4BAAA,YAKA,cACE,MAAA,aAMJ,QAAA,MAAA,OAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAWF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,kBACA,UAAA,KACA,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KEvBF,sBFgCE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAQF,EACE,WAAA,EACA,cAAA,KChDF,0BD0DA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCrDF,GDwDA,GCzDA,GD4DE,WAAA,EACA,cAAA,KAGF,MCxDA,MACA,MAFA,MD6DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,IACE,WAAA,OAIF,EC1DA,OD4DE,YAAA,OAIF,MACE,UAAA,IAQF,IChEA,IDkEE,SAAA,SACA,UAAA,IACA,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YACA,6BAAA,QG3LA,QH8LE,MAAA,QACA,gBAAA,UAUJ,8BACE,MAAA,QACA,gBAAA,KGvMA,oCAAA,oCH0ME,MAAA,QACA,gBAAA,KANJ,oCAUI,QAAA,EClEJ,KACA,ID2EA,IC1EA,KD8EE,YAAA,SAAA,CAAA,UACA,UAAA,IAIF,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAGA,mBAAA,UAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,eACE,SAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OACE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBC9GF,ODiHA,MC/GA,SADA,OAEA,SDmHE,OAAA,EACA,YAAA,QACA,UAAA,QACA,YAAA,QAGF,OCjHA,MDmHE,SAAA,QAGF,OCjHA,ODmHE,eAAA,KC7GF,aACA,cDkHA,OCpHA,mBDwHE,mBAAA,OCjHF,gCACA,+BACA,gCDmHA,yBAIE,QAAA,EACA,aAAA,KClHF,qBDqHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCrHA,2BACA,kBAFA,iBD+HE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MACA,UAAA,OACA,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SEnIF,yCDEA,yCDuIE,OAAA,KEpIF,cF4IE,eAAA,KACA,mBAAA,KExIF,4CDEA,yCD+IE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KErJF,SF2JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Setting @viewport causes scrollbars to overlap content in IE11 and Edge, so\n// we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n// 6. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -ms-text-size-adjust: 100%; // 4\n -ms-overflow-style: scrollbar; // 5\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // 6\n}\n\n// IE10+ doesn't honor `` in some cases.\n@at-root {\n @-ms-viewport {\n width: device-width;\n }\n}\n\n// stylelint-disable selector-list-comma-newline-after\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n// stylelint-enable selector-list-comma-newline-after\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use the\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n font-size: $font-size-base;\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, ``-`` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n// stylelint-enable selector-list-comma-newline-after\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on ``s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: .5rem;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\nhtml [type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. ``s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: .5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n/*# sourceMappingURL=bootstrap-reboot.css.map */","/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: .5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable indentation\n\n// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Origally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS—an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular psuedo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover {\n &:hover { @content; }\n}\n\n@mixin hover-focus {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} --------------------------------------------------------------------------------
') 85 | .addClass('tweet__content'); 86 | //.text(modifiedText); 87 | $tweetElement.append(modifiedText); 88 | $modifiedTweet.after($tweetElement).remove(); 89 | } 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vinit1414.08@bitmesra.ac.in. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Tweet = mongoose.model("Tweet"); 3 | const Schema = mongoose.Schema; 4 | const bcrypt = require('bcrypt'); 5 | const authTypes = ['github']; 6 | 7 | // ## Define UserSchema 8 | const UserSchema = new Schema( 9 | { 10 | name: String, 11 | email: String, 12 | username: String, 13 | provider: String, 14 | hashedPassword: String, 15 | salt: String, 16 | github: {}, 17 | followers: [{ type: Schema.ObjectId, ref: "User" }], 18 | following: [{ type: Schema.ObjectId, ref: "User" }], 19 | tweets: Number 20 | }, 21 | { usePushEach: true } 22 | ); 23 | 24 | UserSchema.virtual("password") 25 | .set(function(password) { 26 | this._password = password; 27 | this.salt = this.makeSalt(); 28 | this.hashedPassword = this.encryptPassword(password); 29 | }) 30 | .get(function() { 31 | return this._password; 32 | }); 33 | 34 | const validatePresenceOf = value => value && value.length; 35 | 36 | UserSchema.path("name").validate(function(name) { 37 | if (authTypes.indexOf(this.provider) !== -1) { 38 | return true; 39 | } 40 | return name.length; 41 | }, "Name cannot be blank"); 42 | 43 | UserSchema.path("email").validate(function(email) { 44 | if (authTypes.indexOf(this.provider) !== -1) { 45 | return true; 46 | } 47 | return email.length; 48 | }, "Email cannot be blank"); 49 | 50 | UserSchema.path("username").validate(function(username) { 51 | if (authTypes.indexOf(this.provider) !== -1) { 52 | return true; 53 | } 54 | return username.length; 55 | }, "username cannot be blank"); 56 | 57 | UserSchema.path("hashedPassword").validate(function(hashedPassword) { 58 | if (authTypes.indexOf(this.provider) !== -1) { 59 | return true; 60 | } 61 | return hashedPassword.length; 62 | }, "Password cannot be blank"); 63 | 64 | UserSchema.pre("save", function(next) { 65 | if ( 66 | !validatePresenceOf(this.password) && 67 | authTypes.indexOf(this.provider) === -1 68 | ) { 69 | next(new Error("Invalid password")); 70 | } else { 71 | next(); 72 | } 73 | }); 74 | 75 | UserSchema.methods = { 76 | authenticate: function(plainText) { 77 | return this.encryptPassword(plainText) === this.hashedPassword; 78 | }, 79 | 80 | makeSalt: function() { 81 | return Math.round(new Date().valueOf() * Math.random()); 82 | }, 83 | 84 | encryptPassword: function(password) { 85 | if (!password) { 86 | return ""; 87 | } 88 | let salt = this.makeSalt(); 89 | return bcrypt.hashSync(password, salt) 90 | }, 91 | }; 92 | 93 | UserSchema.statics = { 94 | addfollow: function(id, cb) { 95 | this.findOne({ _id: id }) 96 | .populate("followers") 97 | .exec(cb); 98 | }, 99 | countUserTweets: function(id, cb) { 100 | return Tweet.find({ user: id }) 101 | .countDocuments() 102 | .exec(cb); 103 | }, 104 | load: function(options, cb) { 105 | options.select = options.select || "name username github"; 106 | return this.findOne(options.criteria) 107 | .select(options.select) 108 | .exec(cb); 109 | }, 110 | list: function(options) { 111 | const criteria = options.criteria || {}; 112 | return this.find(criteria) 113 | .populate("user", "name username") 114 | .limit(options.perPage) 115 | .skip(options.perPage * options.page); 116 | }, 117 | countTotalUsers: function() { 118 | return this.find({}).countDocuments(); 119 | } 120 | }; 121 | 122 | mongoose.model("User", UserSchema); 123 | -------------------------------------------------------------------------------- /app/controllers/tweets.js: -------------------------------------------------------------------------------- 1 | // ## Tweet Controller 2 | const createPagination = require("./analytics").createPagination; 3 | const mongoose = require("mongoose"); 4 | const Tweet = mongoose.model("Tweet"); 5 | const User = mongoose.model("User"); 6 | const Analytics = mongoose.model("Analytics"); 7 | const _ = require("underscore"); 8 | const logger = require("../middlewares/logger"); 9 | 10 | exports.tweet = (req, res, next, id) => { 11 | Tweet.load(id, (err, tweet) => { 12 | if (err) { 13 | return next(err); 14 | } 15 | if (!tweet) { 16 | return next(new Error("Failed to load tweet" + id)); 17 | } 18 | req.tweet = tweet; 19 | next(); 20 | }); 21 | }; 22 | 23 | // ### Create a Tweet 24 | exports.create = (req, res) => { 25 | const tweet = new Tweet(req.body); 26 | tweet.user = req.user; 27 | tweet.tags = parseHashtag(req.body.body); 28 | 29 | tweet.uploadAndSave({}, err => { 30 | if (err) { 31 | res.render("pages/500", { error: err }); 32 | } else { 33 | res.redirect("/"); 34 | } 35 | }); 36 | }; 37 | 38 | // ### Update a tweet 39 | exports.update = (req, res) => { 40 | let tweet = req.tweet; 41 | tweet = _.extend(tweet, { body: req.body.tweet }); 42 | tweet.uploadAndSave({}, err => { 43 | if (err) { 44 | return res.render("pages/500", { error: err }); 45 | } 46 | res.redirect("/"); 47 | }); 48 | }; 49 | 50 | // ### Delete a tweet 51 | exports.destroy = (req, res) => { 52 | const tweet = req.tweet; 53 | tweet.remove(err => { 54 | if (err) { 55 | return res.render("pages/500"); 56 | } 57 | res.redirect("/"); 58 | }); 59 | }; 60 | 61 | // ### Parse a hashtag 62 | 63 | function parseHashtag(inputText) { 64 | var regex = /(?:^|\s)(?:#)([a-zA-Z\d]+)/g; 65 | var matches = []; 66 | var match; 67 | while ((match = regex.exec(inputText)) !== null) { 68 | matches.push(match[1]); 69 | } 70 | return matches; 71 | } 72 | 73 | exports.parseHashtag = parseHashtag; 74 | 75 | let showTweets = (req, res, criteria) => { 76 | const findCriteria = criteria || {}; 77 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 78 | const perPage = 10; 79 | const options = { 80 | perPage: perPage, 81 | page: page, 82 | criteria: findCriteria 83 | }; 84 | let followingCount = req.user.following.length; 85 | let followerCount = req.user.followers.length; 86 | let tweets, tweetCount, pageViews, analytics, pagination; 87 | User.countUserTweets(req.user._id).then(result => { 88 | tweetCount = result; 89 | }); 90 | Tweet.list(options) 91 | .then(result => { 92 | tweets = result; 93 | return Tweet.countTweets(findCriteria); 94 | }) 95 | .then(result => { 96 | pageViews = result; 97 | pagination = createPagination( 98 | req, 99 | Math.ceil(pageViews / perPage), 100 | page + 1 101 | ); 102 | return Analytics.list({ perPage: 15 }); 103 | }) 104 | .then(result => { 105 | analytics = result; 106 | res.render("pages/index", { 107 | title: "List of Tweets", 108 | tweets: tweets, 109 | analytics: analytics, 110 | page: page + 1, 111 | tweetCount: tweetCount, 112 | pagination: pagination, 113 | followerCount: followerCount, 114 | followingCount: followingCount, 115 | pages: Math.ceil(pageViews / perPage) 116 | }); 117 | }) 118 | .catch(error => { 119 | logger.error(error); 120 | res.render("pages/500"); 121 | }); 122 | }; 123 | 124 | // ### Find a tag 125 | exports.findTag = (req, res) => { 126 | let tag = req.params.tag; 127 | showTweets(req, res, { tags: tag.toLowerCase() }); 128 | }; 129 | 130 | exports.index = (req, res) => { 131 | showTweets(req, res); 132 | }; 133 | -------------------------------------------------------------------------------- /public/css/bootstrap/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const log = require("./middlewares/logger"); 4 | 5 | const users = require("../app/controllers/users"); 6 | const apiv1 = require("../app/controllers/apiv1"); 7 | const chat = require("../app/controllers/chat"); 8 | const analytics = require("../app/controllers/analytics"); 9 | const tweets = require("../app/controllers/tweets"); 10 | const comments = require("../app/controllers/comments"); 11 | const favorites = require("../app/controllers/favorites"); 12 | const follows = require("../app/controllers/follows"); 13 | const activity = require("../app/controllers/activity"); 14 | 15 | module.exports = (app, passport, auth) => { 16 | app.use("/", router); 17 | /** 18 | * Main unauthenticated routes 19 | */ 20 | router.get("/login", users.login); 21 | router.get("/signup", users.signup); 22 | router.get("/logout", users.logout); 23 | 24 | /** 25 | * Authentication routes 26 | */ 27 | router.get( 28 | "/auth/github", 29 | passport.authenticate("github", { failureRedirect: "/login" }), 30 | users.signin 31 | ); 32 | router.get( 33 | "/auth/github/callback", 34 | passport.authenticate("github", { failureRedirect: "/login" }), 35 | users.authCallback 36 | ); 37 | 38 | /** 39 | * API routes 40 | */ 41 | router.get("/apiv1/tweets", apiv1.tweetList); 42 | router.get("/apiv1/users", apiv1.usersList); 43 | 44 | /** 45 | * Authentication middleware 46 | * All routes specified after this middleware require authentication in order 47 | * to access 48 | */ 49 | router.use(auth.requiresLogin); 50 | /** 51 | * Analytics logging middleware 52 | * Anytime an authorized user makes a get request, it will be logged into 53 | * analytics 54 | */ 55 | router.get("/*", log.analytics); 56 | 57 | /** 58 | * Acivity routes 59 | */ 60 | router.get("/activities", activity.index); 61 | /** 62 | * Home route 63 | */ 64 | router.get("/", tweets.index); 65 | /** 66 | * User routes 67 | */ 68 | router.get("/users/:userId", users.show); 69 | router.get("/users/:userId/followers", users.showFollowers); 70 | router.get("/users/:userId/following", users.showFollowing); 71 | router.post("/users", users.create); 72 | router.post( 73 | "/users/sessions", 74 | passport.authenticate("local", { 75 | failureRedirect: "/login", 76 | failureFlash: "Invalid email or password" 77 | }), 78 | users.session 79 | ); 80 | router.post("/users/:userId/follow", follows.follow); 81 | router.post("/users/:userId/delete", users.delete); 82 | router.param("userId", users.user); 83 | 84 | /** 85 | * Chat routes 86 | */ 87 | router.get("/chat", chat.index); 88 | router.get("/chat/:id", chat.show); 89 | router.get("/chat/get/:userid", chat.getChat); 90 | router.post("/chats", chat.create); 91 | /** 92 | * Analytics routes 93 | */ 94 | router.get("/analytics", analytics.index); 95 | 96 | /** 97 | * Tweet routes 98 | */ 99 | router 100 | .route("/tweets") 101 | .get(tweets.index) 102 | .post(tweets.create); 103 | 104 | router 105 | .route("/tweets/:id") 106 | .post(auth.tweet.hasAuthorization, tweets.update) 107 | .delete(auth.tweet.hasAuthorization, tweets.destroy); 108 | 109 | router.param("id", tweets.tweet); 110 | 111 | /** 112 | * Comment routes 113 | */ 114 | router 115 | .route("/tweets/:id/comments") 116 | .get(comments.create) 117 | .post(comments.create) 118 | .delete(comments.destroy); 119 | 120 | /** 121 | * Favorite routes 122 | */ 123 | router 124 | .route("/tweets/:id/favorites") 125 | .post(favorites.create) 126 | .delete(favorites.destroy); 127 | 128 | /** 129 | * Find tags 130 | */ 131 | router 132 | .route("/tweets/hashtag/:tag") 133 | .get(tweets.findTag); 134 | 135 | /** 136 | * Page not found route (must be at the end of all routes) 137 | */ 138 | router.use((req, res) => { 139 | res.status(404).render("pages/404", { 140 | url: req.originalUrl, 141 | error: "Not found" 142 | }); 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Introduction 4 | 5 | First, thank you for considering contributing to node-twitter! It's people like you that make the open source community such a great community! 😊 6 | 7 | We welcome any type of contribution, not only code. You can help with 8 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 9 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 10 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 11 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 12 | - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-twitter). 13 | 14 | ## Your First Contribution 15 | 16 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 17 | 18 | ## Submitting code 19 | 20 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 21 | 22 | ## Code review process 23 | 24 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 25 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 26 | 27 | ## Financial contributions 28 | 29 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-twitter). 30 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 31 | 32 | ## Questions 33 | 34 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 35 | You can also reach us at hello@node-twitter.opencollective.com. 36 | 37 | ## Credits 38 | 39 | ### Contributors 40 | 41 | Thank you to all the people who have already contributed to node-twitter! 42 | 43 | 44 | 45 | ### Backers 46 | 47 | Thank you to all our backers! [[Become a backer](https://opencollective.com/node-twitter#backer)] 48 | 49 | 50 | 51 | 52 | ### Sponsors 53 | 54 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/node-twitter#sponsor)) 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/models/tweets.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const utils = require("../../lib/utils"); 4 | 5 | // Getters and Setters 6 | const setTags = tags => tags.map(t => t.toLowerCase()); 7 | 8 | // Tweet Schema 9 | const TweetSchema = new Schema( 10 | { 11 | body: { type: String, default: "", trim: true, maxlength: 280 }, 12 | user: { type: Schema.ObjectId, ref: "User" }, 13 | comments: [ 14 | { 15 | body: { type: String, default: "", maxlength: 280 }, 16 | user: { type: Schema.ObjectId, ref: "User" }, 17 | commenterName: { type: String, default: "" }, 18 | commenterPicture: { type: String, default: "" }, 19 | createdAt: { type: Date, default: Date.now } 20 | } 21 | ], 22 | tags: { type: [String], set: setTags }, 23 | favorites: [{ type: Schema.ObjectId, ref: "User" }], 24 | favoriters: [{ type: Schema.ObjectId, ref: "User" }], // same as favorites 25 | favoritesCount: Number, 26 | createdAt: { type: Date, default: Date.now } 27 | }, 28 | { usePushEach: true } 29 | ); 30 | 31 | // Pre save hook 32 | TweetSchema.pre("save", function(next) { 33 | if (this.favorites) { 34 | this.favoritesCount = this.favorites.length; 35 | } 36 | if (this.favorites) { 37 | this.favoriters = this.favorites; 38 | } 39 | next(); 40 | }); 41 | 42 | // Validations in the schema 43 | TweetSchema.path("body").validate( 44 | body => body.length > 0, 45 | "Tweet body cannot be blank" 46 | ); 47 | 48 | TweetSchema.virtual("_favorites").set(function(user) { 49 | if (this.favorites.indexOf(user._id) === -1) { 50 | this.favorites.push(user._id); 51 | } else { 52 | this.favorites.splice(this.favorites.indexOf(user._id), 1); 53 | } 54 | }); 55 | 56 | TweetSchema.methods = { 57 | uploadAndSave: function(images, callback) { 58 | // const imager = new Imager(imagerConfig, "S3"); 59 | const self = this; 60 | if (!images || !images.length) { 61 | return this.save(callback); 62 | } 63 | imager.upload( 64 | images, 65 | (err, cdnUri, files) => { 66 | if (err) { 67 | return callback(err); 68 | } 69 | if (files.length) { 70 | self.image = { cdnUri: cdnUri, files: files }; 71 | } 72 | self.save(callback); 73 | }, 74 | "article" 75 | ); 76 | }, 77 | addComment: function(user, comment, cb) { 78 | if (user.name) { 79 | this.comments.push({ 80 | body: comment.body, 81 | user: user._id, 82 | commenterName: user.name, 83 | commenterPicture: user.github.avatar_url 84 | }); 85 | this.save(cb); 86 | } else { 87 | this.comments.push({ 88 | body: comment.body, 89 | user: user._id, 90 | commenterName: user.username, 91 | commenterPicture: user.github.avatar_url 92 | }); 93 | 94 | this.save(cb); 95 | } 96 | }, 97 | 98 | removeComment: function(commentId, cb) { 99 | let index = utils.indexof(this.comments, { id: commentId }); 100 | if (~index) { 101 | this.comments.splice(index, 1); 102 | } else { 103 | return cb("not found"); 104 | } 105 | this.save(cb); 106 | } 107 | }; 108 | 109 | // ## Static Methods in the TweetSchema 110 | TweetSchema.statics = { 111 | // Load tweets 112 | load: function(id, callback) { 113 | this.findOne({ _id: id }) 114 | .populate("user", "name username provider github") 115 | .populate("comments.user") 116 | .exec(callback); 117 | }, 118 | // List tweets 119 | list: function(options) { 120 | const criteria = options.criteria || {}; 121 | return this.find(criteria) 122 | .populate("user", "name username provider github") 123 | .sort({ createdAt: -1 }) 124 | .limit(options.perPage) 125 | .skip(options.perPage * options.page); 126 | }, 127 | // List tweets 128 | limitedList: function(options) { 129 | const criteria = options.criteria || {}; 130 | return this.find(criteria) 131 | .populate("user", "name username") 132 | .sort({ createdAt: -1 }) 133 | .limit(options.perPage) 134 | .skip(options.perPage * options.page); 135 | }, 136 | // Tweets of User 137 | userTweets: function(id, callback) { 138 | this.find({ user: ObjectId(id) }) 139 | .toArray() 140 | .exec(callback); 141 | }, 142 | 143 | // Count the number of tweets for a specific user 144 | countUserTweets: function(id, callback) { 145 | return this.find({ user: id }) 146 | .countDocuments() 147 | .exec(callback); 148 | }, 149 | 150 | // Count the app tweets by criteria 151 | countTweets: function(criteria) { 152 | return this.find(criteria).countDocuments(); 153 | } 154 | }; 155 | 156 | mongoose.model("Tweet", TweetSchema); 157 | -------------------------------------------------------------------------------- /app/controllers/analytics.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Analytics = mongoose.model("Analytics"); 3 | const Tweet = mongoose.model("Tweet"); 4 | const User = mongoose.model("User"); 5 | const qs = require("querystring"); 6 | const url = require("url"); 7 | const logger = require("../middlewares/logger"); 8 | 9 | exports.createPagination = (req, pages, page) => { 10 | let params = qs.parse(url.parse(req.url).query); 11 | let str = ""; 12 | let pageNumberClass; 13 | let pageCutLow = page - 1; 14 | let pageCutHigh = page + 1; 15 | // Show the Previous button only if you are on a page other than the first 16 | if (page > 1) { 17 | str += 18 | '
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `