├── .gitignore ├── frontend ├── src │ ├── pages │ │ ├── privatePages │ │ │ ├── messages │ │ │ │ ├── Messages.css │ │ │ │ └── Messages.js │ │ │ ├── profile │ │ │ │ ├── Profile.css │ │ │ │ └── Profile.js │ │ │ ├── settings │ │ │ │ ├── Settings.css │ │ │ │ └── Settings.js │ │ │ ├── protectedRoutes │ │ │ │ ├── ProtectedRoutes.css │ │ │ │ └── ProtectedRoutes.js │ │ │ └── home │ │ │ │ ├── Home.js │ │ │ │ └── Home.css │ │ └── publicPages │ │ │ ├── signup │ │ │ ├── Signup.css │ │ │ └── Signup.js │ │ │ └── login │ │ │ ├── Login.css │ │ │ └── Login.js │ ├── components │ │ ├── searchBar │ │ │ ├── SearchBar.js │ │ │ └── SearchBar.css │ │ ├── sideBoard │ │ │ ├── SideBoard.js │ │ │ └── SideBoard.css │ │ ├── followersBoard │ │ │ ├── FollowersBoard.js │ │ │ └── FollowersBoard.css │ │ └── sideBar │ │ │ ├── SideBar.css │ │ │ └── SideBar.js │ ├── index.js │ ├── App.js │ └── index.css ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── .prettierrc ├── .gitignore ├── README.md ├── .eslintrc.json └── package.json ├── public ├── login.js ├── lost.html ├── handleLogin.js ├── signup.js ├── search.js ├── nav.js ├── login.html ├── socket.js ├── home.html └── style.css ├── package.json ├── LICENSE ├── README.md ├── index.html └── app.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /passwords.js -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/messages/Messages.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/profile/Profile.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/settings/Settings.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akvnn/xero/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akvnn/xero/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akvnn/xero/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true 8 | } -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Profile = () => { 4 | return
Profile
; 5 | }; 6 | 7 | export default Profile; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/messages/Messages.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Messages = () => { 4 | return
Messages
; 5 | }; 6 | 7 | export default Messages; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Settings = () => { 4 | return
Settings
; 5 | }; 6 | 7 | export default Settings; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/protectedRoutes/ProtectedRoutes.css: -------------------------------------------------------------------------------- 1 | .homeContainer { 2 | width: 100vw; 3 | height: 100vh; 4 | background-color: #f6f5f7; 5 | display: flex; 6 | flex-direction: row; 7 | margin: 1rem 10rem; 8 | position: relative; 9 | } -------------------------------------------------------------------------------- /frontend/src/components/searchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | import "./SearchBar.css"; 2 | 3 | const SearchBar = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default SearchBar; 12 | -------------------------------------------------------------------------------- /frontend/src/components/searchBar/SearchBar.css: -------------------------------------------------------------------------------- 1 | .searchBarInput { 2 | width: 45%; 3 | padding: 0.8rem; 4 | font-size: 1rem; 5 | font-weight: 300; 6 | border: 1px solid #ccc; 7 | border-radius: 10px; 8 | outline: none; 9 | margin: 1rem; 10 | margin-bottom: 0; 11 | margin-right: auto; 12 | } 13 | .searchBarInput:focus { 14 | border: 1px solid #000; 15 | } -------------------------------------------------------------------------------- /public/login.js: -------------------------------------------------------------------------------- 1 | const loginbtn = document.getElementById('loginbtn') 2 | loginbtn.addEventListener('click', async (event) => { 3 | event.preventDefault() 4 | const usernameOremailOrphone = document.getElementById( 5 | 'usernameOremailOrphone' 6 | ).value 7 | const password = document.getElementById('passwordLogin').value 8 | await login(usernameOremailOrphone, password) 9 | }) 10 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | import "./index.css"; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById("root")); 8 | root.render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialmediawebsite", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.18.2", 15 | "jsonwebtoken": "^9.0.1", 16 | "mongodb": "^5.7.0", 17 | "socket.io": "^4.7.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/sideBoard/SideBoard.js: -------------------------------------------------------------------------------- 1 | import Searchbar from "../searchBar/SearchBar"; 2 | import FollowersBoard from "../followersBoard/FollowersBoard"; 3 | import "./SideBoard.css"; 4 | 5 | const SideBoard = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default SideBoard; 16 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # How to run this React frontend App locally on your machine 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | The following steps can be used to run this app locally: 6 | 7 | 1. Git clone the repository on your terminal 8 | 9 | 2. cd into xero 10 | 11 | 3. Then cd into frontend 12 | 13 | 4. npm install dependencies 14 | 15 | 5. npm start 16 | 17 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 18 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/protectedRoutes/ProtectedRoutes.js: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import SideBar from "../../../components/sideBar/SideBar"; 3 | import SideBoard from "../../../components/sideBoard/SideBoard"; 4 | import "./ProtectedRoutes.css"; 5 | 6 | const ProtectedRoutes = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default ProtectedRoutes; 19 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "rules": { 19 | "react/react-in-jsx-scope": "off", 20 | "react/no-unknown-property": ["error", { "ignore" : ["jsx", "js"] }] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/pages/publicPages/signup/Signup.css: -------------------------------------------------------------------------------- 1 | .alertBox { 2 | font-family: 'Montserrat', sans-serif; 3 | font-weight: 100; 4 | position: fixed; 5 | top: 1.25rem; 6 | left: 1.25rem; 7 | width: 20%; 8 | padding: 1.2rem; 9 | color: #fff; 10 | font-size: 1rem; 11 | border-radius: 0.3125rem; 12 | display: none; 13 | z-index: 1000; 14 | transform: translate(10%, 30%); 15 | transition: transform 0.5s ease-in-out; 16 | } 17 | .loginLink { 18 | cursor: pointer; 19 | } 20 | .loginLinkDiv { 21 | margin-top: 10rem; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: flex-start; 26 | } -------------------------------------------------------------------------------- /frontend/src/pages/publicPages/login/Login.css: -------------------------------------------------------------------------------- 1 | 2 | .loginSection { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | .alternatebtn { 8 | width: 17rem; 9 | padding: 1rem 3rem; 10 | border: none; 11 | border-radius: 20px; 12 | cursor: pointer; 13 | font-family: 'Montserrat', sans-serif; 14 | font-weight: 500; 15 | color: #fff; 16 | background-color: #000; 17 | margin-top: 1rem; 18 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 19 | transition: all 0.3s; 20 | } 21 | .alternatebtn:hover { 22 | background-color: #fff; 23 | color: #000; 24 | } 25 | .or { 26 | margin: 1rem 0; 27 | font-size: 1.2rem; 28 | font-weight: 500; 29 | color: #000; 30 | position: relative; 31 | } 32 | .loginform { 33 | margin-top: 0; 34 | } 35 | -------------------------------------------------------------------------------- /public/lost.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xero 7 | 11 | 12 | 13 | 14 |
15 |

you seem lost huh?

16 |

17 | go back to sign up 18 |

19 |
20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/sideBoard/SideBoard.css: -------------------------------------------------------------------------------- 1 | .search { 2 | background-color: #fff; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: flex-start; 6 | } 7 | .searchResults { 8 | width: 45%; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: flex-start; 12 | height: auto; 13 | margin: 1rem; 14 | margin-top: 0; 15 | } 16 | .searchResult { 17 | padding: 0.5rem; 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | cursor: pointer; 22 | transition: all 0.3s; 23 | border-bottom: 1px solid #ccc; 24 | border-radius: 5px; 25 | } 26 | .searchResult:hover { 27 | background-color: #ccc; 28 | } 29 | .searchImg { 30 | margin-right: 0rem; 31 | width: 2rem; 32 | height: 2rem; 33 | border-radius: 50%; 34 | } 35 | .searchName { 36 | margin-right: 0.5rem; 37 | margin-left: 1rem; 38 | font-size: 1rem; 39 | font-weight: 500; 40 | } 41 | .searchUsername { 42 | margin-right: 0.5rem; 43 | font-size: 0.8rem; 44 | font-weight: 300; 45 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | 3 | import Signup from "./pages/publicPages/signup/Signup"; 4 | import Login from "./pages/publicPages/login/Login"; 5 | import ProtectedRoutes from "./pages/privatePages/protectedRoutes/ProtectedRoutes"; 6 | import Home from "./pages/privatePages/home/Home"; 7 | import Profile from "./pages/privatePages/profile/Profile"; 8 | import Messages from "./pages/privatePages/messages/Messages"; 9 | import Settings from "./pages/privatePages/settings/Settings"; 10 | 11 | function App() { 12 | return ( 13 | 14 | } /> 15 | } /> 16 | }> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | 22 | 23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Akvn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/components/followersBoard/FollowersBoard.js: -------------------------------------------------------------------------------- 1 | import "./FollowersBoard.css"; 2 | 3 | const FollowersBoard = () => { 4 | return ( 5 | <> 6 |
7 |

Who To Follow?

8 |
9 | 10 |

John Doe

11 |

@johndoe

12 | 13 |
14 |
15 | 16 |

akvn

17 |

@akvn

18 | 19 |
20 |
21 | 22 |

bonga

23 |

@duokobia

24 | 25 |
26 |
27 | 28 | ); 29 | }; 30 | 31 | export default FollowersBoard; 32 | -------------------------------------------------------------------------------- /frontend/src/pages/publicPages/login/Login.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import "./Login.css"; 4 | 5 | const Login = () => { 6 | const navigate = useNavigate(); 7 | 8 | const handleSignIn = () => { 9 | navigate("/home"); 10 | }; 11 | return ( 12 |
13 |
14 |
15 |

Sign in to xero

16 | 19 | 22 |

- or -

23 |
24 | 30 | 31 | 32 | 33 | 36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default Login; 43 | -------------------------------------------------------------------------------- /frontend/src/components/followersBoard/FollowersBoard.css: -------------------------------------------------------------------------------- 1 | 2 | .whoToFollowHeading { 3 | margin: 1rem; 4 | margin-top: 0.5rem; 5 | font-size: 1.2rem; 6 | font-weight: 500; 7 | } 8 | .whoToFollow { 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: flex-start; 12 | margin: 1rem; 13 | background-color: #f6f5f7; 14 | margin-right: auto; 15 | padding: 1rem; 16 | border-radius: 10px; 17 | max-width: 70%; 18 | } 19 | 20 | .suggestionAccount { 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | margin-bottom: 1rem; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | border-bottom: 1px solid #ccc; 28 | border-radius: 5px; 29 | } 30 | .suggestionAccount:hover { 31 | transform: translateY(-5px); 32 | } 33 | .suggestionImg { 34 | margin-right: 0rem; 35 | width: 2rem; 36 | height: 2rem; 37 | border-radius: 50%; 38 | } 39 | .suggestionName { 40 | margin-right: 0.5rem; 41 | margin-left: 1rem; 42 | font-size: 1rem; 43 | font-weight: 500; 44 | } 45 | .suggestionUsername { 46 | margin-right: 0.5rem; 47 | font-size: 0.8rem; 48 | font-weight: 300; 49 | } 50 | .followButton { 51 | padding: 0.5rem 1rem; 52 | border: none; 53 | font-weight: 500; 54 | font-size: 0.5rem; 55 | border-radius: 5px; 56 | cursor: pointer; 57 | transition: all 0.3s; 58 | } 59 | .followButton:hover { 60 | background-color: #ccc; 61 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-icons": "^5.0.1", 12 | "react-router-dom": "^6.22.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint --fix", 23 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.56.0", 45 | "eslint-config-prettier": "^9.1.0", 46 | "eslint-plugin-prettier": "^5.1.3", 47 | "eslint-plugin-react": "^7.33.2", 48 | "prettier": "^3.2.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/pages/publicPages/signup/Signup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import "./Signup.css"; 4 | 5 | const Signup = () => { 6 | return ( 7 |
8 |
9 |
10 |

Join xero Today.

11 |
12 | 13 | 14 | 15 | 16 | 19 |
20 |
21 |

22 | Already a member?{" "} 23 | 24 | {" "} 25 | Login{" "} 26 | 27 |

28 |

29 | Too lazy?{" "} 30 | 31 | {" "} 32 | Use a demo account{" "} 33 | 34 |

35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Signup; 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xero 2 | xero is a full stack social media platform that enables users to share thoughts, interact, and chat in real-time.
3 | ### __Go To__ [__Live App__](https://xero.onrender.com/)
4 | ### Project Screenshots 5 | ![xero](https://github.com/akvnn/xero/assets/106168970/d03b5c94-f625-435b-b2b3-f5ff81631092) 6 |

7 | ![xero2](https://github.com/akvnn/xero/assets/106168970/2e1c7a89-e169-450d-8565-417c8421acf3) 8 |

9 | ![xero3](https://github.com/akvnn/xero/assets/106168970/c8e642b7-e28e-4f1f-a466-454262452614) 10 | 11 | ### Technologies Used 12 | NodeJS (ESM) & ExpressJS for the server-side
13 | Socket.io for real-time communication
14 | MongoDB as the persistent database
15 | HTML/CSS/JavaScript for the client-side
16 | 17 | ### Main Features 18 | Secure authentication using JWT (JSON Web Tokens)
19 | User registration and login functionality
20 | Account creation and management
21 | User profiles with profile pictures, usernames, and other essential details
22 | Ability to view and update user profiles
23 | Real-time news feed displaying posts from followed users
24 | Create and post text content
25 | Post comments and reply to comments with threaded discussions
26 | Follow and unfollow other users to see their messages in your feed
27 | Send private messages to other users
28 | Real-time updates for new posts and messages using socket.io
29 | Search for users and discover new users
30 | and much more! 31 | 32 | ### Prerequisites 33 | Clone the repository
34 | Install required packages using `npm install`
35 | Add passwords.js file with your mongodb credentials
36 | Run the application using `node app.mjs`
37 | Note: make sure you are using ESM and not CommonJS as the module system
38 | 39 | ### Credits 40 | xero is fully developed by myself [_Akvn_](https://www.akvn.xyz/) __@akvnn__ 41 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/sideBar/SideBar.css: -------------------------------------------------------------------------------- 1 | .navbarContainer { 2 | width: 30rem; 3 | height: 100vh; 4 | background-color: #fff; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: flex-start; 8 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 9 | } 10 | .navbar { 11 | background-color: #fff; 12 | display: flex; 13 | } 14 | .navBtn { 15 | width: auto; 16 | box-shadow: none; 17 | border: none; 18 | margin-right: 0.5rem; 19 | } 20 | .listItem { 21 | width: auto; 22 | margin: 1rem 0; 23 | padding: 1rem 2rem; 24 | border-radius: 20px; 25 | font-family: 'Montserrat', sans-serif; 26 | font-weight: 500; 27 | font-size: 1rem; 28 | cursor: pointer; 29 | transition: all 0.3s; 30 | } 31 | li.listItem:hover { 32 | background-color: #cfd1d7; 33 | color: #fff !important; 34 | } 35 | .navLink { 36 | text-decoration: none; 37 | color: #000; 38 | transition: all 0.3s; 39 | } 40 | .navLinkActive { 41 | color: #8a2be2; 42 | } 43 | .hover-color { 44 | color: #fff; 45 | } 46 | .profileNavbar { 47 | width: 100%; 48 | margin: auto; 49 | margin-bottom: 1rem; 50 | display: flex; 51 | flex-direction: row; 52 | align-items: center; 53 | justify-content: space-evenly; 54 | border-radius: 15px; 55 | cursor: pointer; 56 | font-family: 'Montserrat', sans-serif; 57 | font-weight: 300; 58 | } 59 | .profileNavbar:hover { 60 | background-color: #ccc; 61 | } 62 | .profilePic { 63 | margin-left: 0.2rem; 64 | width: 2rem; 65 | height: 2rem; 66 | border-radius: 50%; 67 | transition: all 0.3s; 68 | } 69 | .profilePic:hover { 70 | opacity: 0.8; 71 | transform: scale(1.1); 72 | } 73 | .name { 74 | margin-left: 1rem; 75 | font-size: 1rem; 76 | font-weight: 500; 77 | margin-right: 0.2rem; 78 | } 79 | .username { 80 | font-size: 0.8rem; 81 | font-weight: 300; 82 | } 83 | .fa-chevron-down { 84 | font-size: 0.8rem; 85 | font-weight: 300; 86 | color: #000; 87 | transition: all 0.3s; 88 | margin-right: 0.2rem; 89 | } 90 | .icon-spacing { 91 | margin-right: 0.3rem; 92 | } -------------------------------------------------------------------------------- /public/handleLogin.js: -------------------------------------------------------------------------------- 1 | const login = async (usernameOremailOrphone, password, type = 'main') => { 2 | try { 3 | if (!usernameOremailOrphone || !password) { 4 | throw new Error('Please fill all required data') 5 | } 6 | if (password.length < 8) { 7 | throw new Error('Invalid Information') 8 | } 9 | const data = { 10 | usernameOremailOrphone: usernameOremailOrphone, 11 | password: password, 12 | } 13 | const response = await fetch('/login', { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify(data), 19 | }) 20 | const dataAfterResponse = await response.json() 21 | if (dataAfterResponse.status != true) { 22 | throw new Error(dataAfterResponse.message) 23 | } 24 | let alertBox = document.getElementById('alertBox2') 25 | if (type == 'demo') { 26 | alertBox = document.getElementById('alertBox') 27 | } 28 | alertBox.innerText = dataAfterResponse.message 29 | alertBox.style.backgroundColor = 'green' 30 | alertBox.style.display = 'block' 31 | setTimeout(() => { 32 | alertBox.style.display = 'none' 33 | }, 3000) 34 | // cookies 35 | const expirationDate = 7 36 | const date = new Date() 37 | date.setTime(date.getTime() + expirationDate * 24 * 60 * 60 * 1000) 38 | document.cookie = `token=${ 39 | dataAfterResponse.token 40 | }; expires=${date.toUTCString()}; path=/` 41 | // end of cookies 42 | window.location.href = '/home' 43 | // localStorage.setItem('token', dataAfterResponse.token) 44 | // window.location.href = '/home' 45 | } catch (err) { 46 | console.error(err) 47 | let alertBox = document.getElementById('alertBox2') 48 | if (alertBox == null) { 49 | alertBox = document.getElementById('alertBox') 50 | } 51 | alertBox.innerText = err 52 | alertBox.style.backgroundColor = 'red' 53 | alertBox.style.display = 'block' 54 | setTimeout(() => { 55 | alertBox.style.display = 'none' 56 | }, 3000) 57 | } 58 | } 59 | window.login = login 60 | -------------------------------------------------------------------------------- /public/signup.js: -------------------------------------------------------------------------------- 1 | const signUp = async () => { 2 | try { 3 | const fullName = document.getElementById('fullName').value 4 | const username = document.getElementById('username').value 5 | const email = document.getElementById('email').value 6 | const password = document.getElementById('password').value 7 | if (!fullName || !username || !email || !password) { 8 | throw new Error('Please fill all required data') 9 | } 10 | if (password.length < 8) { 11 | throw new Error('Password must include more than 8 characters') 12 | } 13 | if (!email.includes('@')) { 14 | throw new Error('Please enter a valid email') 15 | } 16 | const data = { 17 | fullName: fullName, 18 | username: username, 19 | email: email, 20 | password: password, 21 | } 22 | const response = await fetch('/signup', { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify(data), 28 | }) 29 | const dataAfterResponse = await response.json() 30 | if (dataAfterResponse.status != true) { 31 | throw new Error(dataAfterResponse.message) 32 | } 33 | const alertBox = document.getElementById('alertBox') 34 | alertBox.innerText = dataAfterResponse.message 35 | alertBox.style.backgroundColor = 'green' 36 | alertBox.style.display = 'block' 37 | setTimeout(() => { 38 | alertBox.style.display = 'none' 39 | }, 3000) 40 | window.location.href = '/login' 41 | } catch (err) { 42 | console.error(err) 43 | const alertBox = document.getElementById('alertBox') 44 | alertBox.innerText = err 45 | alertBox.style.backgroundColor = 'red' 46 | alertBox.style.display = 'block' 47 | setTimeout(() => { 48 | alertBox.style.display = 'none' 49 | }, 3000) 50 | } 51 | } 52 | const signupbtn = document.getElementById('signupbtn') 53 | signupbtn.addEventListener('click', async (event) => { 54 | event.preventDefault() 55 | await signUp() 56 | }) 57 | 58 | const demologin = document.getElementById('demoLink') 59 | demologin.addEventListener('click', async (event) => { 60 | event.preventDefault() 61 | await login('dd', '12345678', 'demo') 62 | }) 63 | -------------------------------------------------------------------------------- /frontend/src/components/sideBar/SideBar.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { IoHome, IoSearch, IoPersonOutline, IoSettingsOutline } from "react-icons/io5"; 3 | import { FaRegEnvelope } from "react-icons/fa6"; 4 | import { IoIosArrowDown } from "react-icons/io"; 5 | import "./SideBar.css"; 6 | 7 | const SideBar = () => { 8 | return ( 9 |
10 | 11 | 42 | 43 | 46 |
47 | 48 |
49 |

50 | Demo 51 |

52 |

53 |
54 | 55 |
56 |
57 | ); 58 | }; 59 | 60 | export default SideBar; 61 | -------------------------------------------------------------------------------- /public/search.js: -------------------------------------------------------------------------------- 1 | const delay = 300 2 | let typingTimer 3 | const searchBarInput = document.getElementById('searchBarInput') 4 | 5 | searchBarInput.addEventListener('input', async (event) => { 6 | try { 7 | clearTimeout(typingTimer) 8 | const token = getCookie('token') 9 | typingTimer = setTimeout(async () => { 10 | const searchInputValue = searchBarInput.value 11 | if (searchInputValue.length <= 0) { 12 | const searchResultsContainer = document.getElementById('searchResults') 13 | searchResultsContainer.innerHTML = '' // clear old search results 14 | return 15 | } 16 | const response = await fetch('/search', { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | Authorization: 'Bearer ' + token, 21 | }, 22 | body: JSON.stringify({ 23 | query: searchInputValue, 24 | }), 25 | }) 26 | const data = await response.json() 27 | if (data.status != true) { 28 | throw new Error(data.message) 29 | } 30 | const searchResultsContainer = document.getElementById('searchResults') 31 | searchResultsContainer.innerHTML = '' // clear old search results 32 | data.users.forEach((user) => { 33 | const searchResult = document.createElement('div') 34 | searchResult.classList.add('searchResult') 35 | const searchImg = document.createElement('img') 36 | searchImg.src = user.profilePicture 37 | searchImg.alt = user.fullName 38 | searchImg.classList.add('searchImg') 39 | const searchName = document.createElement('div') 40 | searchName.classList.add('searchName') 41 | searchName.innerText = user.fullName 42 | const searchUsername = document.createElement('div') 43 | searchUsername.classList.add('searchUsername') 44 | searchUsername.innerText = '@' + user.username 45 | searchResult.appendChild(searchImg) 46 | searchResult.appendChild(searchName) 47 | searchResult.appendChild(searchUsername) 48 | searchResult.addEventListener('click', (e) => { 49 | e.stopPropagation() 50 | goToProfile(user.username) 51 | searchBarInput.value = '' // clear search bar 52 | searchResultsContainer.innerHTML = '' // clear search results 53 | }) 54 | searchResultsContainer.appendChild(searchResult) 55 | }) 56 | }, delay) 57 | } catch (err) { 58 | console.log(err) 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /public/nav.js: -------------------------------------------------------------------------------- 1 | // post tweet button 2 | const postTweetBtn = document.getElementById('post') 3 | postTweetBtn.addEventListener('click', (event) => { 4 | const ele = document.getElementById('postTweetDiv') 5 | if (ele.classList.contains('postTweet')) { 6 | ele.classList.remove('postTweet') 7 | ele.classList.add('postTweetHidden') 8 | } else { 9 | ele.classList.remove('postTweetHidden') 10 | ele.classList.add('postTweet') 11 | } 12 | }) 13 | const postTweetClose = document.getElementById('postTweetX') 14 | postTweetClose.addEventListener('click', (event) => { 15 | const ele = document.getElementById('postTweetDiv') 16 | ele.classList.remove('postTweet') 17 | ele.classList.add('postTweetHidden') 18 | }) 19 | // handle post tweet 20 | const postTweetButton = document.getElementById('postTweetButton') 21 | postTweetButton.addEventListener('click', async () => { 22 | try { 23 | const textArea = document.getElementById('postTweetTextArea') 24 | const content = textArea.value 25 | if (content.length == 0) { 26 | throw new Error('Tweet cannot be empty') 27 | } 28 | const cookie = getCookie('token') 29 | const response = await fetch('/postTweet', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | Authorization: 'Bearer ' + cookie, 34 | }, 35 | body: JSON.stringify({ 36 | content: content, 37 | }), 38 | }) 39 | const data = await response.json() 40 | if (data.status != true) { 41 | throw new Error(data.message) 42 | } 43 | // to do : if we are in home page.. 44 | // add tweet div (show tweet for the user who posted it) 45 | const tweetsContainer = document.getElementById('tweets') 46 | const tweetDiv = createTweet(data.tweet) 47 | tweetsContainer.prepend(tweetDiv) 48 | // clear textarea 49 | textArea.value = '' 50 | // close post tweet div 51 | const ele = document.getElementById('postTweetDiv') 52 | ele.classList.remove('postTweet') 53 | ele.classList.add('postTweetHidden') 54 | } catch (err) { 55 | console.log(err) 56 | } 57 | }) 58 | //navbar profile picture 59 | const profilePic = document.getElementById('profilePic') 60 | profilePic.addEventListener('click', (e) => { 61 | e.stopPropagation() 62 | const url = window.location.href 63 | const urlSplit = url.split('/') 64 | const username = document.getElementById('profileUsername').innerText 65 | if (urlSplit[urlSplit.length - 1] != username) { 66 | goToProfile(username) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xero | sign up 7 | 11 | 12 | 63 | 64 | 65 |
66 |
67 |

Join xero Today.

68 |
69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |

77 | Already a member? Login 78 |

79 |

80 | Too lazy? Use a demo account 81 |

82 |
83 |
84 | 85 | 86 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xero | log in 7 | 11 | 12 | 16 | 67 | 68 | 69 |
70 |
71 |

Sign in to xero

72 | 75 | 78 |

- or -

79 |
80 | 86 | 87 | 93 | 94 | 95 |
96 |
97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | overflow-x: hidden; 6 | font-family: 'Montserrat', sans-serif; 7 | } 8 | html { 9 | scroll-behavior: smooth; 10 | } 11 | .body { 12 | font-family: 'Montserrat', sans-serif; 13 | font-size: 1.6rem; 14 | } 15 | a { 16 | text-decoration: none; 17 | } 18 | 19 | /* scroll bar */ 20 | ::-webkit-scrollbar { 21 | width: 0.5; 22 | display: none; 23 | } 24 | ::-webkit-scrollbar-track { 25 | background: #f1f1f1; 26 | } 27 | ::-webkit-scrollbar-thumb { 28 | background: #888; 29 | } 30 | ::-webkit-scrollbar-thumb:hover { 31 | background: #555; 32 | } 33 | /* end of scroll bar */ 34 | 35 | .section { 36 | width: 100vw; 37 | height: 100vh; 38 | background-color: #f6f5f7; 39 | } 40 | .heading { 41 | text-align: center; 42 | margin: 4rem 0; 43 | font-size: 3rem; 44 | font-weight: 300; 45 | } 46 | .form { 47 | width: 100%; 48 | padding: 15px; 49 | display: flex; 50 | flex-direction: column; 51 | margin: auto; 52 | align-items: center; 53 | } 54 | .form input { 55 | margin-bottom: 1rem; 56 | width: 60vw; 57 | height: 5vh; 58 | border: 1px solid #ccc; 59 | border-radius: 5px; 60 | padding: 1rem; 61 | font-family: 'Montserrat' sans-serif; 62 | font-weight: 500; 63 | font-size: 1rem; 64 | outline: none; 65 | } 66 | .form input:focus { 67 | border: 1px solid #000; 68 | } 69 | .labelHidden { 70 | display: none; 71 | } 72 | .btn { 73 | padding: 1rem 3rem; 74 | border: none; 75 | border-radius: 5px; 76 | cursor: pointer; 77 | font-family: 'Montserrat', sans-serif; 78 | font-weight: 500; 79 | color: #fff; 80 | background-color: #000; 81 | margin-top: 1rem; 82 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 83 | transition: all 0.3s; 84 | } 85 | .btn:hover { 86 | background-color: #fff; 87 | color: #000; 88 | } 89 | .btnHidden { 90 | display: none; 91 | } 92 | .member { 93 | text-align: center; 94 | font-size: 1rem; 95 | font-weight: 600; 96 | color: #000; 97 | text-decoration: none; 98 | transition: all 0.3s; 99 | margin-bottom: 1rem; 100 | } 101 | .member a { 102 | text-decoration: none; 103 | transition: all 0.3s; 104 | } 105 | .member a:hover { 106 | color: #000; 107 | text-decoration: underline; 108 | } 109 | 110 | @media (max-width: 768px) { 111 | .alertBox { 112 | width: 80%; 113 | padding: 0.8rem; 114 | font-size: 0.9rem; 115 | border-radius: 0.25rem; 116 | top: 3rem; 117 | left: 50%; 118 | transform: translateX(-50%); 119 | } 120 | } 121 | .alert { 122 | display: block; 123 | background-color: crimson; 124 | color: #fff; 125 | text-align: center; 126 | padding: 0.8rem; 127 | border-radius: 0.25rem; 128 | margin-bottom: 0.5rem; 129 | } 130 | 131 | @media screen and (max-width: 768px) { 132 | .homeContainer { 133 | margin: 0; 134 | } 135 | } 136 | 137 | /* lost page */ 138 | .lostContainer { 139 | display: flex; 140 | flex-direction: column; 141 | align-items: center; 142 | } 143 | .lostParagraph { 144 | font-size: 1.5rem; 145 | font-weight: 500; 146 | margin: 1rem; 147 | } 148 | .lostLink { 149 | font-weight: 600; 150 | color: #8a2be2; 151 | cursor: pointer; 152 | transition: all 0.3s; 153 | } 154 | .lostLink:hover { 155 | color: #000; 156 | } 157 | /* end of lost page */ 158 | 159 | /* settings page */ 160 | .passwordDivHidden { 161 | display: none; 162 | } 163 | .passwordDiv { 164 | display: flex; 165 | flex-direction: column; 166 | align-items: center; 167 | width: auto; 168 | margin: 1rem; 169 | } 170 | .passwordDiv input { 171 | margin-bottom: 1rem; 172 | height: 5vh; 173 | border: 1px solid #ccc; 174 | border-radius: 5px; 175 | padding: 1rem; 176 | font-family: 'Montserrat' sans-serif; 177 | font-weight: 500; 178 | font-size: 1rem; 179 | outline: none; 180 | } 181 | .passwordDiv input:focus { 182 | border: 1px solid #000; 183 | } 184 | .passwordMessage { 185 | margin-top: 1rem; 186 | font-size: 1rem; 187 | font-weight: 300; 188 | color: #8a2be2; 189 | } 190 | /* end of settings page */ 191 | -------------------------------------------------------------------------------- /public/socket.js: -------------------------------------------------------------------------------- 1 | // handle socket.io 2 | const socket = io({ 3 | query: { token: getCookie('token') }, 4 | }) 5 | 6 | socket.on('connect', () => { 7 | console.log('connected') 8 | }) 9 | socket.on('newTweet', (tweet) => { 10 | // check what page we are on 11 | const url = window.location.href 12 | const urlSplit = url.split('/') 13 | const page = urlSplit[3] 14 | if (page == 'home') { 15 | // add tweet div for followers 16 | console.log(tweet) 17 | const tweetsContainer = document.getElementById('tweets') 18 | const tweetDiv = createTweet(tweet) 19 | tweetsContainer.prepend(tweetDiv) 20 | tweetDiv.classList.add('newTweet') // add new tweet style (background color) 21 | setTimeout(() => { 22 | tweetDiv.classList.remove('newTweet') 23 | }, 10000) 24 | } else { 25 | //do nothing for now 26 | } 27 | }) 28 | socket.on('newMessage', (message) => { 29 | // check what page we are on 30 | const url = window.location.href 31 | const urlSplit = url.split('/') 32 | const page = urlSplit[3] 33 | const messagesDetails = document.getElementById('messagesDetails') 34 | if ( 35 | page == 'messages' && 36 | messagesDetails.classList.contains('messagesDetails') 37 | ) { 38 | const currentMessageProfileId = messagesDetails 39 | .querySelector('.messageDetails') 40 | .getAttribute('id') 41 | if (message.from != currentMessageProfileId) { 42 | // if the message is not from the user currently in view 43 | return 44 | } 45 | const messageDetailsMessages = messagesDetails.querySelector( 46 | '#messageDetailsMessages' 47 | ) 48 | const messageDetailsMessage = document.createElement('div') 49 | messageDetailsMessage.classList.add('messageDetailsMessage') 50 | const messageDetailsMessageText = document.createElement('p') 51 | messageDetailsMessageText.classList.add('messageDetailsMessageText') 52 | messageDetailsMessageText.innerText = message.content 53 | const messageDetailsMessageTime = document.createElement('p') 54 | messageDetailsMessageTime.classList.add('messageDetailsMessageTime') 55 | const date = new Date() 56 | const month = date.getMonth() + 1 57 | const day = date.getDate() 58 | const hour = date.getHours() 59 | const minute = date.getMinutes() 60 | messageDetailsMessageTime.innerText = 61 | day + '/' + month + ' ' + hour + ':' + minute 62 | messageDetailsMessage.appendChild(messageDetailsMessageText) 63 | messageDetailsMessage.appendChild(messageDetailsMessageTime) 64 | messageDetailsMessages.appendChild(messageDetailsMessage) 65 | messageDetailsMessage.scrollIntoView() // scroll to bottom 66 | } else if (page == 'messages') { 67 | //change the lastMessage 68 | const lastMessage = document.querySelector( 69 | `[dataUserId="${message.from}"] p.lastMessage` 70 | ) 71 | if (lastMessage != null) { 72 | lastMessage.innerText = message.content 73 | } else { 74 | //create messageProfile from the user if its the first message 75 | const messageProfile = document.createElement('div') 76 | messageProfile.classList.add('messageProfile') 77 | messageProfile.setAttribute('dataUserId', message.from) 78 | const messageViewProfilePictureContainer = document.createElement('div') 79 | messageViewProfilePictureContainer.classList.add( 80 | 'messageViewProfilePictureContainer' 81 | ) 82 | const messageViewProfilePicture = document.createElement('img') 83 | messageViewProfilePicture.classList.add('messageViewProfilePicture') 84 | messageViewProfilePicture.src = message.fromProfile.profilePicture 85 | messageViewProfilePicture.alt = message.fromProfile.fullName 86 | messageViewProfilePictureContainer.appendChild(messageViewProfilePicture) 87 | const messageViewInfo = document.createElement('div') 88 | messageViewInfo.classList.add('messageViewInfo') 89 | const messageViewHeader = document.createElement('div') 90 | messageViewHeader.classList.add('messageViewHeader') 91 | const messageViewName = document.createElement('h3') 92 | messageViewName.classList.add('messageViewName') 93 | messageViewName.innerText = message.fromProfile.fullName 94 | const messageViewUsername = document.createElement('p') 95 | messageViewUsername.classList.add('messageViewUsername') 96 | messageViewUsername.innerText = '@' + message.fromProfile.username 97 | const messageViewDate = document.createElement('p') 98 | messageViewDate.classList.add('messageViewDate') 99 | const date = new Date(message.createdAt) 100 | const month = date.getMonth() + 1 101 | const day = date.getDate() 102 | messageViewDate.innerText = day + '/' + month 103 | messageViewHeader.appendChild(messageViewName) 104 | messageViewHeader.appendChild(messageViewUsername) 105 | messageViewHeader.appendChild(messageViewDate) 106 | const lastMessage = document.createElement('p') 107 | lastMessage.classList.add('lastMessage') 108 | lastMessage.innerText = message.content 109 | messageViewInfo.appendChild(messageViewHeader) 110 | messageViewInfo.appendChild(lastMessage) 111 | messageProfile.appendChild(messageViewProfilePictureContainer) 112 | messageProfile.appendChild(messageViewInfo) 113 | messageProfile.addEventListener('click', () => { 114 | goToMessages(message.fromProfile.username) //temporary 115 | }) 116 | const messagesView = document.getElementById('messagesView') 117 | messagesView.prepend(messageProfile) 118 | } 119 | } else { 120 | //do nothing for now 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/home/Home.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unknown-property */ 2 | import { Link } from "react-router-dom"; 3 | import { FaArrowLeft } from "react-icons/fa"; 4 | import "./Home.css"; 5 | 6 | const Home = () => { 7 | return ( 8 |
9 | 10 |

11 | {" "} 12 | 13 | Home 14 |

15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |

John Doe

24 |

@johndoe

25 |

May 17

26 |
27 |

Lorem ipsum dolor sit amet.

28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | 42 |
43 |
44 | 50 | 53 | 54 | 57 |
58 | 59 |
60 |

61 | Akvn 62 |

63 |

64 | @ak 65 |

66 |

67 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Delectus recusandae sapiente 68 | cupiditate molestiae harum, deleniti quaerat quisquam labore itaque vitae? 69 |

70 |
71 |
72 |
73 |

74 | 0 75 |

76 |

Following

77 |
78 |
79 |

80 | 0 81 |

82 |

Followers

83 |
84 |
85 |

86 | 0 87 |

88 |

Tweets

89 |
90 |
91 |
92 | 95 | 98 | 101 |
102 |
103 |
104 | 105 | 106 |
107 |
108 |
109 |
110 | 111 |

John Doe

112 |

@johndoe

113 |

28m

114 |
115 |
116 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsum, amet consequuntur 117 | ipsam qui deserunt quod ex adipisci minus quos Link. 118 |
119 |
120 |

121 | 2 122 |

123 |

124 | 7 125 |

126 |

127 | 12 128 |

129 |
130 |
131 |
132 |
133 |
134 | 135 |

x x

136 |

@xx

137 |

138 | 28m 139 |

140 |
141 |
142 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Rerum magni expl 143 |
144 |
145 |

146 | 2 147 |

148 |

149 | 7 150 |

151 |

152 | 12 153 |

154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | 164 |

x x

165 |

@xx

166 |

167 | 28m 168 |

169 |
170 |
171 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Rerum magni explicabo esse 172 | obcaecati vero optio, odio deserunt, cumque, dolor debitis reprehenderit beatae 173 | mollitia vel saepe incidunt quis id sunt. Aperiam modi laudantium porro autem neque 174 | deleniti optio, numquam eum, sint aliquam consequuntur, assumenda quos nesciunt! Harum 175 | ad ut nobis explicabo accusantium aliquam tempore suscipit, cupiditate culpa modi, 176 | quisquam mollitia, enim vero? Ea ex officiis quia mollitia perferendis doloribus 177 | numquam amet beatae quibusdam, blanditiis unde, at explicabo rem ducimus tempore 178 | suscipit cumque est nam quod corrupti? Voluptatum impedit, porro hic, eos, velit 179 | fugiat voluptatem id et quibusdam numquam voluptas quisquam incidunt! Link. 180 |
181 |
182 |

183 | 2 184 |

185 |

186 | 7 187 |

188 |

189 | 12 190 |

191 |
192 |
193 |
194 |
195 |
196 | ); 197 | }; 198 | 199 | export default Home; 200 | -------------------------------------------------------------------------------- /frontend/src/pages/privatePages/home/Home.css: -------------------------------------------------------------------------------- 1 | /* Spacings */ 2 | .horizontal-spacing1{ 3 | margin: 0 0.75rem 0 1.5rem; 4 | } 5 | 6 | /* profile section */ 7 | .profileHeader { 8 | border-bottom: 1px solid #ccc; 9 | position: relative; 10 | } 11 | .backColumn { 12 | margin-left: 1rem; 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: flex-start; 17 | } 18 | .fa-arrow-left { 19 | cursor: pointer; 20 | transition: all 0.3s; 21 | } 22 | .profileBannerPic { 23 | width: 100%; 24 | height: 10rem; 25 | object-fit: cover; 26 | border-radius: 10px; 27 | margin-bottom: 0.5rem; 28 | } 29 | .imgAndEdit { 30 | display: flex; 31 | flex-direction: row; 32 | align-items: flex-start; 33 | justify-content: flex-end; 34 | } 35 | .mainProfilePic { 36 | width: 8rem; 37 | height: 8rem; 38 | border-radius: 50%; 39 | margin-right: 1rem; 40 | transition: all 0.3s; 41 | cursor: pointer; 42 | position: absolute; 43 | top: 27%; 44 | left: 7%; 45 | border: 5px solid #fff; 46 | z-index: 1000; 47 | } 48 | .mainProfilePic:hover { 49 | transform: scale(1.1); 50 | } 51 | .editProfileButton { 52 | margin-right: 3rem; 53 | padding: 0.5rem 1.5rem; 54 | } 55 | .followUnfollowButton { 56 | margin-right: 3rem; 57 | padding: 0.5rem 1.5rem; 58 | } 59 | .directMessageButton { 60 | margin-right: 3rem; 61 | padding: 0.5rem 1.5rem; 62 | } 63 | .mainProfileNameDiv { 64 | display: flex; 65 | flex-direction: column; 66 | margin: 0.5rem; 67 | } 68 | .bio { 69 | margin: 0.5rem 0; 70 | font-size: 1rem; 71 | width: auto; 72 | } 73 | .profileCount { 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: flex-start; 77 | margin: 0.5rem; 78 | margin-top: 0; 79 | margin-bottom: 1rem; 80 | } 81 | .countItem { 82 | display: flex; 83 | flex-direction: row; 84 | align-items: center; 85 | justify-content: space-around; 86 | margin-right: 1rem; 87 | font-size: 0.8rem; 88 | } 89 | .countNumber { 90 | font-size: 1rem; 91 | font-weight: 500; 92 | margin-right: 0.2rem; 93 | } 94 | .profileNavigationButtons { 95 | display: flex; 96 | flex-direction: row; 97 | justify-content: flex-start; 98 | align-items: center; 99 | margin-top: 0.5rem; 100 | margin-bottom: 0rem; 101 | } 102 | .profileNavButton { 103 | margin-bottom: 0; 104 | font-size: 0.8rem; 105 | cursor: pointer; 106 | } 107 | .profileNavButtonActive { 108 | background-color: #ccc; 109 | } 110 | .editProfile { 111 | display: flex; 112 | flex-direction: column; 113 | justify-content: flex-start; 114 | align-items: center; 115 | position: absolute; 116 | width: 50vw; 117 | height: 80vh; 118 | background-color: #fff; 119 | display: flex; 120 | top: 50%; 121 | left: 10%; 122 | z-index: 10000; 123 | transform: translateY(-50%); 124 | border-radius: 10px; 125 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 126 | } 127 | .editProfileHeading { 128 | margin: 1rem; 129 | font-size: 1.5rem; 130 | font-weight: 500; 131 | } 132 | .editProfileForm { 133 | display: flex; 134 | flex-direction: column; 135 | justify-content: flex-start; 136 | align-items: center; 137 | } 138 | .editProfileButtons { 139 | display: flex; 140 | } 141 | .editProfileInput { 142 | width: 100%; 143 | padding: 1rem; 144 | font-size: 1rem; 145 | font-weight: 300; 146 | border: 1px solid #ccc; 147 | border-radius: 10px; 148 | margin: 0.5rem; 149 | overflow-wrap: break-word; 150 | word-wrap: break-word; 151 | } 152 | .editProfileFormBio { 153 | width: 100%; 154 | padding: 1rem; 155 | font-size: 1rem; 156 | font-weight: 300; 157 | border: 1px solid #ccc; 158 | border-radius: 10px; 159 | margin: 0.5rem; 160 | overflow-wrap: break-word; 161 | word-wrap: break-word; 162 | resize: none; 163 | } 164 | .editProfileCancelButton { 165 | margin-left: 1rem; 166 | } 167 | 168 | /* messages section */ 169 | .messagesView { 170 | display: flex; 171 | flex-direction: column; 172 | justify-content: flex-start; 173 | height: auto; 174 | } 175 | .messagesViewHidden { 176 | display: none; 177 | } 178 | .messageProfile { 179 | display: flex; 180 | flex-direction: row; 181 | align-items: center; 182 | cursor: pointer; 183 | transition: all 0.3s; 184 | border-bottom: 1px solid #ccc; 185 | border-radius: 5px; 186 | width: 100%; 187 | } 188 | .messageProfile:hover { 189 | background-color: #ccc; 190 | } 191 | .messageViewProfilePictureContainer { 192 | width: 4rem; 193 | height: 4rem; 194 | border-radius: 50%; 195 | margin: 1rem; 196 | overflow: hidden; 197 | display: flex; 198 | justify-content: center; 199 | align-items: center; 200 | } 201 | .messageViewProfilePicture { 202 | max-width: 100%; 203 | max-height: 100%; 204 | width: auto; 205 | height: auto; 206 | object-fit: contain; 207 | } 208 | .messageViewHeader { 209 | display: flex; 210 | flex-direction: row; 211 | align-items: center; 212 | justify-content: space-between; 213 | margin-bottom: 0.5rem; 214 | width: 100%; 215 | } 216 | .messageViewInfo { 217 | display: flex; 218 | flex-direction: column; 219 | align-items: flex-start; 220 | width: 100%; 221 | } 222 | .messageViewName { 223 | font-size: 1rem; 224 | font-weight: 500; 225 | margin-right: 0.5rem; 226 | } 227 | .messageViewUsername { 228 | font-size: 0.8rem; 229 | font-weight: 300; 230 | } 231 | .messageViewDate { 232 | font-size: 0.8rem; 233 | font-weight: 300; 234 | margin-left: auto; 235 | margin-right: 1rem; 236 | } 237 | .lastMessage { 238 | font-size: 0.8rem; 239 | font-weight: 300; 240 | } 241 | .messagesDetailsHidden { 242 | display: none; 243 | } 244 | .messageDetails { 245 | display: flex; 246 | flex-direction: column; 247 | justify-content: flex-start; 248 | height: 38rem; 249 | width: 100%; 250 | } 251 | .messageDetailsHidden { 252 | display: none; 253 | } 254 | .messageDetailsHeader { 255 | display: flex; 256 | flex-direction: row; 257 | align-items: center; 258 | justify-content: space-between; 259 | border-bottom: 1px solid #ccc; 260 | position: fixed; 261 | width: 41.5rem; 262 | } 263 | .messageDetailsInfo { 264 | display: flex; 265 | flex-direction: column; 266 | align-items: flex-start; 267 | width: 100%; 268 | } 269 | .messageDetailsName { 270 | font-size: 1rem; 271 | font-weight: 500; 272 | margin-right: 0.5rem; 273 | } 274 | .messageDetailsUsername { 275 | font-size: 0.8rem; 276 | font-weight: 300; 277 | } 278 | .messageDetailsProfilePictureContainer { 279 | width: 4rem; 280 | height: 4rem; 281 | border-radius: 50%; 282 | margin: 1rem; 283 | overflow: hidden; 284 | display: flex; 285 | justify-content: center; 286 | align-items: center; 287 | } 288 | .messageDetailsProfilePicture { 289 | max-width: 100%; 290 | max-height: 100%; 291 | width: auto; 292 | height: auto; 293 | object-fit: contain; 294 | cursor: pointer; 295 | } 296 | .messageDetailsMessages { 297 | display: flex; 298 | flex-direction: column; 299 | justify-content: flex-start; 300 | padding: 1rem; 301 | margin-top: 7rem; 302 | height: auto; 303 | } 304 | .messageDetailsMessage { 305 | display: flex; 306 | flex-direction: column; 307 | justify-content: flex-start; 308 | align-items: flex-start; 309 | margin-bottom: 0.5rem; 310 | width: 100%; 311 | flex-shrink: 0; 312 | } 313 | .messageDetailsMessageText { 314 | font-size: 0.8rem; 315 | font-weight: 300; 316 | margin-bottom: 0.5rem; 317 | border-radius: 5px; 318 | padding: 0.5rem; 319 | background-color: #e0e0e0; 320 | width: 70%; 321 | word-wrap: break-word; 322 | overflow-wrap: break-word; 323 | word-break: break-all; 324 | overflow: hidden; 325 | } 326 | .messageDetailsMessageTextSent { 327 | margin-left: auto; 328 | background-color: #8a2be2; 329 | color: #fff; 330 | } 331 | .messageDetailsMessageTime { 332 | font-size: 0.6rem; 333 | font-weight: 300; 334 | } 335 | .messageDetailsMessageTimeReceived { 336 | margin-right: auto; 337 | } 338 | .messageDetailsMessageTimeSent { 339 | margin-left: auto; 340 | } 341 | .messageDetailsInput { 342 | height: 100%; 343 | display: flex; 344 | flex-direction: row; 345 | align-items: center; 346 | justify-content: center; 347 | margin-top: auto; 348 | } 349 | .messageDetailsInputHidden { 350 | display: none; 351 | } 352 | .messageDetailsInputText { 353 | width: 90%; 354 | padding: 0.8rem; 355 | font-size: 1rem; 356 | font-weight: 300; 357 | border-radius: 10px; 358 | outline: none; 359 | margin: 0.5rem; 360 | margin-bottom: 0; 361 | border: none; 362 | resize: none; 363 | background-color: #e0e0e0; 364 | transition: all 0.3s; 365 | } 366 | .messageDetailsInputText:focus { 367 | border: 1px solid #000; 368 | } 369 | .messageDetailsInputButton { 370 | margin-right: 1rem; 371 | border: none; 372 | padding: 0.5rem 1.5rem; 373 | font-weight: 500; 374 | font-size: 0.8rem; 375 | border-radius: 5px; 376 | cursor: pointer; 377 | transition: all 0.3s; 378 | box-shadow: none; 379 | } 380 | .targetHeading { 381 | margin: 1rem; 382 | font-size: 1.5rem; 383 | font-weight: 500; 384 | } 385 | .followingButton { 386 | width: 100%; 387 | padding: 1rem 2rem; 388 | border: none; 389 | font-weight: 500; 390 | font-size: 1rem; 391 | border-radius: 5px; 392 | margin-bottom: 1rem; 393 | transition: all 0.3s; 394 | cursor: default; 395 | } 396 | .followingButton:hover { 397 | background-color: #ccc; 398 | } 399 | .tweets { 400 | display: flex; 401 | flex-direction: column; 402 | height: auto; 403 | position: relative; 404 | } 405 | .tweet { 406 | width: 100%; 407 | padding: 1rem; 408 | border-bottom: 1px solid #ccc; 409 | cursor: pointer; 410 | transition: all 0.3s; 411 | } 412 | .tweet:hover { 413 | background-color: #ccc; 414 | } 415 | .newTweet { 416 | background-color: bisque; 417 | } 418 | .tweet:hover .tweetComments { 419 | background-color: #f6f5f7; 420 | } 421 | .tweetLoading { 422 | border-color: #000; 423 | } 424 | .tweetCommentsHidden { 425 | display: none; 426 | } 427 | .tweetComments { 428 | display: block; 429 | width: 90%; 430 | margin: 0 auto; 431 | } 432 | .tweetProfilePic { 433 | width: 2rem; 434 | height: 2rem; 435 | border-radius: 50%; 436 | margin-right: 1rem; 437 | transition: all 0.3s; 438 | } 439 | .tweetProfilePic:hover { 440 | opacity: 0.8; 441 | transform: scale(1.1); 442 | } 443 | .profileName { 444 | display: flex; 445 | flex-direction: row; 446 | align-items: center; 447 | margin-bottom: 0.5rem; 448 | } 449 | .tweetName { 450 | font-size: 1.2rem; 451 | font-weight: 500; 452 | margin-right: 0.5rem; 453 | } 454 | .tweetTime { 455 | margin-left: auto; 456 | } 457 | .tweetText { 458 | font-size: 1rem; 459 | font-weight: 300; 460 | margin-bottom: 0.5rem; 461 | } 462 | .tweetText img { 463 | width: 100%; 464 | height: 20rem; 465 | object-fit: cover; 466 | border-radius: 10px; 467 | margin-bottom: 0.5rem; 468 | } 469 | .tweetChoices { 470 | display: flex; 471 | flex-direction: row; 472 | justify-content: center; 473 | } 474 | .tweetChoice { 475 | margin-right: 1rem; 476 | padding: 0.5rem; 477 | transition: all 0.3s; 478 | cursor: pointer; 479 | } 480 | .tweetChoiceActive { 481 | color: #8a2be2; 482 | transition: all 0.3s; 483 | transform: scale(1.1); 484 | } 485 | .tweetChoice:hover { 486 | color: #fff; 487 | transform: scale(1.1); 488 | } 489 | .postTweetHidden { 490 | display: none; 491 | } 492 | .postTweet { 493 | flex-direction: column; 494 | justify-content: flex-start; 495 | align-items: center; 496 | position: absolute; 497 | width: 50vw; 498 | height: 50vh; 499 | background-color: #fff; 500 | display: flex; 501 | top: 50%; 502 | left: 10%; 503 | z-index: 10000; 504 | transform: translateY(-50%); 505 | border-radius: 10px; 506 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 507 | } 508 | .postTweetX { 509 | font-weight: 500; 510 | font-size: 1.2rem; 511 | margin: 1rem; 512 | margin-right: auto; 513 | cursor: pointer; 514 | } 515 | .postTweetInput { 516 | width: 90%; 517 | height: auto; 518 | padding: 0.8rem; 519 | font-size: 1rem; 520 | font-weight: 300; 521 | border: 1px solid #ccc; 522 | border-radius: 10px; 523 | outline: none; 524 | margin: 1rem; 525 | margin-bottom: 0; 526 | border: none; 527 | overflow-wrap: break-word; 528 | word-wrap: break-word; 529 | resize: none; 530 | } 531 | .postTweetButton { 532 | width: 50%; 533 | } 534 | -------------------------------------------------------------------------------- /public/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xero 7 | 11 | 12 | 16 | 67 | 68 | 69 |
70 |
71 | X 72 | 73 | 81 | 82 |
83 |
84 | X 85 | 86 | 94 | 95 |
96 | 133 | 135 |
136 | 137 | 250 | 254 | 255 | 256 | 325 | 328 | 387 | 425 | 426 |
427 | 461 |
462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | overflow-x: hidden; 6 | font-family: 'Montserrat', sans-serif; 7 | } 8 | html { 9 | scroll-behavior: smooth; 10 | } 11 | .body { 12 | font-family: 'Montserrat', sans-serif; 13 | font-size: 1.6rem; 14 | } 15 | a { 16 | text-decoration: none; 17 | } 18 | /* scroll bar */ 19 | ::-webkit-scrollbar { 20 | width: 0.5; 21 | display: none; 22 | } 23 | ::-webkit-scrollbar-track { 24 | background: #f1f1f1; 25 | } 26 | ::-webkit-scrollbar-thumb { 27 | background: #888; 28 | } 29 | ::-webkit-scrollbar-thumb:hover { 30 | background: #555; 31 | } 32 | /* end of scroll bar */ 33 | .section { 34 | width: 100vw; 35 | height: 100vh; 36 | background-color: #f6f5f7; 37 | } 38 | .heading { 39 | text-align: center; 40 | margin: 4rem 0; 41 | font-size: 3rem; 42 | font-weight: 300; 43 | } 44 | .form { 45 | width: 100%; 46 | padding: 15px; 47 | display: flex; 48 | flex-direction: column; 49 | margin: auto; 50 | align-items: center; 51 | } 52 | .form input { 53 | margin-bottom: 1rem; 54 | width: 60vw; 55 | height: 5vh; 56 | border: 1px solid #ccc; 57 | border-radius: 5px; 58 | padding: 1rem; 59 | font-family: 'Montserrat' sans-serif; 60 | font-weight: 500; 61 | font-size: 1rem; 62 | outline: none; 63 | } 64 | .form input:focus { 65 | border: 1px solid #000; 66 | } 67 | .labelHidden { 68 | display: none; 69 | } 70 | .btn { 71 | padding: 1rem 3rem; 72 | border: none; 73 | border-radius: 5px; 74 | cursor: pointer; 75 | font-family: 'Montserrat', sans-serif; 76 | font-weight: 500; 77 | color: #fff; 78 | background-color: #000; 79 | margin-top: 1rem; 80 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 81 | transition: all 0.3s; 82 | } 83 | .btn:hover { 84 | background-color: #fff; 85 | color: #000; 86 | } 87 | .btnHidden { 88 | display: none; 89 | } 90 | .member { 91 | text-align: center; 92 | font-size: 1rem; 93 | font-weight: 600; 94 | color: #000; 95 | text-decoration: none; 96 | transition: all 0.3s; 97 | margin-bottom: 1rem; 98 | } 99 | .member a { 100 | text-decoration: none; 101 | transition: all 0.3s; 102 | } 103 | .member a:hover { 104 | color: #000; 105 | text-decoration: underline; 106 | } 107 | .loginSection { 108 | display: flex; 109 | flex-direction: column; 110 | align-items: center; 111 | } 112 | .alternatebtn { 113 | width: 17rem; 114 | padding: 1rem 3rem; 115 | border: none; 116 | border-radius: 20px; 117 | cursor: pointer; 118 | font-family: 'Montserrat', sans-serif; 119 | font-weight: 500; 120 | color: #fff; 121 | background-color: #000; 122 | margin-top: 1rem; 123 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 124 | transition: all 0.3s; 125 | } 126 | .alternatebtn:hover { 127 | background-color: #fff; 128 | color: #000; 129 | } 130 | .or { 131 | margin: 1rem 0; 132 | font-size: 1.2rem; 133 | font-weight: 500; 134 | color: #000; 135 | position: relative; 136 | } 137 | .loginform { 138 | margin-top: 0; 139 | } 140 | .loginLink { 141 | cursor: pointer; 142 | } 143 | .loginLinkDiv { 144 | margin-top: 10rem; 145 | display: flex; 146 | flex-direction: column; 147 | align-items: center; 148 | justify-content: flex-start; 149 | } 150 | 151 | .alertBox { 152 | font-family: 'Montserrat', sans-serif; 153 | font-weight: 100; 154 | position: fixed; 155 | top: 1.25rem; 156 | left: 1.25rem; 157 | width: 20%; 158 | padding: 1.2rem; 159 | color: #fff; 160 | font-size: 1rem; 161 | border-radius: 0.3125rem; 162 | display: none; 163 | z-index: 1000; 164 | transform: translate(10%, 30%); 165 | transition: transform 0.5s ease-in-out; 166 | } 167 | @media (max-width: 768px) { 168 | .alertBox { 169 | width: 80%; 170 | padding: 0.8rem; 171 | font-size: 0.9rem; 172 | border-radius: 0.25rem; 173 | top: 3rem; 174 | left: 50%; 175 | transform: translateX(-50%); 176 | } 177 | } 178 | .alert { 179 | display: block; 180 | background-color: crimson; 181 | color: #fff; 182 | text-align: center; 183 | padding: 0.8rem; 184 | border-radius: 0.25rem; 185 | margin-bottom: 0.5rem; 186 | } 187 | /* home / navbar */ 188 | /* .homeHidden { 189 | display: none; 190 | } 191 | .profilePageHidden { 192 | display: none; 193 | } 194 | .messagePageHidden { 195 | display: none; 196 | } */ 197 | .homeContainer { 198 | width: 100vw; 199 | height: 100vh; 200 | background-color: #f6f5f7; 201 | display: flex; 202 | flex-direction: row; 203 | margin: 0 10rem; 204 | position: relative; 205 | } 206 | @media screen and (max-width: 768px) { 207 | .homeContainer { 208 | margin: 0; 209 | } 210 | } 211 | .navbarContainer { 212 | width: 30rem; 213 | height: 100vh; 214 | background-color: #fff; 215 | display: flex; 216 | flex-direction: column; 217 | justify-content: flex-start; 218 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 219 | } 220 | .navbar { 221 | background-color: #fff; 222 | display: flex; 223 | } 224 | .navBtn { 225 | width: auto; 226 | box-shadow: none; 227 | border: none; 228 | margin-right: 0.5rem; 229 | } 230 | .listItem { 231 | width: auto; 232 | margin: 1rem 0; 233 | padding: 1rem 2rem; 234 | border-radius: 20px; 235 | font-family: 'Montserrat', sans-serif; 236 | font-weight: 500; 237 | font-size: 1rem; 238 | cursor: pointer; 239 | transition: all 0.3s; 240 | } 241 | .listItem:hover { 242 | background-color: #000; 243 | color: #fff; 244 | } 245 | .navLink { 246 | text-decoration: none; 247 | color: #000; 248 | transition: all 0.3s; 249 | } 250 | .navLinkActive { 251 | color: #8a2be2; 252 | } 253 | .hover-color { 254 | color: #fff; 255 | } 256 | .profileNavbar { 257 | width: 100%; 258 | margin: auto; 259 | margin-bottom: 1rem; 260 | display: flex; 261 | flex-direction: row; 262 | align-items: center; 263 | justify-content: space-evenly; 264 | border-radius: 15px; 265 | cursor: pointer; 266 | font-family: 'Montserrat', sans-serif; 267 | font-weight: 300; 268 | } 269 | .profileNavbar:hover { 270 | background-color: #ccc; 271 | } 272 | .profilePic { 273 | margin-left: 0.2rem; 274 | width: 2rem; 275 | height: 2rem; 276 | border-radius: 50%; 277 | transition: all 0.3s; 278 | } 279 | .profilePic:hover { 280 | opacity: 0.8; 281 | transform: scale(1.1); 282 | } 283 | .name { 284 | margin-left: 1rem; 285 | font-size: 1rem; 286 | font-weight: 500; 287 | margin-right: 0.2rem; 288 | } 289 | .username { 290 | font-size: 0.8rem; 291 | font-weight: 300; 292 | } 293 | .fa-chevron-down { 294 | font-size: 0.8rem; 295 | font-weight: 300; 296 | color: #000; 297 | transition: all 0.3s; 298 | margin-right: 0.2rem; 299 | } 300 | /* end of navbar */ 301 | /* home / tweets */ 302 | .targetHeading { 303 | margin: 1rem; 304 | font-size: 1.5rem; 305 | font-weight: 500; 306 | } 307 | .followingButton { 308 | width: 100%; 309 | padding: 1rem 2rem; 310 | border: none; 311 | font-weight: 500; 312 | font-size: 1rem; 313 | border-radius: 5px; 314 | margin-bottom: 1rem; 315 | transition: all 0.3s; 316 | cursor: default; 317 | } 318 | .followingButton:hover { 319 | background-color: #ccc; 320 | } 321 | .tweets { 322 | display: flex; 323 | flex-direction: column; 324 | height: auto; 325 | position: relative; 326 | } 327 | .tweet { 328 | width: 100%; 329 | padding: 1rem; 330 | border-bottom: 1px solid #ccc; 331 | cursor: pointer; 332 | transition: all 0.3s; 333 | } 334 | .tweet:hover { 335 | background-color: #ccc; 336 | } 337 | .newTweet { 338 | background-color: bisque; 339 | } 340 | .tweet:hover .tweetComments { 341 | background-color: #f6f5f7; 342 | } 343 | .tweetLoading { 344 | border-color: #000; 345 | } 346 | /* end of to do */ 347 | 348 | .tweetCommentsHidden { 349 | display: none; 350 | } 351 | .tweetComments { 352 | display: block; 353 | width: 90%; 354 | margin: 0 auto; 355 | } 356 | 357 | .tweetProfilePic { 358 | width: 2rem; 359 | height: 2rem; 360 | border-radius: 50%; 361 | margin-right: 1rem; 362 | transition: all 0.3s; 363 | } 364 | .tweetProfilePic:hover { 365 | opacity: 0.8; 366 | transform: scale(1.1); 367 | } 368 | .profileName { 369 | display: flex; 370 | flex-direction: row; 371 | align-items: center; 372 | margin-bottom: 0.5rem; 373 | } 374 | .tweetName { 375 | font-size: 1.2rem; 376 | font-weight: 500; 377 | margin-right: 0.5rem; 378 | } 379 | .tweetTime { 380 | margin-left: auto; 381 | } 382 | .tweetText { 383 | font-size: 1rem; 384 | font-weight: 300; 385 | margin-bottom: 0.5rem; 386 | } 387 | .tweetText img { 388 | width: 100%; 389 | height: 20rem; 390 | object-fit: cover; 391 | border-radius: 10px; 392 | margin-bottom: 0.5rem; 393 | } 394 | .tweetChoices { 395 | display: flex; 396 | flex-direction: row; 397 | justify-content: center; 398 | } 399 | .tweetChoice { 400 | margin-right: 1rem; 401 | padding: 0.5rem; 402 | transition: all 0.3s; 403 | cursor: pointer; 404 | } 405 | .tweetChoiceActive { 406 | color: #8a2be2; 407 | transition: all 0.3s; 408 | transform: scale(1.1); 409 | } 410 | .tweetChoice:hover { 411 | color: #fff; 412 | transform: scale(1.1); 413 | } 414 | .postTweetHidden { 415 | display: none; 416 | } 417 | .postTweet { 418 | flex-direction: column; 419 | justify-content: flex-start; 420 | align-items: center; 421 | position: absolute; 422 | width: 50vw; 423 | height: 50vh; 424 | background-color: #fff; 425 | display: flex; 426 | top: 50%; 427 | left: 10%; 428 | z-index: 10000; 429 | transform: translateY(-50%); 430 | border-radius: 10px; 431 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 432 | } 433 | .postTweetX { 434 | font-weight: 500; 435 | font-size: 1.2rem; 436 | margin: 1rem; 437 | margin-right: auto; 438 | cursor: pointer; 439 | } 440 | .postTweetInput { 441 | width: 90%; 442 | height: auto; 443 | padding: 0.8rem; 444 | font-size: 1rem; 445 | font-weight: 300; 446 | border: 1px solid #ccc; 447 | border-radius: 10px; 448 | outline: none; 449 | margin: 1rem; 450 | margin-bottom: 0; 451 | border: none; 452 | overflow-wrap: break-word; 453 | word-wrap: break-word; 454 | resize: none; 455 | } 456 | .postTweetButton { 457 | width: 50%; 458 | } 459 | /* end of tweets */ 460 | /* home / search */ 461 | .search { 462 | background-color: #fff; 463 | display: flex; 464 | flex-direction: column; 465 | justify-content: flex-start; 466 | } 467 | .searchBarInput { 468 | width: 45%; /* to do */ 469 | padding: 0.8rem; 470 | font-size: 1rem; 471 | font-weight: 300; 472 | border: 1px solid #ccc; 473 | border-radius: 10px; 474 | outline: none; 475 | margin: 1rem; 476 | margin-bottom: 0; 477 | margin-right: auto; /* to do */ 478 | } 479 | .searchBarInput:focus { 480 | border: 1px solid #000; 481 | } 482 | .searchResults { 483 | width: 45%; 484 | display: flex; 485 | flex-direction: column; 486 | justify-content: flex-start; 487 | height: auto; 488 | margin: 1rem; 489 | margin-top: 0; 490 | } 491 | .searchResult { 492 | padding: 0.5rem; 493 | display: flex; 494 | flex-direction: row; 495 | align-items: center; 496 | cursor: pointer; 497 | transition: all 0.3s; 498 | border-bottom: 1px solid #ccc; 499 | border-radius: 5px; 500 | } 501 | .searchResult:hover { 502 | background-color: #ccc; 503 | } 504 | .searchImg { 505 | margin-right: 0rem; 506 | width: 2rem; 507 | height: 2rem; 508 | border-radius: 50%; 509 | } 510 | .searchName { 511 | margin-right: 0.5rem; 512 | margin-left: 1rem; 513 | font-size: 1rem; 514 | font-weight: 500; 515 | } 516 | .searchUsername { 517 | margin-right: 0.5rem; 518 | font-size: 0.8rem; 519 | font-weight: 300; 520 | } 521 | 522 | .whoToFollowHeading { 523 | margin: 1rem; 524 | margin-top: 0.5rem; 525 | font-size: 1.2rem; 526 | font-weight: 500; 527 | } 528 | .whoToFollow { 529 | display: flex; 530 | flex-direction: column; 531 | justify-content: flex-start; 532 | margin: 1rem; 533 | background-color: #f6f5f7; 534 | margin-right: auto; /* to do */ 535 | padding: 1rem; 536 | border-radius: 10px; 537 | max-width: 70%; 538 | } 539 | .suggestionAccount { 540 | display: flex; 541 | flex-direction: row; 542 | align-items: center; 543 | margin-bottom: 1rem; 544 | cursor: pointer; 545 | transition: all 0.3s; 546 | border-bottom: 1px solid #ccc; 547 | border-radius: 5px; 548 | } 549 | .suggestionAccount:hover { 550 | transform: translateY(-5px); 551 | } 552 | .suggestionImg { 553 | margin-right: 0rem; 554 | width: 2rem; 555 | height: 2rem; 556 | border-radius: 50%; 557 | } 558 | .suggestionName { 559 | margin-right: 0.5rem; 560 | margin-left: 1rem; 561 | font-size: 1rem; 562 | font-weight: 500; 563 | } 564 | .suggestionUsername { 565 | margin-right: 0.5rem; 566 | font-size: 0.8rem; 567 | font-weight: 300; 568 | } 569 | .followButton { 570 | padding: 0.5rem 1rem; 571 | border: none; 572 | font-weight: 500; 573 | font-size: 0.5rem; 574 | border-radius: 5px; 575 | cursor: pointer; 576 | transition: all 0.3s; 577 | } 578 | .followButton:hover { 579 | background-color: #ccc; 580 | } 581 | /* end of search */ 582 | /* profile page */ 583 | .profileHeader { 584 | border-bottom: 1px solid #ccc; 585 | position: relative; 586 | } 587 | .backColumn { 588 | /* margin: 1rem; */ 589 | margin-left: 1rem; 590 | display: flex; 591 | flex-direction: row; 592 | align-items: center; 593 | justify-content: flex-start; 594 | /* position: fixed; */ /* to do */ 595 | } 596 | .fa-arrow-left { 597 | cursor: pointer; 598 | transition: all 0.3s; 599 | } 600 | .profileBannerPic { 601 | width: 100%; 602 | height: 10rem; 603 | object-fit: cover; 604 | border-radius: 10px; 605 | margin-bottom: 0.5rem; 606 | } 607 | .imgAndEdit { 608 | display: flex; 609 | flex-direction: row; 610 | align-items: flex-start; 611 | justify-content: flex-end; 612 | /* position: relative; */ 613 | } 614 | .mainProfilePic { 615 | width: 8rem; 616 | height: 8rem; 617 | border-radius: 50%; 618 | margin-right: 1rem; 619 | transition: all 0.3s; 620 | cursor: pointer; 621 | position: absolute; 622 | top: 27%; 623 | left: 7%; /*img position */ 624 | border: 5px solid #fff; 625 | z-index: 1000; 626 | } 627 | .mainProfilePic:hover { 628 | transform: scale(1.1); 629 | } 630 | .editProfileButton { 631 | margin-right: 3rem; 632 | padding: 0.5rem 1.5rem; 633 | } 634 | .followUnfollowButton { 635 | margin-right: 3rem; 636 | padding: 0.5rem 1.5rem; 637 | } 638 | .directMessageButton { 639 | /* to do.. check */ 640 | margin-right: 3rem; 641 | padding: 0.5rem 1.5rem; 642 | } 643 | .mainProfileNameDiv { 644 | display: flex; 645 | flex-direction: column; 646 | margin: 0.5rem; 647 | } 648 | .bio { 649 | margin: 0.5rem 0; 650 | font-size: 1rem; 651 | width: auto; 652 | } 653 | .profileCount { 654 | display: flex; 655 | flex-direction: row; 656 | justify-content: flex-start; 657 | margin: 0.5rem; 658 | margin-top: 0; 659 | margin-bottom: 1rem; 660 | } 661 | .countItem { 662 | display: flex; 663 | flex-direction: row; 664 | align-items: center; 665 | justify-content: space-around; 666 | margin-right: 1rem; 667 | font-size: 0.8rem; 668 | } 669 | .countNumber { 670 | font-size: 1rem; 671 | font-weight: 500; 672 | margin-right: 0.2rem; 673 | } 674 | .profileNavigationButtons { 675 | display: flex; 676 | flex-direction: row; 677 | justify-content: flex-start; 678 | align-items: center; 679 | margin-top: 0.5rem; 680 | margin-bottom: 0rem; 681 | } 682 | .profileNavButton { 683 | margin-bottom: 0; 684 | font-size: 0.8rem; 685 | cursor: pointer; 686 | } 687 | .profileNavButtonActive { 688 | background-color: #ccc; 689 | } 690 | /* edit profile */ 691 | .editProfile { 692 | display: flex; 693 | flex-direction: column; 694 | justify-content: flex-start; 695 | align-items: center; 696 | position: absolute; 697 | width: 50vw; 698 | height: 80vh; 699 | background-color: #fff; 700 | display: flex; 701 | top: 50%; 702 | left: 10%; 703 | z-index: 10000; 704 | transform: translateY(-50%); 705 | border-radius: 10px; 706 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 707 | } 708 | .editProfileHeading { 709 | margin: 1rem; 710 | font-size: 1.5rem; 711 | font-weight: 500; 712 | } 713 | .editProfileForm { 714 | display: flex; 715 | flex-direction: column; 716 | justify-content: flex-start; 717 | align-items: center; 718 | } 719 | .editProfileButtons { 720 | display: flex; 721 | } 722 | .editProfileInput { 723 | width: 100%; 724 | padding: 1rem; 725 | font-size: 1rem; 726 | font-weight: 300; 727 | border: 1px solid #ccc; 728 | border-radius: 10px; 729 | margin: 0.5rem; 730 | overflow-wrap: break-word; 731 | word-wrap: break-word; 732 | } 733 | .editProfileFormBio { 734 | width: 100%; 735 | padding: 1rem; 736 | font-size: 1rem; 737 | font-weight: 300; 738 | border: 1px solid #ccc; 739 | border-radius: 10px; 740 | margin: 0.5rem; 741 | overflow-wrap: break-word; 742 | word-wrap: break-word; 743 | resize: none; 744 | } 745 | 746 | .editProfileCancelButton { 747 | margin-left: 1rem; 748 | } 749 | /* end of edit profile */ 750 | /* end of profile page */ 751 | /* messages page */ 752 | .messagesView { 753 | display: flex; 754 | flex-direction: column; 755 | justify-content: flex-start; 756 | height: auto; 757 | } 758 | .messagesViewHidden { 759 | display: none; 760 | } 761 | .messageProfile { 762 | display: flex; 763 | flex-direction: row; 764 | align-items: center; 765 | cursor: pointer; 766 | transition: all 0.3s; 767 | border-bottom: 1px solid #ccc; 768 | border-radius: 5px; 769 | width: 100%; 770 | } 771 | .messageProfile:hover { 772 | background-color: #ccc; 773 | } 774 | .messageViewProfilePictureContainer { 775 | width: 4rem; 776 | height: 4rem; 777 | border-radius: 50%; 778 | margin: 1rem; 779 | overflow: hidden; 780 | display: flex; 781 | justify-content: center; 782 | align-items: center; 783 | } 784 | .messageViewProfilePicture { 785 | max-width: 100%; 786 | max-height: 100%; 787 | width: auto; 788 | height: auto; 789 | object-fit: contain; 790 | } 791 | .messageViewHeader { 792 | display: flex; 793 | flex-direction: row; 794 | align-items: center; 795 | justify-content: space-between; 796 | margin-bottom: 0.5rem; 797 | width: 100%; 798 | } 799 | .messageViewInfo { 800 | display: flex; 801 | flex-direction: column; 802 | align-items: flex-start; 803 | width: 100%; 804 | } 805 | .messageViewName { 806 | font-size: 1rem; 807 | font-weight: 500; 808 | margin-right: 0.5rem; 809 | } 810 | .messageViewUsername { 811 | font-size: 0.8rem; 812 | font-weight: 300; 813 | } 814 | .messageViewDate { 815 | font-size: 0.8rem; 816 | font-weight: 300; 817 | margin-left: auto; 818 | margin-right: 1rem; 819 | } 820 | .lastMessage { 821 | font-size: 0.8rem; 822 | font-weight: 300; 823 | } 824 | /* */ 825 | 826 | .messagesDetailsHidden { 827 | display: none; 828 | } 829 | .messageDetails { 830 | display: flex; 831 | flex-direction: column; 832 | justify-content: flex-start; 833 | height: 38rem; /* height of message details */ 834 | width: 100%; 835 | } 836 | .messageDetailsHidden { 837 | display: none; 838 | } 839 | .messageDetailsHeader { 840 | display: flex; 841 | flex-direction: row; 842 | align-items: center; 843 | justify-content: space-between; 844 | border-bottom: 1px solid #ccc; 845 | position: fixed; 846 | width: 41.5rem; /*fixed width*/ 847 | } 848 | @media screen and (max-width: 1200px) { 849 | .messageDetailsHeader { 850 | border-bottom: none; 851 | } 852 | } 853 | .messageDetailsInfo { 854 | display: flex; 855 | flex-direction: column; 856 | align-items: flex-start; 857 | width: 100%; 858 | } 859 | .messageDetailsName { 860 | font-size: 1rem; 861 | font-weight: 500; 862 | margin-right: 0.5rem; 863 | } 864 | .messageDetailsUsername { 865 | font-size: 0.8rem; 866 | font-weight: 300; 867 | } 868 | .messageDetailsProfilePictureContainer { 869 | width: 4rem; 870 | height: 4rem; 871 | border-radius: 50%; 872 | margin: 1rem; 873 | overflow: hidden; 874 | display: flex; 875 | justify-content: center; 876 | align-items: center; 877 | } 878 | .messageDetailsProfilePicture { 879 | max-width: 100%; 880 | max-height: 100%; 881 | width: auto; 882 | height: auto; 883 | object-fit: contain; 884 | cursor: pointer; 885 | } 886 | .messageDetailsMessages { 887 | display: flex; 888 | flex-direction: column; 889 | justify-content: flex-start; 890 | padding: 1rem; 891 | margin-top: 7rem; 892 | height: auto; 893 | } 894 | .messageDetailsMessage { 895 | display: flex; 896 | flex-direction: column; 897 | justify-content: flex-start; 898 | align-items: flex-start; 899 | margin-bottom: 0.5rem; 900 | width: 100%; 901 | flex-shrink: 0; 902 | } 903 | .messageDetailsMessageText { 904 | font-size: 0.8rem; 905 | font-weight: 300; 906 | margin-bottom: 0.5rem; 907 | border-radius: 5px; 908 | padding: 0.5rem; 909 | background-color: #e0e0e0; 910 | /* background-color: #8a2be2; */ 911 | width: 70%; 912 | word-wrap: break-word; 913 | overflow-wrap: break-word; 914 | word-break: break-all; 915 | overflow: hidden; 916 | } 917 | .messageDetailsMessageTextSent { 918 | margin-left: auto; 919 | background-color: #8a2be2; 920 | color: #fff; 921 | } 922 | .messageDetailsMessageTime { 923 | font-size: 0.6rem; 924 | font-weight: 300; 925 | /* margin-right: auto; */ 926 | /* margin-left: auto; */ 927 | } 928 | .messageDetailsMessageTimeReceived { 929 | margin-right: auto; 930 | } 931 | .messageDetailsMessageTimeSent { 932 | margin-left: auto; 933 | } 934 | .messageDetailsInput { 935 | height: 100%; 936 | display: flex; 937 | flex-direction: row; 938 | align-items: center; 939 | justify-content: center; 940 | margin-top: auto; 941 | } 942 | .messageDetailsInputHidden { 943 | display: none; 944 | } 945 | .messageDetailsInputText { 946 | width: 90%; 947 | padding: 0.8rem; 948 | font-size: 1rem; 949 | font-weight: 300; 950 | border-radius: 10px; 951 | outline: none; 952 | margin: 0.5rem; 953 | margin-bottom: 0; 954 | border: none; 955 | resize: none; 956 | background-color: #e0e0e0; 957 | transition: all 0.3s; 958 | } 959 | .messageDetailsInputText:focus { 960 | border: 1px solid #000; 961 | } 962 | .messageDetailsInputButton { 963 | margin-right: 1rem; 964 | border: none; 965 | padding: 0.5rem 1.5rem; 966 | font-weight: 500; 967 | font-size: 0.8rem; 968 | border-radius: 5px; 969 | cursor: pointer; 970 | transition: all 0.3s; 971 | box-shadow: none; 972 | } 973 | /* end of messages page */ 974 | /* lost page */ 975 | .lostContainer { 976 | display: flex; 977 | flex-direction: column; 978 | align-items: center; 979 | } 980 | .lostParagraph { 981 | font-size: 1.5rem; 982 | font-weight: 500; 983 | margin: 1rem; 984 | } 985 | .lostLink { 986 | font-weight: 600; 987 | color: #8a2be2; 988 | cursor: pointer; 989 | transition: all 0.3s; 990 | } 991 | .lostLink:hover { 992 | color: #000; 993 | } 994 | /* end of lost page */ 995 | /* settings page */ 996 | .passwordDivHidden { 997 | display: none; 998 | } 999 | .passwordDiv { 1000 | display: flex; 1001 | flex-direction: column; 1002 | align-items: center; 1003 | width: auto; 1004 | margin: 1rem; 1005 | } 1006 | .passwordDiv input { 1007 | margin-bottom: 1rem; 1008 | height: 5vh; 1009 | border: 1px solid #ccc; 1010 | border-radius: 5px; 1011 | padding: 1rem; 1012 | font-family: 'Montserrat' sans-serif; 1013 | font-weight: 500; 1014 | font-size: 1rem; 1015 | outline: none; 1016 | } 1017 | .passwordDiv input:focus { 1018 | border: 1px solid #000; 1019 | } 1020 | .passwordMessage { 1021 | margin-top: 1rem; 1022 | font-size: 1rem; 1023 | font-weight: 300; 1024 | color: #8a2be2; 1025 | } 1026 | /* end of settings page */ 1027 | -------------------------------------------------------------------------------- /app.mjs: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import path from 'path' 3 | import { MongoClient, ObjectId, ServerApiVersion } from 'mongodb' 4 | import { fileURLToPath } from 'url' 5 | import { createServer } from 'http' 6 | // import cors from 'cors' 7 | import { Server } from 'socket.io' 8 | import jwt from 'jsonwebtoken' 9 | // import fs from 'fs' 10 | import mongoDBCredentials from './passwords.js' // import mongodb credentials from passwords.js (not included in repo for security reasons) 11 | const app = express() 12 | const httpServer = createServer(app) 13 | const io = new Server(httpServer, { 14 | cors: { 15 | origin: '*', 16 | methods: ['GET', 'POST'], 17 | }, 18 | }) // WebSocket server alongside the regular Express server 19 | 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 21 | app.use(express.static(path.join(__dirname, 'public'))) 22 | // app.use(cors()) 23 | // mongodb 24 | const uri = mongoDBCredentials 25 | let mongoDBConnection = false 26 | const client = new MongoClient(uri, { 27 | serverApi: { 28 | version: ServerApiVersion.v1, 29 | strict: true, 30 | deprecationErrors: true, 31 | }, 32 | }) 33 | async function run() { 34 | try { 35 | // Connect the client to the server (optional starting in v4.7) 36 | await client.connect() 37 | console.log('Connected to MongoDB') 38 | mongoDBConnection = true 39 | } catch (err) { 40 | console.log('Failed to connect to MongoDB' + err) 41 | mongoDBConnection = false 42 | } 43 | } 44 | const db = client.db('x') 45 | const collection = db.collection('users') 46 | const tweetCollection = db.collection('tweets') 47 | const messageCollection = db.collection('messages') 48 | run().catch(console.dir) 49 | //end of mongodb 50 | 51 | // mongodb functions 52 | const findUser = async (username) => { 53 | const result = await collection.findOne({ username: username }) 54 | return result 55 | } 56 | const findEmail = async (email) => { 57 | const result = await collection.findOne({ email: email }) 58 | return result 59 | } 60 | // end of mongodb functions 61 | // middlewares 62 | app.use(express.json()) 63 | const verifyToken = (req, res, next) => { 64 | const bearerHeader = req.headers['authorization'] 65 | if (!bearerHeader) { 66 | res.status(403).send('Forbidden') 67 | return 68 | } 69 | const [scheme, token] = bearerHeader.split(' ') 70 | if (scheme !== 'Bearer' || !token) { 71 | res 72 | .status(401) 73 | .json({ status: false, message: 'Access denied. Invalid token.' }) 74 | return 75 | } 76 | try { 77 | const decoded = jwt.verify(token, 'someSecretKey') 78 | req.userId = decoded.id 79 | next() 80 | } catch (error) { 81 | res 82 | .status(401) 83 | .json({ status: false, message: 'Access denied. Invalid token.' }) 84 | } 85 | } 86 | // end of middlewares 87 | //------------------------ 88 | app.get( 89 | '/profile/:username', 90 | /*verifyToken,*/ (req, res) => { 91 | res.sendFile(path.join(__dirname, 'public', 'home.html')) 92 | } 93 | ) 94 | app.get('/', (req, res) => { 95 | res.sendFile(path.join(__dirname, 'index.html')) 96 | }) 97 | app.get('/signup', (req, res) => { 98 | res.sendFile(path.join(__dirname, 'index.html')) 99 | }) 100 | app.get('/login', (req, res) => { 101 | res.sendFile(path.join(__dirname, 'public', 'login.html')) 102 | }) 103 | app.get('/settings', (req, res) => { 104 | res.sendFile(path.join(__dirname, 'public', 'home.html')) 105 | }) 106 | app.post('/signup', async (req, res) => { 107 | const fullName = req.body.fullName 108 | const email = req.body.email 109 | const password = req.body.password 110 | const username = req.body.username 111 | const findUserx = await findUser(username) 112 | const findEmailx = await findEmail(email) 113 | if (findUserx) { 114 | res.status(400).json({ status: false, message: 'User already exists' }) 115 | return 116 | } 117 | if (findEmailx) { 118 | res.status(400).json({ status: false, message: 'Email already exists' }) 119 | return 120 | } 121 | if (password.length < 8) { 122 | res.status(400).json({ 123 | status: false, 124 | message: 'Password must be at least 8 characters', 125 | }) 126 | return 127 | } 128 | const specialCharacters = '/*&^%$# \\' 129 | for (let i = 0; i < specialCharacters.length; i++) { 130 | if (username.includes(specialCharacters[i])) { 131 | res.status(400).json({ 132 | status: false, 133 | message: 'Username cannot contain special characters or whitespace', 134 | }) 135 | return 136 | } 137 | } 138 | const user = { 139 | fullName: fullName, 140 | email: email, 141 | password: password, 142 | username: username, 143 | phoneNumber: 'null', 144 | following: [], 145 | followers: [], 146 | tweetCount: 0, 147 | bio: '', 148 | profilePicture: 149 | 'https://i.pinimg.com/474x/65/25/a0/6525a08f1df98a2e3a545fe2ace4be47.jpg', 150 | coverPicture: 'https://i.imgflip.com/5an5fg.jpg?a469824', 151 | likedTweets: [], 152 | retweetedTweets: [], 153 | savedTweets: [], 154 | createdAt: String(new Date()), 155 | createdAtISO: new Date().toISOString(), 156 | lastLogin: String(new Date()), 157 | verified: false, 158 | } 159 | await collection.insertOne(user, (err, result) => { 160 | if (err) { 161 | res 162 | .status(400) 163 | .json({ status: false, message: 'Failed to add user to database' }) 164 | throw err 165 | } 166 | }) 167 | res.status(200).json({ status: true, message: 'Signed Up Successfully' }) 168 | }) 169 | app.post('/login', async (req, res) => { 170 | const usernameOremailOrphone = req.body.usernameOremailOrphone 171 | const password = req.body.password 172 | const user = await collection.findOne( 173 | { 174 | $or: [ 175 | { username: usernameOremailOrphone, password: password }, 176 | { email: usernameOremailOrphone, password: password }, 177 | { phoneNumber: usernameOremailOrphone, password: password }, 178 | ], 179 | }, 180 | (err, result) => { 181 | if (err) { 182 | res.status(400).json({ status: false, message: 'Invalid Credentials' }) 183 | throw err 184 | } 185 | return result 186 | } 187 | ) 188 | if (!user) { 189 | res.status(400).json({ status: false, message: 'Invalid Credentials' }) 190 | return 191 | } 192 | const token = jwt.sign({ id: user._id }, 'someSecretKey') 193 | res 194 | .status(200) 195 | .json({ status: true, message: 'Login Successful', token: token }) 196 | }) 197 | 198 | app.get('/home', (req, res) => { 199 | res.sendFile(path.join(__dirname, 'public', 'home.html')) 200 | }) 201 | app.get('/messages', (req, res) => { 202 | res.sendFile(path.join(__dirname, 'public', 'home.html')) 203 | }) 204 | app.get( 205 | '/messages/:username', 206 | /*verifyToken,*/ (req, res) => { 207 | res.sendFile(path.join(__dirname, 'public', 'home.html')) 208 | } 209 | ) 210 | app.get('/getTweets', verifyToken, async (req, res) => { 211 | try { 212 | const userId = req.userId // from verifyToken middleware 213 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 214 | if (!user) { 215 | res.status(400).json({ status: false, message: 'User not found' }) 216 | return 217 | } 218 | const following = user.following 219 | const tweets = await tweetCollection 220 | 221 | .aggregate([ 222 | { 223 | $match: { 224 | $or: [ 225 | { createdBy: { $in: following }, type: 'main' }, // Tweets created by users in following array 226 | { createdBy: userId, type: 'main' }, // Tweets created by the same user 227 | ], 228 | }, 229 | }, 230 | { 231 | $addFields: { 232 | datetime: { $toDate: '$createdAtISO' }, 233 | }, 234 | }, 235 | { 236 | $sort: { datetime: -1 }, 237 | }, 238 | { 239 | $limit: 100, 240 | }, 241 | { 242 | $lookup: { 243 | from: 'users', 244 | localField: 'createdByObjectId', 245 | foreignField: '_id', 246 | as: 'userDetails', 247 | }, 248 | }, 249 | { 250 | $unwind: '$userDetails', 251 | }, 252 | { 253 | $project: { 254 | _id: 1, 255 | createdBy: 1, 256 | content: 1, 257 | createdAt: 1, 258 | createdAtISO: 1, 259 | type: 1, 260 | commentTo: 1, 261 | likes: 1, 262 | retweets: 1, 263 | comments: 1, 264 | userDetails: { 265 | fullName: 1, 266 | username: 1, 267 | profilePicture: 1, 268 | _id: 1, 269 | }, 270 | }, 271 | }, 272 | ]) 273 | .toArray() 274 | // get 3 random users 275 | const randomUsers = await collection 276 | .aggregate([ 277 | { 278 | $match: { 279 | _id: { $nin: following }, 280 | }, 281 | }, 282 | { 283 | $sample: { size: 3 }, 284 | }, 285 | { 286 | $project: { 287 | _id: 1, 288 | fullName: 1, 289 | username: 1, 290 | profilePicture: 1, 291 | }, 292 | }, 293 | ]) 294 | .toArray() 295 | 296 | res.status(200).json({ 297 | status: true, 298 | tweets: tweets, 299 | profile: { 300 | fullName: user.fullName, 301 | username: user.username, 302 | profilePicture: user.profilePicture, 303 | _id: user._id, 304 | }, 305 | randomUsers: randomUsers, 306 | }) 307 | } catch (err) { 308 | res 309 | .status(400) 310 | .json({ status: false, message: err + ' Failed to get tweets' }) 311 | return 312 | } 313 | }) 314 | app.get('/getTweetComments/:tweetId', verifyToken, async (req, res) => { 315 | try { 316 | const tweetId = req.params.tweetId 317 | const tweet = await tweetCollection.findOne({ _id: new ObjectId(tweetId) }) 318 | if (!tweet) { 319 | res.status(400).json({ status: false, message: 'Tweet not found' }) 320 | return 321 | } 322 | const comments = await tweetCollection 323 | .aggregate([ 324 | { 325 | $match: { 326 | commentTo: tweetId, 327 | type: 'comment', 328 | }, 329 | }, 330 | { 331 | $addFields: { 332 | datetime: { $toDate: '$createdAtISO' }, 333 | }, 334 | }, 335 | { 336 | $sort: { datetime: -1 }, 337 | }, 338 | { 339 | $limit: 20, 340 | }, 341 | { 342 | $lookup: { 343 | from: 'users', 344 | localField: 'createdByObjectId', 345 | foreignField: '_id', 346 | as: 'userDetails', 347 | }, 348 | }, 349 | { 350 | $unwind: '$userDetails', 351 | }, 352 | { 353 | $project: { 354 | _id: 1, 355 | createdBy: 1, 356 | content: 1, 357 | createdAt: 1, 358 | createdAtISO: 1, 359 | type: 1, 360 | commentTo: 1, 361 | likes: 1, 362 | retweets: 1, 363 | comments: 1, 364 | userDetails: { 365 | fullName: 1, 366 | username: 1, 367 | profilePicture: 1, 368 | _id: 1, 369 | }, 370 | }, 371 | }, 372 | ]) 373 | .toArray() 374 | res.status(200).json({ status: true, tweets: comments }) 375 | } catch (err) { 376 | res 377 | .status(400) 378 | .json({ status: false, message: err + ' Failed to get comments' }) 379 | return 380 | } 381 | }) 382 | app.get('/getSpecificMesssages/:username', verifyToken, async (req, res) => { 383 | try { 384 | const userId = req.userId 385 | const username = req.params.username 386 | const user = await collection.findOne({ username: username }) 387 | const currentUser = await collection.findOne({ 388 | _id: new ObjectId(userId), 389 | }) 390 | const stringUserId = String(user._id) 391 | if (!user) { 392 | res.status(400).json({ status: false, message: 'User not found' }) 393 | return 394 | } 395 | const messages = await messageCollection 396 | .aggregate([ 397 | { 398 | $match: { 399 | $or: [ 400 | { to: userId, from: stringUserId }, 401 | { to: stringUserId, from: userId }, 402 | ], 403 | }, 404 | }, 405 | { 406 | $addFields: { 407 | datetime: { $toDate: '$createdAtISO' }, 408 | }, 409 | }, 410 | { 411 | $sort: { datetime: -1 }, 412 | }, 413 | // { 414 | // $limit: 20, 415 | // }, 416 | // { 417 | // $addFields: { 418 | // idObject: { 419 | // $cond: [{ $eq: ['$to', userId] }, '$from', '$to'], // if to is equal to userId, then return from, else return to 420 | // }, 421 | // }, 422 | // }, 423 | // { 424 | // $lookup: { 425 | // from: 'users', 426 | // localField: 'idObject', 427 | // foreignField: '_id', 428 | // as: 'userDetails', 429 | // }, 430 | // }, 431 | // { 432 | // $unwind: '$userDetails', 433 | // }, 434 | { 435 | $project: { 436 | _id: 1, 437 | to: 1, 438 | from: 1, 439 | content: 1, 440 | createdAt: 1, 441 | createdAtISO: 1, 442 | }, 443 | }, 444 | ]) 445 | .toArray() 446 | res.status(200).json({ 447 | status: true, 448 | messages: messages, 449 | profile: { 450 | fullName: currentUser.fullName, 451 | username: currentUser.username, 452 | profilePicture: currentUser.profilePicture, 453 | _id: currentUser._id, 454 | }, 455 | targetedProfile: { 456 | fullName: user.fullName, 457 | username: user.username, 458 | profilePicture: user.profilePicture, 459 | _id: user._id, 460 | }, 461 | }) 462 | } catch (err) { 463 | res 464 | .status(400) 465 | .json({ status: false, message: err + ' Failed to get messages' }) 466 | console.log(err) 467 | } 468 | }) 469 | app.get('/getProfile/:username', verifyToken, async (req, res) => { 470 | const userId = req.userId 471 | const username = req.params.username 472 | const currentUser = await collection.findOne({ 473 | _id: new ObjectId(userId), 474 | }) 475 | const user = await collection.findOne({ username: username }) 476 | if (!user) { 477 | res.status(400).json({ status: false, message: 'User not found' }) 478 | return 479 | } 480 | const tweets = await tweetCollection 481 | .aggregate([ 482 | { 483 | $match: { 484 | $or: [ 485 | { createdByObjectId: user._id, type: 'main' }, 486 | { retweets: String(user._id) }, 487 | ], 488 | }, 489 | }, 490 | { 491 | $addFields: { 492 | datetime: { $toDate: '$createdAtISO' }, 493 | }, 494 | }, 495 | { 496 | $sort: { datetime: -1 }, 497 | }, 498 | { 499 | $limit: 20, 500 | }, 501 | { 502 | $lookup: { 503 | from: 'users', 504 | localField: 'createdByObjectId', 505 | foreignField: '_id', 506 | as: 'userDetails', 507 | }, 508 | }, 509 | { 510 | $unwind: '$userDetails', 511 | }, 512 | { 513 | $project: { 514 | _id: 1, 515 | createdBy: 1, 516 | content: 1, 517 | createdAt: 1, 518 | createdAtISO: 1, 519 | type: 1, 520 | commentTo: 1, 521 | likes: 1, 522 | retweets: 1, 523 | comments: 1, 524 | userDetails: { 525 | fullName: 1, 526 | username: 1, 527 | profilePicture: 1, 528 | _id: 1, 529 | }, 530 | }, 531 | }, 532 | ]) 533 | .toArray() 534 | const sameUser = userId == user._id 535 | let isFollowing = false 536 | if (!sameUser) { 537 | isFollowing = currentUser.following.includes(String(user._id)) 538 | // console.log(isFollowing) 539 | } 540 | // get 3 random users 541 | const following = currentUser.following 542 | const randomUsers = await collection 543 | .aggregate([ 544 | { 545 | $match: { 546 | _id: { $nin: following }, 547 | }, 548 | }, 549 | { 550 | $sample: { size: 3 }, 551 | }, 552 | { 553 | $project: { 554 | _id: 1, 555 | fullName: 1, 556 | username: 1, 557 | profilePicture: 1, 558 | }, 559 | }, 560 | ]) 561 | .toArray() 562 | res.status(200).json({ 563 | status: true, 564 | tweets: tweets, 565 | profile: { 566 | fullName: currentUser.fullName, 567 | username: currentUser.username, 568 | profilePicture: currentUser.profilePicture, 569 | _id: currentUser._id, 570 | }, 571 | targetedProfile: { 572 | fullName: user.fullName, 573 | username: user.username, 574 | profilePicture: user.profilePicture, 575 | profileBanner: user.coverPicture, 576 | bio: user.bio, 577 | followersCount: user.followers.length, 578 | followingCount: user.following.length, 579 | tweetCount: user.tweetCount, 580 | _id: user._id, 581 | sameUser: sameUser, 582 | isFollowing: isFollowing, 583 | }, 584 | randomUsers: randomUsers, 585 | }) 586 | }) 587 | app.get('/getProfileReplies/:username', verifyToken, async (req, res) => { 588 | // const userId = req.userId 589 | const username = req.params.username 590 | const user = await collection.findOne({ username: username }) 591 | if (!user) { 592 | res.status(400).json({ status: false, message: 'User not found' }) 593 | return 594 | } 595 | const tweets = await tweetCollection 596 | .aggregate([ 597 | { 598 | $match: { 599 | createdByObjectId: user._id, 600 | type: 'comment', 601 | }, 602 | }, 603 | { 604 | $addFields: { 605 | datetime: { $toDate: '$createdAtISO' }, 606 | }, 607 | }, 608 | { 609 | $sort: { datetime: -1 }, 610 | }, 611 | { 612 | $limit: 20, 613 | }, 614 | { 615 | $lookup: { 616 | from: 'users', 617 | localField: 'createdByObjectId', 618 | foreignField: '_id', 619 | as: 'userDetails', 620 | }, 621 | }, 622 | { 623 | $unwind: '$userDetails', 624 | }, 625 | { 626 | $project: { 627 | _id: 1, 628 | createdBy: 1, 629 | content: 1, 630 | createdAt: 1, 631 | createdAtISO: 1, 632 | type: 1, 633 | commentTo: 1, 634 | likes: 1, 635 | retweets: 1, 636 | comments: 1, 637 | userDetails: { 638 | fullName: 1, 639 | username: 1, 640 | profilePicture: 1, 641 | _id: 1, 642 | }, 643 | }, 644 | }, 645 | ]) 646 | .toArray() 647 | res.status(200).json({ 648 | status: true, 649 | tweets: tweets, 650 | }) 651 | }) 652 | app.get('/getProfileLikes/:username', verifyToken, async (req, res) => { 653 | // const userId = req.userId 654 | const username = req.params.username 655 | const user = await collection.findOne({ username: username }) 656 | if (!user) { 657 | res.status(400).json({ status: false, message: 'User not found' }) 658 | return 659 | } 660 | const tweets = await tweetCollection 661 | .aggregate([ 662 | { 663 | $match: { 664 | likes: String(user._id), 665 | }, 666 | }, 667 | { 668 | $addFields: { 669 | datetime: { $toDate: '$createdAtISO' }, 670 | }, 671 | }, 672 | { 673 | $sort: { datetime: -1 }, 674 | }, 675 | { 676 | $limit: 20, 677 | }, 678 | // { 679 | // $addFields: { 680 | // createdByObjectId: { $toObjectId: '$createdBy' }, 681 | // }, 682 | // }, 683 | { 684 | $lookup: { 685 | from: 'users', 686 | localField: 'createdByObjectId', 687 | foreignField: '_id', 688 | as: 'userDetails', 689 | }, 690 | }, 691 | { 692 | $unwind: '$userDetails', 693 | }, 694 | { 695 | $project: { 696 | _id: 1, 697 | createdBy: 1, 698 | content: 1, 699 | createdAt: 1, 700 | createdAtISO: 1, 701 | type: 1, 702 | commentTo: 1, 703 | likes: 1, 704 | retweets: 1, 705 | comments: 1, 706 | userDetails: { 707 | fullName: 1, 708 | username: 1, 709 | profilePicture: 1, 710 | _id: 1, 711 | }, 712 | }, 713 | }, 714 | ]) 715 | .toArray() 716 | res.status(200).json({ 717 | status: true, 718 | tweets: tweets, 719 | }) 720 | }) 721 | app.get('/getMessages', verifyToken, async (req, res) => { 722 | const userId = req.userId 723 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 724 | if (!user) { 725 | //not needed 726 | res.status(400).json({ status: false, message: 'User not found' }) 727 | return 728 | } 729 | const messages = await messageCollection 730 | .aggregate([ 731 | { 732 | $match: { 733 | $or: [{ to: userId }, { from: userId }], 734 | }, 735 | }, 736 | { 737 | $addFields: { 738 | datetime: { $toDate: '$createdAtISO' }, 739 | }, 740 | }, 741 | { 742 | $group: { 743 | _id: { 744 | $cond: [{ $eq: ['$to', userId] }, '$from', '$to'], // if to is equal to userId, then return from, else return to 745 | }, 746 | messages: { 747 | $push: '$$ROOT', 748 | }, 749 | maxDatetime: { 750 | $max: '$datetime', 751 | }, 752 | }, 753 | }, 754 | { 755 | $sort: { maxDatetime: -1 }, // Sort conversations by maxDatetime 756 | }, 757 | { 758 | $limit: 50, //50 conversations 759 | }, 760 | { 761 | $addFields: { 762 | idObject: { $toObjectId: '$_id' }, 763 | }, 764 | }, 765 | { 766 | $lookup: { 767 | from: 'users', 768 | localField: 'idObject', 769 | foreignField: '_id', 770 | as: 'userDetails', 771 | }, 772 | }, 773 | { 774 | $unwind: '$userDetails', 775 | }, 776 | { 777 | $project: { 778 | _id: 1, 779 | messages: 1, 780 | userDetails: { 781 | fullName: 1, 782 | username: 1, 783 | profilePicture: 1, 784 | _id: 1, 785 | }, 786 | }, 787 | }, 788 | ]) 789 | .toArray() 790 | 791 | // Sort the messages within each conversation in your application code 792 | messages.forEach((conversation) => { 793 | conversation.messages = conversation.messages.sort( 794 | (a, b) => new Date(b.createdAtISO) - new Date(a.createdAtISO) 795 | ) 796 | }) 797 | // who To follow 798 | const following = user.following 799 | // get 3 random users 800 | const randomUsers = await collection 801 | .aggregate([ 802 | { 803 | $match: { 804 | _id: { $nin: following }, 805 | }, 806 | }, 807 | { 808 | $sample: { size: 3 }, 809 | }, 810 | { 811 | $project: { 812 | _id: 1, 813 | fullName: 1, 814 | username: 1, 815 | profilePicture: 1, 816 | }, 817 | }, 818 | ]) 819 | .toArray() 820 | res.status(200).json({ 821 | status: true, 822 | messages: messages, 823 | profile: { 824 | fullName: user.fullName, 825 | username: user.username, 826 | profilePicture: user.profilePicture, 827 | _id: user._id, 828 | }, 829 | randomUsers: randomUsers, 830 | }) 831 | }) 832 | 833 | app.post('/sendMessage', verifyToken, async (req, res) => { 834 | try { 835 | const userId = req.userId 836 | const to = req.body.to 837 | if (userId == to) { 838 | res.status(400).json({ 839 | status: false, 840 | message: 'You cannot send a message to yourself', 841 | }) 842 | return 843 | } 844 | const content = req.body.content 845 | 846 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 847 | if (!user) { 848 | //not needed 849 | res.status(400).json({ status: false, message: 'User not found' }) 850 | } 851 | const userTo = await collection.findOne({ _id: new ObjectId(to) }) 852 | if (!userTo) { 853 | //not needed 854 | res.status(400).json({ status: false, message: 'User not found' }) 855 | } 856 | let message = { 857 | to: to, // might change to ObjectId 858 | from: userId, 859 | content: content, 860 | createdAt: String(new Date()), 861 | createdAtISO: new Date().toISOString(), 862 | } 863 | await messageCollection.insertOne(message) 864 | // add profile info to message before emitting it 865 | message = { 866 | ...message, 867 | fromProfile: { 868 | fullName: user.fullName, 869 | username: user.username, 870 | profilePicture: user.profilePicture, 871 | }, 872 | } 873 | // socket.io 874 | io.to(`user:${to}`).emit('newMessage', message) 875 | // end of socket.io 876 | res.status(200).json({ status: true, message: 'Message sent successfully' }) 877 | } catch (err) { 878 | res 879 | .status(400) 880 | .json({ status: false, message: err + ' Failed to send message' }) 881 | } 882 | }) 883 | app.post('/postTweet', verifyToken, async (req, res) => { 884 | try { 885 | const userId = req.userId 886 | const content = req.body.content 887 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 888 | if (!user) { 889 | res.status(400).json({ status: false, message: 'User not found' }) 890 | return 891 | } 892 | const tweet = { 893 | createdBy: userId, 894 | createdByObjectId: new ObjectId(userId), 895 | content: content, 896 | type: 'main', 897 | commentTo: 'null', 898 | createdAt: String(new Date()), 899 | createdAtISO: new Date().toISOString(), 900 | likes: [], 901 | retweets: [], 902 | comments: [], 903 | } 904 | await tweetCollection.insertOne(tweet) 905 | // increment tweetCount 906 | await collection.updateOne( 907 | { _id: new ObjectId(userId) }, 908 | { $inc: { tweetCount: 1 } } 909 | ) 910 | const aggregatedTweet = await tweetCollection //for consistency 911 | .aggregate([ 912 | { 913 | $match: { 914 | _id: tweet._id, 915 | }, 916 | }, 917 | { 918 | $lookup: { 919 | from: 'users', 920 | localField: 'createdByObjectId', 921 | foreignField: '_id', 922 | as: 'userDetails', 923 | }, 924 | }, 925 | { 926 | $unwind: '$userDetails', 927 | }, 928 | { 929 | $project: { 930 | _id: 1, 931 | createdBy: 1, 932 | content: 1, 933 | createdAt: 1, 934 | createdAtISO: 1, 935 | type: 1, 936 | commentTo: 1, 937 | likes: 1, 938 | retweets: 1, 939 | comments: 1, 940 | userDetails: { 941 | fullName: 1, 942 | username: 1, 943 | profilePicture: 1, 944 | _id: 1, 945 | }, 946 | }, 947 | }, 948 | ]) 949 | .toArray() 950 | // socket.io 951 | io.to(`followers:${String(userId)}`).emit('newTweet', aggregatedTweet[0]) 952 | // end of socket.io 953 | res.status(200).json({ 954 | status: true, 955 | message: 'Posted successfully', 956 | tweet: aggregatedTweet[0], 957 | }) 958 | } catch (err) { 959 | res 960 | .status(400) 961 | .json({ status: false, message: err + ' Failed to post tweet' }) 962 | return 963 | } 964 | }) 965 | app.post('/postComment', verifyToken, async (req, res) => { 966 | const userId = req.userId 967 | const content = req.body.content 968 | const commentToTweet = req.body.tweetId 969 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 970 | if (!user) { 971 | res.status(400).json({ status: false, message: 'User not found' }) 972 | return 973 | } 974 | const tweet = { 975 | createdBy: userId, 976 | createdByObjectId: new ObjectId(userId), 977 | content: content, 978 | type: 'comment', 979 | commentTo: commentToTweet, 980 | createdAt: String(new Date()), 981 | likes: [], 982 | retweets: [], 983 | comments: [], 984 | } 985 | await tweetCollection.insertOne(tweet) 986 | // increment tweetCount 987 | await collection.updateOne( 988 | { _id: new ObjectId(userId) }, 989 | { $inc: { tweetCount: 1 } } 990 | ) 991 | // increment commentCount 992 | await tweetCollection.updateOne( 993 | { _id: new ObjectId(commentToTweet) }, 994 | { $inc: { commentCount: 1 } } 995 | ) 996 | // add ID in comments array of tweet 997 | await tweetCollection.updateOne( 998 | { _id: new ObjectId(commentToTweet) }, 999 | { $push: { comments: tweet._id } } 1000 | ) 1001 | const aggregatedTweet = await tweetCollection 1002 | .aggregate([ 1003 | { 1004 | $match: { 1005 | _id: tweet._id, 1006 | }, 1007 | }, 1008 | { 1009 | $lookup: { 1010 | from: 'users', 1011 | localField: 'createdByObjectId', 1012 | foreignField: '_id', 1013 | as: 'userDetails', 1014 | }, 1015 | }, 1016 | { 1017 | $unwind: '$userDetails', 1018 | }, 1019 | { 1020 | $project: { 1021 | _id: 1, 1022 | createdBy: 1, 1023 | content: 1, 1024 | createdAt: 1, 1025 | createdAtISO: 1, 1026 | type: 1, 1027 | commentTo: 1, 1028 | likes: 1, 1029 | retweets: 1, 1030 | comments: 1, 1031 | userDetails: { 1032 | fullName: 1, 1033 | username: 1, 1034 | profilePicture: 1, 1035 | _id: 1, 1036 | }, 1037 | }, 1038 | }, 1039 | ]) 1040 | .toArray() 1041 | res.status(200).json({ 1042 | status: true, 1043 | message: 'Posted successfully', 1044 | tweet: aggregatedTweet[0], 1045 | }) 1046 | }) 1047 | app.post('/retweetOrLike', verifyToken, async (req, res) => { 1048 | const userId = req.userId 1049 | const tweetId = req.body.tweetId 1050 | const type = req.body.type 1051 | const action = req.body.action 1052 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 1053 | if (!user) { 1054 | res.status(400).json({ status: false, message: 'User not found' }) 1055 | return 1056 | } 1057 | const tweet = await tweetCollection.findOne({ 1058 | _id: new ObjectId(tweetId), 1059 | }) 1060 | if (!tweet) { 1061 | res.status(400).json({ status: false, message: 'Tweet not found' }) 1062 | return 1063 | } 1064 | if (type === 'retweet') { 1065 | if (action === 'do') { 1066 | // make sure that user has not retweeted the tweet before retweeting 1067 | if ( 1068 | tweet.retweets.includes(userId) || 1069 | user.retweetedTweets.includes(tweetId) 1070 | ) { 1071 | res.status(400).json({ 1072 | status: false, 1073 | message: 'You have already retweeted this tweet', 1074 | }) 1075 | return 1076 | } 1077 | await tweetCollection.updateOne( 1078 | { _id: new ObjectId(tweetId) }, 1079 | { 1080 | $push: { retweets: userId }, 1081 | } 1082 | ) 1083 | await collection.updateOne( 1084 | { _id: new ObjectId(userId) }, 1085 | { 1086 | $push: { retweetedTweets: tweetId }, 1087 | } 1088 | ) 1089 | } else { 1090 | // make sure that user has retweeted the tweet before unretweeting 1091 | if ( 1092 | !tweet.retweets.includes(userId) || 1093 | !user.retweetedTweets.includes(tweetId) 1094 | ) { 1095 | res.status(400).json({ 1096 | status: false, 1097 | message: 'You have not retweeted this tweet', 1098 | }) 1099 | return 1100 | } 1101 | await tweetCollection.updateOne( 1102 | { _id: new ObjectId(tweetId) }, 1103 | { 1104 | $pull: { retweets: userId }, 1105 | } 1106 | ) 1107 | await collection.updateOne( 1108 | { _id: new ObjectId(userId) }, 1109 | { 1110 | $pull: { retweetedTweets: tweetId }, 1111 | } 1112 | ) 1113 | } 1114 | } else if (type === 'like') { 1115 | if (action === 'do') { 1116 | // make sure that user has not liked the tweet before liking 1117 | if (tweet.likes.includes(userId) || user.likedTweets.includes(tweetId)) { 1118 | res.status(400).json({ 1119 | status: false, 1120 | message: 'You have already liked this tweet', 1121 | }) 1122 | return 1123 | } 1124 | await tweetCollection.updateOne( 1125 | { _id: new ObjectId(tweetId) }, 1126 | { 1127 | $push: { likes: userId }, 1128 | } 1129 | ) 1130 | await collection.updateOne( 1131 | { _id: new ObjectId(userId) }, 1132 | { 1133 | $push: { likedTweets: tweetId }, 1134 | } 1135 | ) 1136 | } else { 1137 | // make sure that user has liked the tweet before unliking 1138 | if ( 1139 | !tweet.likes.includes(userId) || 1140 | !user.likedTweets.includes(tweetId) 1141 | ) { 1142 | res.status(400).json({ 1143 | status: false, 1144 | message: 'You have not liked this tweet', 1145 | }) 1146 | return 1147 | } 1148 | await tweetCollection.updateOne( 1149 | { _id: new ObjectId(tweetId) }, 1150 | { 1151 | $pull: { likes: userId }, 1152 | } 1153 | ) 1154 | await collection.updateOne( 1155 | { _id: new ObjectId(userId) }, 1156 | { 1157 | $pull: { likedTweets: tweetId }, 1158 | } 1159 | ) 1160 | } 1161 | } 1162 | res.status(200).json({ status: true, message: 'Success' }) 1163 | }) 1164 | app.post('/followOrUnfollow/:username', verifyToken, async (req, res) => { 1165 | const userId = req.userId 1166 | const username = req.params.username 1167 | const action = req.body.action 1168 | const user = await collection.findOne({ username: username }) 1169 | const currentUser = await collection.findOne({ _id: new ObjectId(userId) }) 1170 | if (!user || !currentUser) { 1171 | res.status(400).json({ status: false, message: 'User not found' }) 1172 | return 1173 | } 1174 | if (user._id == userId) { 1175 | res 1176 | .status(400) 1177 | .json({ status: false, message: 'You cannot follow yourself' }) 1178 | return 1179 | } 1180 | 1181 | if (action === 'Follow') { 1182 | if ( 1183 | currentUser.following.includes( 1184 | String(user._id) || user.followers.includes(userId) //can be && instead of || 1185 | ) 1186 | ) { 1187 | res 1188 | .status(400) 1189 | .json({ status: false, message: 'You are already following this user' }) 1190 | return 1191 | } 1192 | await collection.updateOne( 1193 | { _id: new ObjectId(userId) }, 1194 | { 1195 | $push: { following: String(user._id) }, 1196 | } 1197 | ) 1198 | await collection.updateOne( 1199 | { _id: new ObjectId(user._id) }, 1200 | { 1201 | $push: { followers: userId }, 1202 | } 1203 | ) 1204 | // socket.io .. join followers room 1205 | // socket.join(`followers:${String(user._id)}`) //to fix 1206 | } else { 1207 | if ( 1208 | !currentUser.following.includes( 1209 | String(user._id) || !user.followers.includes(userId) //can be && instead of || 1210 | ) 1211 | ) { 1212 | res.status(400).json({ 1213 | status: false, 1214 | message: 'You are already not following this user', 1215 | }) 1216 | return 1217 | } 1218 | await collection.updateOne( 1219 | { _id: new ObjectId(userId) }, 1220 | { 1221 | $pull: { following: String(user._id) }, 1222 | } 1223 | ) 1224 | await collection.updateOne( 1225 | { _id: new ObjectId(user._id) }, 1226 | { 1227 | $pull: { followers: userId }, 1228 | } 1229 | ) 1230 | // socket.io .. leave followers room 1231 | // socket.leave(`followers:${String(user._id)}`) //to fix 1232 | } 1233 | res.status(200).json({ status: true, message: 'Success' }) 1234 | }) 1235 | app.post('/search', verifyToken, async (req, res) => { 1236 | const userId = req.userId 1237 | const query = req.body.query.replace('@', '') 1238 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 1239 | if (!user) { 1240 | //not needed 1241 | res.status(400).json({ status: false, message: 'User not found' }) 1242 | return 1243 | } 1244 | const users = await collection 1245 | .aggregate([ 1246 | { 1247 | $match: { 1248 | $or: [ 1249 | { fullName: { $regex: query, $options: 'i' } }, // case insensitive in the regex search 1250 | { username: { $regex: query, $options: 'i' } }, 1251 | ], 1252 | _id: { $nin: [user._id] }, // exclude current user 1253 | }, 1254 | }, 1255 | { 1256 | $addFields: { 1257 | datetime: { $toDate: '$createdAtISO' }, 1258 | }, 1259 | }, 1260 | { 1261 | $sort: { datetime: -1 }, 1262 | }, 1263 | { 1264 | $limit: 20, 1265 | }, 1266 | { 1267 | $project: { 1268 | _id: 1, 1269 | fullName: 1, 1270 | username: 1, 1271 | profilePicture: 1, 1272 | }, 1273 | }, 1274 | ]) 1275 | .toArray() 1276 | res.status(200).json({ 1277 | status: true, 1278 | users: users, 1279 | }) 1280 | }) 1281 | app.post('/editProfile', verifyToken, async (req, res) => { 1282 | const userId = req.userId 1283 | const fullName = req.body.fullName 1284 | const username = req.body.username 1285 | const bio = req.body.bio 1286 | const profilePicture = req.body.profilePicture 1287 | const coverPicture = req.body.coverPicture 1288 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 1289 | if (!user) { 1290 | //not needed 1291 | res.status(200).json({ status: true, message: 'User not found' }) //change to 400 & true later 1292 | return 1293 | } 1294 | if (user.username == 'dd') { 1295 | res 1296 | .status(401) 1297 | .json({ status: false, message: 'Cannot edit profile for demo user' }) 1298 | return 1299 | } 1300 | //check if username is taken 1301 | const usernameCheck = await collection.findOne({ username: username }) 1302 | if (usernameCheck && String(usernameCheck._id) != userId) { 1303 | res.status(400).json({ status: false, message: 'Username is taken' }) 1304 | return 1305 | } 1306 | const specialCharacters = '/*&^%$# \\' 1307 | for (let i = 0; i < specialCharacters.length; i++) { 1308 | if (username.includes(specialCharacters[i])) { 1309 | res.status(400).json({ 1310 | status: false, 1311 | message: 'Username cannot contain special characters or whitespace', 1312 | }) 1313 | return 1314 | } 1315 | } 1316 | await collection.updateOne( 1317 | { _id: new ObjectId(userId) }, 1318 | { 1319 | $set: { 1320 | fullName: fullName, 1321 | username: username, 1322 | bio: bio, 1323 | profilePicture: profilePicture, 1324 | coverPicture: coverPicture, 1325 | }, 1326 | } 1327 | ) 1328 | res.status(200).json({ 1329 | status: true, 1330 | message: 'Profile updated successfully', 1331 | }) 1332 | }) 1333 | app.post('/changePassword', verifyToken, async (req, res) => { 1334 | const userId = req.userId 1335 | const oldPassword = req.body.oldPassword 1336 | const newPassword = req.body.newPassword 1337 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 1338 | if (!user) { 1339 | //not needed 1340 | res.status(400).json({ status: false, message: 'User not found' }) 1341 | return 1342 | } 1343 | if (user.username == 'dd') { 1344 | res 1345 | .status(401) 1346 | .json({ status: false, message: 'Cannot change password for demo user' }) 1347 | return 1348 | } 1349 | const isMatch = oldPassword === user.password //possible improvement: add hashing 1350 | if (!isMatch) { 1351 | res.status(400).json({ status: false, message: 'Wrong password' }) 1352 | return 1353 | } 1354 | if (newPassword.length < 8) { 1355 | res.status(400).json({ 1356 | status: false, 1357 | message: 'Password must be at least 8 characters', 1358 | }) 1359 | return 1360 | } 1361 | await collection.updateOne( 1362 | { _id: new ObjectId(userId) }, 1363 | { 1364 | $set: { 1365 | password: newPassword, 1366 | }, 1367 | } 1368 | ) 1369 | res.status(200).json({ 1370 | status: true, 1371 | message: 'Password changed successfully', 1372 | }) 1373 | }) 1374 | app.get('/getNavProfile', verifyToken, async (req, res) => { 1375 | const userId = req.userId 1376 | const user = await collection.findOne({ _id: new ObjectId(userId) }) 1377 | if (!user) { 1378 | //not needed 1379 | res.status(400).json({ status: false, message: 'User not found' }) 1380 | return 1381 | } 1382 | // get 3 random users 1383 | const following = user.following 1384 | const randomUsers = await collection 1385 | .aggregate([ 1386 | { 1387 | $match: { 1388 | _id: { $nin: following }, 1389 | }, 1390 | }, 1391 | { 1392 | $sample: { size: 3 }, 1393 | }, 1394 | { 1395 | $project: { 1396 | _id: 1, 1397 | fullName: 1, 1398 | username: 1, 1399 | profilePicture: 1, 1400 | }, 1401 | }, 1402 | ]) 1403 | .toArray() 1404 | res.status(200).json({ 1405 | status: true, 1406 | profile: { 1407 | fullName: user.fullName, 1408 | username: user.username, 1409 | profilePicture: user.profilePicture, 1410 | _id: user._id, 1411 | }, 1412 | randomUsers: randomUsers, 1413 | }) 1414 | }) 1415 | app.get('*', (req, res) => { 1416 | res.sendFile(path.join(__dirname, 'public', 'lost.html')) 1417 | }) 1418 | // ------------------- 1419 | // Socket.io 1420 | // ------------------- 1421 | io.on('connection', (socket) => { 1422 | try { 1423 | const token = socket.handshake.query.token 1424 | if (!token) { 1425 | socket.disconnect() 1426 | return 1427 | } 1428 | const decoded = jwt.verify(token, 'someSecretKey') 1429 | if (!decoded) { 1430 | socket.disconnect() 1431 | return 1432 | } 1433 | const userId = decoded.id 1434 | socket.join(`user:${userId}`) 1435 | console.log(`user:${userId} connected`) 1436 | 1437 | socket.on('disconnect', () => { 1438 | console.log(`user:${userId} disconnected`) 1439 | }) 1440 | } catch (err) { 1441 | console.log(err) 1442 | } 1443 | }) 1444 | // Listening 1445 | httpServer.listen(3000, () => { 1446 | console.log('Server is listening on port 3000') 1447 | }) 1448 | 1449 | // Some ESM Pointers 1450 | // need to use the .mjs extension specifically for the entry point file (usually the main script file) to indicate that it is an ECMAScript module (hence, app.mjs) 1451 | // For CommonJS files that should be used with ESM modules, use the .cjs extension 1452 | --------------------------------------------------------------------------------