├── .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 |
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 |
Follow
13 |
14 |
15 |
16 |
akvn
17 |
@akvn
18 |
Follow
19 |
20 |
21 |
22 |
bonga
23 |
@duokobia
24 |
Follow
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 |
17 | Sign in with Google
18 |
19 |
20 | Sign in with Apple
21 |
22 | - or -
23 |
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 |
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 | 
6 |
7 | 
8 |
9 | 
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 | You need to enable JavaScript to run this app.
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 |
12 |
13 |
14 | Home
15 |
16 |
17 |
18 |
19 | Search
20 |
21 |
22 |
23 |
24 |
25 |
26 | Messages
27 |
28 |
29 |
30 |
31 |
32 | Profile
33 |
34 |
35 |
36 |
37 |
38 | Settings
39 |
40 |
41 |
42 |
43 |
44 | Post
45 |
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 |
75 |
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 |
73 | Sign in with Google
74 |
75 |
76 | Sign in with Apple
77 |
78 | - or -
79 |
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 |
51 | Message
52 |
53 |
54 |
55 | Follow
56 |
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 |
90 |
91 |
92 |
93 | Posts
94 |
95 |
96 | Replies
97 |
98 |
99 | Likes
100 |
101 |
102 |
103 |
104 |
105 | Following
106 |
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 |
83 |
96 |
97 |
98 |
122 |
123 |
Post
124 |
125 |
126 |
130 |
131 |
132 |
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 |
--------------------------------------------------------------------------------