├── Backend ├── firebase.json ├── functions │ ├── .gitignore │ ├── util │ │ ├── admin.js │ │ ├── config.js │ │ ├── firebaseAuth.js │ │ └── validators.js │ ├── package.json │ └── routers │ │ ├── posts.js │ │ └── users.js ├── .firebaserc ├── .gitignore ├── README.md └── DB_schema.js ├── Frontend ├── .env ├── public │ ├── _redirects │ ├── og.png │ ├── favicon.ico │ ├── robots.txt │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── manifest.json │ ├── safari-pinned-tab.svg │ └── index.html ├── .firebaserc ├── src │ ├── util │ │ ├── History.js │ │ ├── ScrollToTop.js │ │ ├── Theme │ │ │ ├── Dark.js │ │ │ └── Light.js │ │ ├── AuthRoute.js │ │ └── Languages │ │ │ ├── English.js │ │ │ └── German.js │ ├── context │ │ ├── UserContext.js │ │ ├── PostsContext.js │ │ ├── UserProfileContext.js │ │ ├── ThemeContext.js │ │ └── LanguageContext.js │ ├── assets │ │ ├── Images │ │ │ ├── default_cp.png │ │ │ ├── default_pp.png │ │ │ ├── logo.svg │ │ │ └── empty.svg │ │ └── IconsSvg │ │ │ ├── DE.svg │ │ │ ├── light.svg │ │ │ └── dark.svg │ ├── components │ │ ├── Spinner │ │ │ ├── Spinner.js │ │ │ └── Spinner.scss │ │ ├── Buttons │ │ │ ├── AddFriendButton │ │ │ │ └── AddFriendButton.scss │ │ │ ├── TwitternButton │ │ │ │ ├── TwitternButton.scss │ │ │ │ └── TwitternButton.js │ │ │ ├── EditCoverImageButton │ │ │ │ ├── EditCoverImageButton.scss │ │ │ │ └── EditCoverImageButton.js │ │ │ ├── EditProfileImageButton │ │ │ │ ├── EditProfileImageButton.scss │ │ │ │ └── EditProfileImageButton.js │ │ │ ├── CommentButton.js │ │ │ ├── SignupButton.js │ │ │ ├── LoginButton.js │ │ │ ├── DeletePostButton │ │ │ │ └── DeletePostButton.scss │ │ │ ├── TwitternBtnNavbar │ │ │ │ ├── TwitternBtnNavbar.scss │ │ │ │ └── TwitternBtnNavbar.js │ │ │ ├── SettingsButton │ │ │ │ └── SettingsButton.scss │ │ │ └── EditProfile │ │ │ │ └── EditProfile.scss │ │ ├── ImageModal │ │ │ ├── ImageModal.js │ │ │ └── ImageModal.scss │ │ ├── CheckVerifiedUserName.js │ │ ├── AddComment │ │ │ └── AddComment.scss │ │ ├── CommentCard │ │ │ ├── CommentCard.scss │ │ │ └── CommentCard.js │ │ ├── LikesModal │ │ │ ├── LikesModal.scss │ │ │ └── LikesModal.js │ │ ├── FriendsModal │ │ │ └── FriendsModal.scss │ │ ├── PostCard │ │ │ ├── PostCard.scss │ │ │ └── PostCard.js │ │ └── PostCardDetails │ │ │ ├── PostCardDetails.scss │ │ │ └── PostCardDetails.js │ ├── style │ │ ├── CssVariables.scss │ │ └── css_resets │ │ │ └── reset.local.scss │ ├── index.scss │ ├── index.js │ ├── pages │ │ ├── PageLoader │ │ │ ├── PageLoader.js │ │ │ └── PageLoader.scss │ │ ├── Page404 │ │ │ ├── Page404.scss │ │ │ └── Page404.js │ │ ├── Signup │ │ │ └── Signup.scss │ │ ├── Home │ │ │ └── Home.scss │ │ ├── Login │ │ │ └── Login.scss │ │ ├── PostDetails │ │ │ ├── PostDetails.scss │ │ │ └── PostDetails.js │ │ ├── Notifications │ │ │ └── Notifications.scss │ │ └── UserProfile │ │ │ └── UserProfile.scss │ ├── parts │ │ ├── TabletNavBarNotAuth │ │ │ ├── TabletNavBarNotAuth.scss │ │ │ └── TabletNavBarNotAuth.js │ │ ├── JoinTwirrer │ │ │ ├── JoinTwirrer.scss │ │ │ └── JoinTwirrer.js │ │ ├── RightSide │ │ │ ├── RightSide.scss │ │ │ └── RightSide.js │ │ ├── CurrentUser │ │ │ ├── CurrentUser.scss │ │ │ └── CurrentUser.js │ │ ├── WhoToAdd │ │ │ └── WhoToAdd.scss │ │ ├── MobileNavbar │ │ │ └── MobileNavbar.scss │ │ ├── AddNewPost │ │ │ └── AddNewPost.scss │ │ ├── PinnedPost │ │ │ ├── PinnedPost.scss │ │ │ └── PinnedPost.js │ │ └── Navbar │ │ │ └── Navbar.scss │ ├── services │ │ ├── PostService.js │ │ └── UserService.js │ └── serviceWorker.js ├── firebase.json ├── .gitignore ├── README.md ├── package.json └── .firebase │ └── hosting.YnVpbGQ.cache ├── home.PNG ├── twirrer.PNG ├── twirrer-home.PNG └── README.md /Backend/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Frontend/.env: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /Backend/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /Frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /home.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/home.PNG -------------------------------------------------------------------------------- /twirrer.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/twirrer.PNG -------------------------------------------------------------------------------- /twirrer-home.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/twirrer-home.PNG -------------------------------------------------------------------------------- /Backend/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "twirrer-app" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Frontend/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "twirrer-app" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Frontend/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/og.png -------------------------------------------------------------------------------- /Frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/favicon.ico -------------------------------------------------------------------------------- /Frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /Frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /Frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /Frontend/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/mstile-150x150.png -------------------------------------------------------------------------------- /Frontend/src/util/History.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /Frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /Frontend/src/context/UserContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(null); 4 | -------------------------------------------------------------------------------- /Frontend/src/context/PostsContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(null); 4 | -------------------------------------------------------------------------------- /Frontend/src/assets/Images/default_cp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/src/assets/Images/default_cp.png -------------------------------------------------------------------------------- /Frontend/src/assets/Images/default_pp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/src/assets/Images/default_pp.png -------------------------------------------------------------------------------- /Frontend/src/context/UserProfileContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(null); 4 | -------------------------------------------------------------------------------- /Frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /Frontend/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadebk/twirrer/HEAD/Frontend/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /Backend/functions/util/admin.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | 3 | admin.initializeApp(); 4 | 5 | const db = admin.firestore(); 6 | 7 | module.exports = { admin, db } -------------------------------------------------------------------------------- /Frontend/src/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Spinner.scss"; 3 | 4 | const Spinner = () => { 5 | return
; 6 | }; 7 | 8 | export default Spinner; -------------------------------------------------------------------------------- /Frontend/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Frontend/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/AddFriendButton/AddFriendButton.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style//CssVariables.scss"; 2 | 3 | .addFriend__button { 4 | padding: 0 14px; 5 | height: 37px; 6 | font-weight: bold; 7 | font-size: 14px; 8 | white-space: nowrap; 9 | border-radius: $radius; 10 | 11 | &:focus { 12 | outline: 0; 13 | } 14 | } -------------------------------------------------------------------------------- /Frontend/src/style/CssVariables.scss: -------------------------------------------------------------------------------- 1 | //global vars 2 | $radius: 9999px; 3 | $mainColor:#1DA1F2; 4 | $secondaryColor: rgba(29, 161, 242, 0.1); 5 | $secondaryHoverColorDark: #172430; 6 | $secondaryHoverColorLight: #F5F8FA; 7 | $red: #E0245E; 8 | $love: #E0245E; 9 | $darkBackground : #15202B; 10 | 11 | // to use in js file 12 | :export { 13 | radius: $radius; 14 | } 15 | -------------------------------------------------------------------------------- /Frontend/src/index.scss: -------------------------------------------------------------------------------- 1 | //-------- css reset ---------// 2 | @import "./style/css_resets/normalize.scss"; 3 | @import "./style/css_resets/reset.local.scss"; 4 | 5 | .App{ 6 | display: flex; 7 | flex-direction: row; 8 | min-height: 100vh; 9 | font-family: system-ui, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 10 | "Ubuntu", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /Frontend/src/util/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | 4 | function ScrollToTop({ history }) { 5 | useEffect(() => { 6 | const unlisten = history.listen(() => { 7 | window.scrollTo(0, 0); 8 | }); 9 | return () => { 10 | unlisten(); 11 | }; 12 | }, [history]); 13 | 14 | return null; 15 | } 16 | 17 | export default withRouter(ScrollToTop); 18 | -------------------------------------------------------------------------------- /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 | /.eslintcache 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /Frontend/src/components/ImageModal/ImageModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ModalImage from "react-modal-image"; 3 | 4 | import "./ImageModal.scss"; 5 | 6 | const ImageModal = ({ imageUrl, className }) => { 7 | return ( 8 | 15 | ); 16 | }; 17 | 18 | export default ImageModal; 19 | -------------------------------------------------------------------------------- /Frontend/src/util/Theme/Dark.js: -------------------------------------------------------------------------------- 1 | const Dark = { 2 | mainColor: "#1DA1F2", 3 | secondaryColor: "rgba(29, 161, 242, 0.1)", 4 | background: "#15202B", 5 | error: "#E0245E", 6 | errorBackground: "rgba(224, 36, 94, 0.1)", 7 | foreground: "#192734", 8 | typoMain: "#ffffff", 9 | typoSecondary: "#8899A6", 10 | logo: "#ffffff", 11 | mobileNavIcon: "#8899a6", 12 | border: "#38444d", 13 | addPostBorder: "#253341", 14 | PinnedPostBorder: "#253341", 15 | }; 16 | 17 | export default Dark; 18 | -------------------------------------------------------------------------------- /Frontend/src/util/Theme/Light.js: -------------------------------------------------------------------------------- 1 | const Light = { 2 | mainColor: "#1DA1F2", 3 | secondaryColor: "rgba(29, 161, 242, 0.1)", 4 | background: "#ffffff", 5 | error: "#E0245E", 6 | errorBackground: "rgba(224, 36, 94, 0.1)", 7 | foreground: "#F5F8FA", 8 | typoMain: "#14171A", 9 | typoSecondary: "#657786", 10 | logo: "#1DA1F2", 11 | mobileNavIcon: "#657786", 12 | border: "#e6ecf0", 13 | addPostBorder: "#e6ecf0", 14 | PinnedPostBorder: "#e6ecf0", 15 | }; 16 | 17 | export default Light; 18 | -------------------------------------------------------------------------------- /Backend/functions/util/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * configuration of our firebase project 3 | */ 4 | 5 | module.exports = { 6 | apiKey: "AIzaSyDenJlzVzL0LmDGNhZ6eH6TlZAmqFT4eJU", 7 | authDomain: "twirrer-app.firebaseapp.com", 8 | databaseURL: "https://twirrer-app.firebaseio.com", 9 | projectId: "twirrer-app", 10 | storageBucket: "twirrer-app.appspot.com", 11 | messagingSenderId: "714072408637", 12 | appId: "1:714072408637:web:eb709d07ebb91959a42054", 13 | measurementId: "G-7B0TRRWSJW" 14 | } -------------------------------------------------------------------------------- /Frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Twirrer", 3 | "name": "Twirrer | Social Network", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/TwitternButton/TwitternButton.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/CssVariables.scss"; 2 | 3 | .postButton { 4 | padding: 0 14px; 5 | height: 37px; 6 | font-weight: bold; 7 | font-size: 14px; 8 | white-space: nowrap; 9 | border-radius: $radius; 10 | 11 | &:focus { 12 | outline: 0; 13 | } 14 | } 15 | 16 | @media only screen and (max-width: 500px) { 17 | } 18 | 19 | @media (min-width: 501px) and (max-width: 991px) { 20 | } 21 | 22 | @media (min-width: 992px) and (max-width: 1280px) { 23 | } 24 | -------------------------------------------------------------------------------- /Frontend/README.md: -------------------------------------------------------------------------------- 1 | # Twirrer | Frontend 2 | 3 | Using 'React.js' + 'React Hooks' + 'React Context api' to build the frontend of Twirrer. 4 | 5 | ## Live Demo 6 | [twirrer.netlify.app](https://twirrer.netlify.app/) 7 | 8 | ## How to run the app locally? 9 | 10 | ### 1- API Base URL 11 | add `https://europe-west3-twirrer-app.cloudfunctions.net/api` as the 'proxy' value in package.json 12 | 13 | ### 2- Install packages 14 | run `npm install` 15 | 16 | ### 3- Run project 17 | run `npm start` 18 | 19 | ### 4- Open it 20 | go to `http://localhost:3000` 21 | -------------------------------------------------------------------------------- /Frontend/src/components/Spinner/Spinner.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .loader__box{ 4 | margin-top: 80px; 5 | } 6 | 7 | .loader { 8 | border: 3px solid $secondaryColor; 9 | border-top: 3px solid $mainColor; 10 | border-radius: 50%; 11 | width: 30px; 12 | height: 30px; 13 | animation: spin 0.8s linear infinite; 14 | margin: 20px auto; 15 | text-align: center; 16 | } 17 | 18 | @keyframes spin { 19 | 0% { 20 | transform: rotate(0deg); 21 | } 22 | 50% { 23 | transform: rotate(180deg); 24 | } 25 | 100% { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Frontend/src/components/ImageModal/ImageModal.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .__react_modal_image__header { 4 | background-color: transparent !important; 5 | } 6 | 7 | .__react_modal_image__modal_container { 8 | background-color: rgba(21, 32, 43, 0.9) !important; 9 | z-index: 999999999 !important; 10 | } 11 | 12 | .__react_modal_image__icon_menu svg { 13 | background: $mainColor !important; 14 | padding: 3px; 15 | border-radius: 50%; 16 | } 17 | 18 | .__react_modal_image__icon_menu { 19 | float: left !important; 20 | } 21 | 22 | .__react_modal_image__modal_content{ 23 | pointer-events: none; 24 | } 25 | -------------------------------------------------------------------------------- /Frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.scss"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.register(); 19 | -------------------------------------------------------------------------------- /Frontend/src/util/AuthRoute.js: -------------------------------------------------------------------------------- 1 | // to force app to redirect from login/signup pages to home page if user is already logged in 2 | 3 | import React, { useContext } from "react"; 4 | import { Route, Redirect } from "react-router-dom"; 5 | import UserContext from "./../context/UserContext"; 6 | 7 | const AuthRoute = ({ component: Component, ...rest }) => { 8 | const { userData } = useContext(UserContext); 9 | return( 10 | 13 | userData.isAuth === true ? : 14 | } 15 | /> 16 | ) 17 | }; 18 | 19 | export default AuthRoute; -------------------------------------------------------------------------------- /Frontend/src/pages/PageLoader/PageLoader.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | // style 4 | import "./PageLoader.scss"; 5 | 6 | // spinner 7 | import Spinner from "../../components/Spinner/Spinner"; 8 | 9 | // context (global state) 10 | import { ThemeContext } from "../../context/ThemeContext"; 11 | 12 | const PageLoader = () => { 13 | // theme context 14 | const { isLightTheme, light, dark } = useContext(ThemeContext); 15 | const theme = isLightTheme ? light : dark; 16 | 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | export default PageLoader; 25 | -------------------------------------------------------------------------------- /Frontend/src/pages/PageLoader/PageLoader.scss: -------------------------------------------------------------------------------- 1 | .pageLoader { 2 | height: auto; 3 | width: 45%; 4 | margin-left: 25%; 5 | overflow: auto; 6 | padding-bottom: 80px; 7 | } 8 | 9 | @media only screen and (max-width: 500px) { 10 | /* For mobile phones: */ 11 | .pageLoader { 12 | width: 100%; 13 | margin-left: 0; 14 | margin-bottom: 49px; 15 | } 16 | } 17 | 18 | @media (min-width: 501px) and (max-width: 991px) { 19 | /* For tablet: */ 20 | .pageLoader { 21 | width: 75%; 22 | margin-left: 15%; 23 | } 24 | } 25 | 26 | @media (min-width: 992px) and (max-width: 1280px) { 27 | /* For small lab: */ 28 | .pageLoader { 29 | width: 50%; 30 | margin-left: 10%; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Backend/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase emulators:start --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "16" 13 | }, 14 | "dependencies": { 15 | "busboy": "^0.3.1", 16 | "cors": "^2.8.5", 17 | "express": "^4.18.1", 18 | "firebase": "^9.8.3", 19 | "firebase-admin": "^10.3.0", 20 | "firebase-functions": "^3.21.2" 21 | }, 22 | "devDependencies": { 23 | "firebase-functions-test": "^0.2.3" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /Frontend/src/parts/TabletNavBarNotAuth/TabletNavBarNotAuth.scss: -------------------------------------------------------------------------------- 1 | @media (min-width: 501px) and (max-width: 991px) { 2 | .TabletNavBarNotAuth { 3 | position: relative; 4 | z-index: 999; 5 | &__main { 6 | height: 50px; 7 | width: 75%; 8 | margin-left:15%; 9 | position: fixed; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | } 14 | } 15 | } 16 | 17 | .buttons__box { 18 | display: flex; 19 | justify-content: space-around; 20 | align-items: center; 21 | height: 100%; 22 | } 23 | 24 | @media only screen and (max-width: 500px) { 25 | .TabletNavBarNotAuth { 26 | display: none; 27 | } 28 | } 29 | 30 | @media only screen and (min-width: 992px) { 31 | .TabletNavBarNotAuth { 32 | display: none; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Frontend/src/components/CheckVerifiedUserName.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from "react"; 2 | 3 | // context (global state) 4 | import { ThemeContext } from "../context/ThemeContext"; 5 | 6 | const CheckVerifiedUserName = ({ userName }) => { 7 | // theme context 8 | const { isLightTheme, light, dark } = useContext(ThemeContext); 9 | const theme = isLightTheme ? light : dark; 10 | 11 | return ( 12 | 13 | {userName === "ABDULHADI✨" ? ( 14 | 15 | {userName} 16 | 20 | 21 | ) : ( 22 | userName 23 | )} 24 | 25 | ); 26 | }; 27 | 28 | export default CheckVerifiedUserName; 29 | -------------------------------------------------------------------------------- /Frontend/src/assets/Images/logo.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/EditCoverImageButton/EditCoverImageButton.scss: -------------------------------------------------------------------------------- 1 | .headerLoad { 2 | background-color: #15202b80; 3 | width: 100%; 4 | height: 100%; 5 | top: 0; 6 | position: absolute; 7 | 8 | .loader__box { 9 | margin-top: 0; 10 | position: absolute; 11 | top: 50%; 12 | right: 50%; 13 | transform: translate(50%, -50%); 14 | } 15 | } 16 | 17 | .editCoverImage { 18 | position: absolute; 19 | right: 10px; 20 | top: 10px; 21 | 22 | input { 23 | display: none; 24 | } 25 | 26 | .pen { 27 | color: rgb(255, 255, 255); 28 | background-color: rgb(29, 161, 242); 29 | padding: 8px; 30 | border-radius: 50%; 31 | 32 | &:hover { 33 | cursor: pointer; 34 | } 35 | } 36 | } 37 | 38 | @media only screen and (max-width: 500px) { 39 | /* For mobile phones: */ 40 | .editCoverImage { 41 | .pen { 42 | padding: 5px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Frontend/src/pages/Page404/Page404.scss: -------------------------------------------------------------------------------- 1 | .page404 { 2 | height: auto; 3 | width: 45%; 4 | margin-left: 25%; 5 | overflow: auto; 6 | padding-bottom: 80px; 7 | 8 | &__imgBox { 9 | width: 100%; 10 | height: 200px; 11 | margin-top: 50px; 12 | 13 | img { 14 | width: 100%; 15 | height: 100%; 16 | } 17 | } 18 | 19 | &__hint { 20 | font-size: 23px; 21 | font-weight: bold; 22 | text-align: center; 23 | margin-top: 20px; 24 | } 25 | } 26 | 27 | @media only screen and (max-width: 500px) { 28 | /* For mobile phones: */ 29 | .page404 { 30 | width: 100%; 31 | margin-left: 0; 32 | margin-bottom: 49px; 33 | } 34 | } 35 | 36 | @media (min-width: 501px) and (max-width: 991px) { 37 | /* For tablet: */ 38 | .page404 { 39 | width: 75%; 40 | margin-left: 15%; 41 | } 42 | } 43 | 44 | @media (min-width: 992px) and (max-width: 1280px) { 45 | /* For small lab: */ 46 | .page404 { 47 | width: 50%; 48 | margin-left: 10%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/EditProfileImageButton/EditProfileImageButton.scss: -------------------------------------------------------------------------------- 1 | .avatarLoad { 2 | background-color: #15202b80; 3 | width: 100%; 4 | height: 100%; 5 | border-radius: 50%; 6 | top: 0; 7 | position: absolute; 8 | left: 0; 9 | 10 | .loader__box { 11 | margin-top: 0; 12 | position: absolute; 13 | top: 50%; 14 | right: 50%; 15 | transform: translate(50%, -50%); 16 | } 17 | } 18 | 19 | .editPPImage { 20 | position: absolute; 21 | top: 80%; 22 | transform: translateY(-80%); 23 | right: -5px; 24 | 25 | input { 26 | display: none; 27 | } 28 | 29 | i { 30 | color: rgb(255, 255, 255); 31 | background-color: rgb(29, 161, 242); 32 | padding: 8px; 33 | border-radius: 50%; 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | } 40 | 41 | @media only screen and (max-width: 500px) { 42 | /* For mobile phones: */ 43 | .editPPImage { 44 | i { 45 | padding: 5px; 46 | font-size: 14px; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Frontend/src/parts/JoinTwirrer/JoinTwirrer.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .joinTwirrer { 4 | &__box { 5 | padding: 20px; 6 | border-radius: 15px; 7 | margin: 30px 60px 30px 20px; 8 | 9 | &__imageBox { 10 | width: 100%; 11 | height: 200px; 12 | img { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | } 17 | 18 | &__body { 19 | &__Title { 20 | margin: 20px 0; 21 | text-align: center; 22 | 23 | h2 { 24 | font-size: 23px; 25 | font-weight: 800; 26 | } 27 | 28 | p { 29 | font-size: 13px; 30 | margin: 0; 31 | } 32 | } 33 | 34 | &__buttons { 35 | div { 36 | width: 100% !important; 37 | height: auto !important; 38 | padding: 7px 0 !important; 39 | box-sizing: border-box; 40 | border-radius: $radius; 41 | } 42 | .signupButton { 43 | margin-bottom: 20px; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Frontend/src/pages/Page404/Page404.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | // style 4 | import "./Page404.scss"; 5 | 6 | // assets 7 | import Error404 from "../../assets/Images/page404.svg"; 8 | 9 | // context (global state) 10 | import { ThemeContext } from "../../context/ThemeContext"; 11 | import { LanguageContext } from "../../context/LanguageContext"; 12 | 13 | const Page404 = () => { 14 | // theme context 15 | const { isLightTheme, light, dark } = useContext(ThemeContext); 16 | const theme = isLightTheme ? light : dark; 17 | 18 | // language context 19 | const { isEnglish, english, german } = useContext(LanguageContext); 20 | var language = isEnglish ? english : german; 21 | 22 | return ( 23 |
24 |
25 | page404 26 |
27 |

28 | {language.page404.hint} 29 |

30 |
31 | ); 32 | }; 33 | 34 | export default Page404; 35 | -------------------------------------------------------------------------------- /Frontend/src/assets/IconsSvg/DE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/CommentButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | 3 | // context (global state) 4 | import { ThemeContext } from "../../context/ThemeContext"; 5 | import PostsContext from "../../context/PostsContext"; 6 | 7 | const CommentButton = ({ post }) => { 8 | // ******* start global state ******* // 9 | 10 | // theme context 11 | const { isLightTheme, light, dark } = useContext(ThemeContext); 12 | const theme = isLightTheme ? light : dark; 13 | 14 | // posts context 15 | const { posts } = useContext(PostsContext); 16 | 17 | // ******* end global state ******* // 18 | 19 | useEffect(() => {}, [posts]); 20 | 21 | return ( 22 |
23 |
24 | 25 |
31 |
32 | {post.commentCount === 0 ? "" : post.commentCount} 33 |
34 | ); 35 | }; 36 | 37 | export default CommentButton; 38 | -------------------------------------------------------------------------------- /Frontend/src/parts/RightSide/RightSide.scss: -------------------------------------------------------------------------------- 1 | .rightSide { 2 | position: relative; 3 | 4 | &__box { 5 | height: 100vh; 6 | width: 30%; 7 | position: fixed; 8 | 9 | &__whoToAddBox { 10 | padding: 20px 0; 11 | border-radius: 15px; 12 | margin: 20px 60px 30px 20px; 13 | 14 | .whoToAdd{ 15 | &__title{ 16 | h2{ 17 | padding: 0 15px 10px; 18 | } 19 | } 20 | 21 | &__usersBox{ 22 | &__user{ 23 | padding: 10px 15px; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | @media only screen and (max-width: 500px) { 32 | /* For mobile phones: */ 33 | .rightSide__box { 34 | width: 0; 35 | display: none; 36 | } 37 | } 38 | 39 | @media (min-width: 501px) and (max-width: 991px) { 40 | /* For tablet: */ 41 | .rightSide__box { 42 | width: 10%; 43 | 44 | &__whoToAddBox{ 45 | display: none; 46 | } 47 | 48 | .joinTwirrer{ 49 | display: none; 50 | } 51 | } 52 | } 53 | 54 | @media (min-width: 992px) and (max-width: 1280px) { 55 | /* For small lab: */ 56 | .rightSide__box { 57 | width: 40%; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Frontend/src/parts/TabletNavBarNotAuth/TabletNavBarNotAuth.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | // style file 4 | import "./TabletNavBarNotAuth.scss"; 5 | 6 | // context (global state) 7 | import { ThemeContext } from "../../context/ThemeContext"; 8 | 9 | // components 10 | import LoginButton from "../../components/Buttons/LoginButton"; 11 | import SignupButton from "../../components/Buttons/SignupButton"; 12 | 13 | const TabletNavBarNotAuth = () => { 14 | // ******* start global state ******* // 15 | 16 | // theme context 17 | const { isLightTheme, light, dark } = useContext(ThemeContext); 18 | const theme = isLightTheme ? light : dark; 19 | 20 | // ******* end global state ******* // 21 | return ( 22 |
23 |
30 |
31 | 32 | 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default TabletNavBarNotAuth; 40 | -------------------------------------------------------------------------------- /Frontend/src/parts/CurrentUser/CurrentUser.scss: -------------------------------------------------------------------------------- 1 | .currentUser { 2 | display: flex; 3 | flex-direction: row; 4 | margin: 20px 60px 0px 20px; 5 | border-radius: 50px; 6 | padding: 10px 15px; 7 | 8 | &__userImage { 9 | width: 50px; 10 | min-width: 50px; 11 | height: 50px; 12 | border-radius: 50%; 13 | margin-right: 10px; 14 | position: relative; 15 | 16 | img { 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 50%; 20 | box-sizing: border-box; 21 | object-fit: cover; 22 | } 23 | 24 | &__greenDot { 25 | width: 15px; 26 | height: 15px; 27 | background-color: #31a24c; 28 | position: absolute; 29 | right: 0; 30 | bottom: 5%; 31 | border-radius: 50%; 32 | } 33 | } 34 | 35 | &__userName { 36 | width: 75%; 37 | h2 { 38 | font-size: 19px; 39 | font-weight: 800; 40 | margin-bottom: 3px; 41 | white-space: nowrap; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | } 45 | 46 | p { 47 | font-size: 15px; 48 | margin-bottom: 0; 49 | } 50 | } 51 | 52 | .loader__box { 53 | margin-top: 0; 54 | text-align: center; 55 | margin: 0 auto; 56 | } 57 | } 58 | 59 | @media (max-width: 991px) { 60 | .currentUser { 61 | display: none; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Frontend/src/style/css_resets/reset.local.scss: -------------------------------------------------------------------------------- 1 | /****** Elad Shechter's RESET *******/ 2 | /*** box sizing border-box for all elements ***/ 3 | *, 4 | *::before, 5 | *::after { 6 | box-sizing: border-box; 7 | } 8 | a { 9 | text-decoration: none; 10 | cursor: pointer; 11 | } 12 | button { 13 | &:focus { 14 | outline: 0; 15 | } 16 | background-color: transparent; 17 | color: inherit; 18 | border-width: 0; 19 | padding: 0; 20 | cursor: pointer; 21 | } 22 | figure { 23 | margin: 0; 24 | } 25 | input::-moz-focus-inner { 26 | border: 0; 27 | padding: 0; 28 | margin: 0; 29 | } 30 | ul, 31 | ol, 32 | dd { 33 | margin: 0; 34 | padding: 0; 35 | list-style: none; 36 | } 37 | h1, 38 | h2, 39 | h3, 40 | h4, 41 | h5, 42 | h6 { 43 | margin: 0; 44 | font-size: inherit; 45 | font-weight: inherit; 46 | } 47 | p { 48 | margin: 0; 49 | } 50 | cite { 51 | font-style: normal; 52 | } 53 | fieldset { 54 | border-width: 0; 55 | padding: 0; 56 | margin: 0; 57 | } 58 | 59 | /******* by myself *******/ 60 | 61 | // delete default box-shadow "red" for input if it was invalid or require and have no value, in firefox browser 62 | input:invalid { 63 | box-shadow: none; 64 | } 65 | 66 | textarea, 67 | input[type="text"] { 68 | -webkit-appearance: none; 69 | -moz-appearance: none; 70 | -o-appearance: none; 71 | appearance: none; 72 | } 73 | -------------------------------------------------------------------------------- /Backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/SignupButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import variables from "../../style/CssVariables.scss"; 5 | 6 | // context (global state) 7 | import { ThemeContext } from "../../context/ThemeContext"; 8 | import { LanguageContext } from "../../context/LanguageContext"; 9 | 10 | const SignupButton = () => { 11 | // ******* start global state ******* // 12 | 13 | // theme context 14 | const { isLightTheme, light, dark } = useContext(ThemeContext); 15 | const theme = isLightTheme ? light : dark; 16 | 17 | // language context 18 | const { isEnglish, english, german } = useContext(LanguageContext); 19 | var language = isEnglish ? english : german; 20 | 21 | // ******* end global state ******* // 22 | return ( 23 |
33 | 44 | {language.signup.signupButton} 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default SignupButton; 51 | -------------------------------------------------------------------------------- /Backend/functions/routers/posts.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | // import middleware authentication 4 | const firebaseAuth = require("../util/firebaseAuth"); 5 | 6 | // import operations of the routes 7 | const { 8 | postsFirstFetch, 9 | postsNextFetch, 10 | pinedPost, 11 | addNewPost, 12 | deletePost, 13 | getOnePost, 14 | commentOnPost, 15 | likePost, 16 | unlikePost, 17 | } = require("../handlers/posts"); 18 | 19 | // 1- crete a post's router 20 | const router = new express.Router(); 21 | 22 | // 2- add post's routes 23 | //----------------------------------------------------------------------------------------------------------------- 24 | // [ Post Routs ] 25 | //----------------------------------------------------------------------------------------------------------------- 26 | 27 | // post routes 28 | app.get("/postsFirstFetch", postsFirstFetch); 29 | app.post("/postsNextFetch", postsNextFetch); 30 | app.get("/post/:postId/get", getOnePost); 31 | app.get("/pinedPost", pinedPost); 32 | app.post("/addNewPost", firebaseAuth, addNewPost); // cause 'FirebaseAuth' fun - if user not authorized, this route will not work. 33 | app.delete("/post/:postId/delete", firebaseAuth, deletePost); 34 | app.post("/post/:postId/comment", firebaseAuth, commentOnPost); 35 | app.get("/post/:postId/like", firebaseAuth, likePost); 36 | app.get("/post/:postId/unlike", firebaseAuth, unlikePost); 37 | 38 | // 3- export post's router, to use it in index.js 39 | module.exports = router; 40 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/LoginButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // Global vars import 5 | import variables from "../../style/CssVariables.scss"; 6 | 7 | // context (global state) 8 | import { ThemeContext } from "../../context/ThemeContext"; 9 | import { LanguageContext } from "../../context/LanguageContext"; 10 | 11 | const LoginButton = () => { 12 | // ******* start global state ******* // 13 | 14 | // theme context 15 | const { isLightTheme, light, dark } = useContext(ThemeContext); 16 | const theme = isLightTheme ? light : dark; 17 | 18 | // language context 19 | const { isEnglish, english, german } = useContext(LanguageContext); 20 | var language = isEnglish ? english : german; 21 | 22 | // ******* end global state ******* // 23 | 24 | return ( 25 |
35 | 46 | {language.login.logInButton} 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default LoginButton; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twirrer | Social Network App 2 | 3 | I tried in this project to build an application similar to Twitter but certainly does not have all the features, but it contains the basic features such as (create an account, add a tweet, add a friend, like, comment, etc..). 4 | 5 | ## Live Demo 6 | [twirrer.netlify.app](https://twirrer.netlify.app/) 7 | 8 | ## Project Screenshot 9 | ![Twirrer screenshot](./home.PNG) 10 | ![Twirrer screenshot](./twirrer.PNG) 11 | 12 | ## Project Features: 13 | - Cache 📂 14 | - Nice design as Twitter design. 15 | - Fully responsive design (100%). 16 | - Dark/Light theme. 17 | - English/German languages. 18 | - Notifications in app. 19 | 20 | ## Technologies used in the project: 21 | ### 1- Backend: 22 | - using 'Firebase cloud function' + 'Express.js' to build an API, to handle all operations with database. 23 | - using 'Firebase Triggers' to execute some events in app like (fire notification, listen to user avatar changes, etc..). 24 | - using 'Firebase Authentication' to handel login/signup users. 25 | - using 'Firebase Firestore&Storage' to store data of the app (NoSQL database). 26 | 27 | ### 2- Frontend: 28 | - using 'React.js' to build the frontend of Twirrer. 29 | - using 'React Hooks' to handle local state & 'React Context api' to handle global state in the app. 30 | - using 'Axios' to execute all RestFull api requests in the app. 31 | - implement infinite scroll (pure js) to posts in home page 32 | - using 'SCSS, CSS Normalize & Css Resets'. 33 | - using 'BEM' methodology to naming the items in HTML. 34 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/DeletePostButton/DeletePostButton.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/CssVariables.scss"; 2 | 3 | .deleteIconBox { 4 | position: absolute; 5 | width: 30px; 6 | height: 30px; 7 | z-index: 99; 8 | } 9 | 10 | .deletePost__main__modal__body { 11 | border-top-left-radius: 15px !important; 12 | border-top-right-radius: 15px !important; 13 | padding: 30px 20px !important; 14 | 15 | &__title { 16 | font-size: 19px; 17 | font-weight: bold; 18 | margin-bottom: 10px; 19 | text-align: center; 20 | } 21 | 22 | &__message { 23 | font-size: 15px; 24 | margin-bottom: 20px; 25 | text-align: center; 26 | } 27 | 28 | &__buttonsBox { 29 | display: flex; 30 | flex-direction: row; 31 | justify-content: space-around; 32 | 33 | button { 34 | padding: 0 15px; 35 | height: 39px; 36 | font-weight: bold; 37 | border-radius: $radius; 38 | font-size: 15px; 39 | width: 47%; 40 | 41 | &:focus { 42 | outline: 0; 43 | } 44 | } 45 | } 46 | } 47 | 48 | /* modal edit */ 49 | .deletePost__main__modal { 50 | .modal-dialog { 51 | width: 350px !important; 52 | } 53 | } 54 | 55 | @media only screen and (max-width: 500px) { 56 | .deletePost__main__modal { 57 | .modal-dialog { 58 | margin-left: 20px !important; 59 | margin-right: 20px !important; 60 | width: auto !important; 61 | 62 | .modal-content { 63 | min-height: auto; 64 | 65 | .modal-body { 66 | border-bottom-left-radius: 15px !important; 67 | border-bottom-right-radius: 15px !important; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Frontend/src/components/AddComment/AddComment.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | /* start add comment box */ 4 | .addCommentBox { 5 | padding: 10px; 6 | padding-top: 15px; 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: space-between; 10 | 11 | &__userImageBox { 12 | div { 13 | width: 35px; 14 | height: 35px; 15 | max-height: 35px; 16 | overflow: hidden; 17 | border-radius: 50%; 18 | margin-right: 10px; 19 | } 20 | 21 | &__image { 22 | width: 100%; 23 | height: 100%; 24 | border-radius: 50%; 25 | object-fit: cover; 26 | } 27 | } 28 | 29 | &__inputBox { 30 | width: 100%; 31 | margin-right: 5px; 32 | ::-webkit-scrollbar { 33 | width: 0px; /* Remove scrollbar space */ 34 | background: transparent; /* Optional: just make scrollbar invisible */ 35 | } 36 | /* Optional: show position indicator in red */ 37 | ::-webkit-scrollbar-thumb { 38 | background: $mainColor; 39 | } 40 | 41 | &__textarea { 42 | width: 100%; 43 | resize: none; 44 | border-radius: 20px; 45 | padding: 8px 10px; 46 | font-size: 15px; 47 | font-weight: 400; 48 | overflow-x: hidden; 49 | 50 | &:focus { 51 | outline: 0; 52 | } 53 | 54 | &::placeholder { 55 | color: #8899a6; 56 | } 57 | } 58 | } 59 | 60 | &__buttonBox { 61 | button { 62 | &:focus { 63 | outline: 0; 64 | border: 0; 65 | } 66 | } 67 | .send { 68 | font-size: 20px; 69 | text-align: center; 70 | vertical-align: middle; 71 | padding: 7px; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Frontend/src/context/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, Component } from "react"; 2 | 3 | // import data files 4 | import Dark from "../util/Theme/Dark"; 5 | import Light from "../util/Theme/Light"; 6 | 7 | export const ThemeContext = createContext(); 8 | 9 | class ThemeContextProvider extends Component { 10 | state = { 11 | isLightTheme: true, 12 | dark: Dark, 13 | light: Light, 14 | }; 15 | 16 | componentDidMount() { 17 | /** 18 | * before render the component, check first if user have selected theme before? 19 | * if yes, update the state according to localStorage value 20 | */ 21 | const _isLightTheme = window.localStorage.getItem("isLightTheme"); 22 | if (_isLightTheme === "false") { 23 | this.setState({ 24 | isLightTheme: false, 25 | }); 26 | } 27 | if (_isLightTheme === "true") { 28 | this.setState({ 29 | isLightTheme: true, 30 | }); 31 | } 32 | } 33 | 34 | // toggle current theme 35 | toggleTheme = () => { 36 | if (this.state.isLightTheme === false) { 37 | window.localStorage.setItem("isLightTheme", true); 38 | this.setState({ 39 | isLightTheme: true, 40 | }); 41 | } else { 42 | window.localStorage.setItem("isLightTheme", false); 43 | this.setState({ 44 | isLightTheme: false, 45 | }); 46 | } 47 | }; 48 | 49 | render() { 50 | return ( 51 | // pass state and fun to whole app 52 | 55 | {this.props.children} 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default ThemeContextProvider; 62 | -------------------------------------------------------------------------------- /Frontend/src/parts/RightSide/RightSide.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, Fragment } from "react"; 2 | 3 | // style file 4 | import "./RightSide.scss"; 5 | 6 | // context (global state) 7 | import { ThemeContext } from "../../context/ThemeContext"; 8 | import UserContext from "../../context/UserContext"; 9 | 10 | // libraries 11 | import WhoToAdd from "../WhoToAdd/WhoToAdd"; 12 | import JoinTwirrer from "../JoinTwirrer/JoinTwirrer"; 13 | import CurrentUser from "../CurrentUser/CurrentUser"; 14 | 15 | const RightSide = () => { 16 | // ******* start global state ******* // 17 | 18 | // theme context 19 | const { isLightTheme, light, dark } = useContext(ThemeContext); 20 | const theme = isLightTheme ? light : dark; 21 | 22 | // user context 23 | const { userData } = useContext(UserContext); 24 | 25 | // ******* end global state ******* // 26 | useEffect(() => {}, [userData.isAuth]); 27 | 28 | return ( 29 |
30 |
37 | {userData.isAuth && window.screen.width > 991 ? ( 38 | 39 | 40 |
46 | 47 |
48 |
49 | ) : ( 50 | 51 | )} 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default RightSide; 58 | -------------------------------------------------------------------------------- /Frontend/src/context/LanguageContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, Component } from "react"; 2 | 3 | // import data files 4 | import English from "../util/Languages/English"; 5 | import German from "../util/Languages/German"; 6 | 7 | export const LanguageContext = createContext(); 8 | 9 | class LanguageContextProvider extends Component { 10 | state = { 11 | isEnglish: true, 12 | english: English, 13 | german: German, 14 | }; 15 | 16 | componentDidMount() { 17 | /** 18 | * before render the component, check first if user have selected language before? 19 | * if yes, update the state according to localStorage value 20 | */ 21 | const _isEnglish = window.localStorage.getItem("isEnglish"); 22 | if (_isEnglish === "false") { 23 | this.setState({ 24 | isEnglish: false, 25 | }); 26 | } 27 | if (_isEnglish === "true") { 28 | this.setState({ 29 | isEnglish: true, 30 | }); 31 | } 32 | } 33 | 34 | // toggle current language 35 | toggleLanguage = () => { 36 | if (this.state.isEnglish === false) { 37 | window.localStorage.setItem("isEnglish", true); 38 | this.setState({ 39 | isEnglish: true, 40 | }); 41 | } else { 42 | window.localStorage.setItem("isEnglish", false); 43 | this.setState({ 44 | isEnglish: false, 45 | }); 46 | } 47 | }; 48 | 49 | render() { 50 | return ( 51 | // pass state and fun to whole app 52 | 55 | {this.props.children} 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default LanguageContextProvider; 62 | -------------------------------------------------------------------------------- /Frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://twirrer-app.web.app/", 3 | "name": "twirrer", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.8", 8 | "@testing-library/react": "^11.2.3", 9 | "@testing-library/user-event": "^12.6.0", 10 | "axios": "^0.21.1", 11 | "bootstrap": "^4.5.3", 12 | "dayjs": "^1.10.2", 13 | "jquery": "^3.5.1", 14 | "jwt-decode": "^3.1.2", 15 | "lodash.debounce": "^4.0.8", 16 | "moment": "^2.29.1", 17 | "moment-twitter": "^0.2.0", 18 | "npm-check-updates": "^10.2.5", 19 | "react": "^17.0.1", 20 | "react-bootstrap": "^1.4.1", 21 | "react-bootstrap-modal": "^4.2.0", 22 | "react-dom": "^17.0.1", 23 | "react-image-lightbox": "^5.1.1", 24 | "react-linkify": "^1.0.0-alpha", 25 | "react-modal-image": "^2.5.0", 26 | "react-responsive-modal": "^6.0.0", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "4.0.1", 29 | "sass": "^1.32.2", 30 | "sass-loader": "^10.1.0", 31 | "superagent": "^6.1.0" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test", 37 | "eject": "react-scripts eject", 38 | "predeploy": "npm run build", 39 | "deploy": "gh-pages -d build" 40 | }, 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | }, 56 | "proxy": "https://europe-west3-twirrer-app.cloudfunctions.net/api" 57 | } 58 | -------------------------------------------------------------------------------- /Backend/functions/util/firebaseAuth.js: -------------------------------------------------------------------------------- 1 | const { admin, db } = require('./admin'); 2 | 3 | /** 4 | * [Middleware Authentication] 5 | * ******************************************************************************* 6 | * usage: authorize the user - check if this user is authorized and has token, 7 | * also check that this token was generated by our system (firebase) or not. 8 | */ 9 | 10 | module.exports = (req, res, next) => { 11 | let idToken; 12 | if ( 13 | req.headers.authorization && 14 | req.headers.authorization.startsWith('Bearer ') 15 | ) { 16 | // authorization: 'Bearer {user_token}' 17 | idToken = req.headers.authorization.split('Bearer ')[1]; 18 | } else { 19 | console.error('No token found'); 20 | return res.status(403).json({ 21 | error: 'Unauthorized' 22 | }); 23 | } 24 | 25 | // verify that the token was generated by our system (firebase) 26 | admin 27 | .auth() 28 | .verifyIdToken(idToken) 29 | .then((decodedToken) => { 30 | req.user = decodedToken; 31 | return db 32 | .collection('users') 33 | .where('userId', '==', req.user.uid) 34 | .limit(1) 35 | .get(); 36 | }) 37 | .then((data) => { 38 | // this data will be accessible across whole project, in each time sending request to server 39 | req.user.userName = data.docs[0].data().userName; 40 | req.user.profilePicture = data.docs[0].data().profilePicture; 41 | return next(); 42 | }) 43 | .catch((err) => { 44 | console.error('Error while verifying token ', err); 45 | return res.status(403).json(err); 46 | }); 47 | } -------------------------------------------------------------------------------- /Backend/functions/routers/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | // import middleware authentication 4 | const firebaseAuth = require("../util/firebaseAuth"); 5 | 6 | const { 7 | signup, 8 | login, 9 | logout, 10 | uploadProfileImage, 11 | uploadCoverImage, 12 | uploadPostImage, 13 | addUserDetails, 14 | getAuthenticatedUser, 15 | getUserDetails, 16 | markNotificationsAsRead, 17 | addFriend, 18 | unFriend, 19 | usersToAdd, 20 | } = require("../handlers/users"); 21 | 22 | // 1- crete a user's router 23 | const router = new express.Router(); 24 | 25 | // 2- add user's routes 26 | //----------------------------------------------------------------------------------------------------------------- 27 | // [ User Routs ] 28 | //----------------------------------------------------------------------------------------------------------------- 29 | 30 | // user routes 31 | router.post("/signup", signup); 32 | router.post("/login", login); 33 | router.get("/logout", logout); 34 | router.post("/uploadProfileImage", firebaseAuth, uploadProfileImage); 35 | router.post("/uploadCoverImage", firebaseAuth, uploadCoverImage); 36 | router.post("/uploadPostImage", firebaseAuth, uploadPostImage); 37 | router.post("/addUserDetails", firebaseAuth, addUserDetails); 38 | router.get("/getAuthenticatedUser", firebaseAuth, getAuthenticatedUser); 39 | router.get("/user/:userName/getUserDetails", getUserDetails); 40 | router.get("/user/:userName/addFriend", firebaseAuth, addFriend); 41 | router.get("/user/:userName/unFriend", firebaseAuth, unFriend); 42 | router.get("/usersToAdd", firebaseAuth, usersToAdd); 43 | router.post("/markNotificationsAsRead", firebaseAuth, markNotificationsAsRead); 44 | 45 | // 3- export user's router, to use it in index.js 46 | module.exports = router; 47 | -------------------------------------------------------------------------------- /Frontend/src/parts/JoinTwirrer/JoinTwirrer.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | // style file 4 | import "./JoinTwirrer.scss"; 5 | 6 | // assets 7 | import joinTwirrer from "../../assets/Images/joinTwirrer.svg"; 8 | 9 | // components 10 | import LoginButton from "../../components/Buttons/LoginButton"; 11 | import SignupButton from "../../components/Buttons/SignupButton"; 12 | 13 | // context (global state) 14 | import { ThemeContext } from "../../context/ThemeContext"; 15 | import { LanguageContext } from "../../context/LanguageContext"; 16 | 17 | const JoinTwirrer = () => { 18 | // ******* start global state ******* // 19 | 20 | // theme context 21 | const { isLightTheme, light, dark } = useContext(ThemeContext); 22 | const theme = isLightTheme ? light : dark; 23 | 24 | // language context 25 | const { isEnglish, english, german } = useContext(LanguageContext); 26 | var language = isEnglish ? english : german; 27 | 28 | // ******* end global state ******* // 29 | return ( 30 |
31 |
32 |
33 | join twirrer 34 |
35 |
36 |
37 |

{language.rightSide.JoinTwirrer}

38 |

{language.rightSide.JoinTwirrerSub}

39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default JoinTwirrer; 51 | -------------------------------------------------------------------------------- /Frontend/src/assets/IconsSvg/light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Frontend/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/TwitternBtnNavbar/TwitternBtnNavbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/CssVariables.scss"; 2 | 3 | .twitternButtonsBox { 4 | .bigButton { 5 | padding: 0 14px; 6 | height: 37px; 7 | font-weight: bold; 8 | font-size: 14px; 9 | white-space: nowrap; 10 | width: 100%; 11 | height: 50px; 12 | font-size: 15px; 13 | border-radius: $radius; 14 | 15 | &:focus { 16 | outline: 0; 17 | } 18 | } 19 | .smallButton { 20 | &:focus { 21 | outline: 0; 22 | } 23 | } 24 | } 25 | 26 | /* modal edit */ 27 | 28 | .modal{ 29 | padding-left: 0 !important; 30 | } 31 | 32 | .modal-content { 33 | background: transparent !important; 34 | border: 0 !important; 35 | 36 | .modal-body { 37 | border-bottom-left-radius: 15px !important; 38 | border-bottom-right-radius: 15px !important; 39 | } 40 | } 41 | 42 | .twitternButtonsBox__modal__header { 43 | padding: 10px 15px !important; 44 | border-top-left-radius: 15px !important; 45 | border-top-right-radius: 15px !important; 46 | 47 | &__iconBox { 48 | position: relative; 49 | margin-right: 25px; 50 | 51 | &:hover > &__background { 52 | display: block; 53 | } 54 | 55 | &:hover { 56 | color: $mainColor; 57 | cursor: pointer; 58 | } 59 | 60 | &__background { 61 | position: absolute; 62 | top: -3px; 63 | left: -10px; 64 | width: 30px; 65 | height: 30px; 66 | border-radius: 50%; 67 | display: none; 68 | } 69 | } 70 | } 71 | 72 | @media only screen and (max-width: 500px) { 73 | .modal-dialog { 74 | margin: 0 !important; 75 | } 76 | 77 | .modal-content { 78 | .modal-body { 79 | border-bottom-left-radius: 0 !important; 80 | border-bottom-right-radius: 0 !important; 81 | } 82 | } 83 | .twitternButtonsBox__modal__header { 84 | border-top-left-radius: 0 !important; 85 | border-top-right-radius: 0 !important; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Frontend/src/pages/Signup/Signup.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables"; 2 | 3 | .signupMain-box { 4 | min-height: 100vh; 5 | width: 100%; 6 | z-index: 9999999999; 7 | } 8 | 9 | .main { 10 | max-width: 600px; 11 | width: 100%; 12 | height: 100%; 13 | margin: 0 auto; 14 | padding: 20px 15px; 15 | 16 | .title { 17 | text-align: center; 18 | font-weight: 700; 19 | margin-bottom: 30px; 20 | font-size: 23px; 21 | } 22 | 23 | .logo { 24 | text-align: center; 25 | margin-bottom: 35px; 26 | &__svg { 27 | width: 35px; 28 | } 29 | } 30 | 31 | .login { 32 | text-align: center; 33 | font-size: 15px; 34 | 35 | .login__link:hover { 36 | text-decoration: underline; 37 | } 38 | } 39 | } 40 | 41 | .form { 42 | padding: 0 15px; 43 | 44 | .form__inputBox { 45 | padding: 0 10px; 46 | padding-top: 3px; 47 | border-radius: 2px; 48 | border-bottom-width: 3px; 49 | border-bottom-style: solid; 50 | } 51 | 52 | .form__inputBox__label { 53 | margin-bottom: 0; 54 | font-size: 15px; 55 | } 56 | 57 | .form__inputBox__input { 58 | background-color: transparent; 59 | padding: 0; 60 | border: 0; 61 | height: 30px; 62 | 63 | &:focus { 64 | background-color: transparent; 65 | border-color: transparent; 66 | outline: 0; 67 | box-shadow: none; 68 | } 69 | } 70 | 71 | .form__button { 72 | width: 100%; 73 | height: 47px; 74 | font-weight: 700; 75 | margin-bottom: 30px; 76 | border-radius: $radius; 77 | border-color: transparent; 78 | 79 | &:hover { 80 | border-color: transparent; 81 | } 82 | 83 | &:focus { 84 | border-color: transparent; 85 | box-shadow: none; 86 | } 87 | 88 | &:active { 89 | box-shadow: none; 90 | } 91 | 92 | &:not(:disabled):not(.disabled):active:focus { 93 | box-shadow: none; 94 | } 95 | 96 | &:not(:disabled):not(.disabled):active { 97 | border-color: transparent; 98 | } 99 | } 100 | 101 | small { 102 | margin-bottom: 0.25rem; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Frontend/src/parts/WhoToAdd/WhoToAdd.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .whoToAdd__spinner { 4 | .loader__box { 5 | margin-top: 0; 6 | } 7 | } 8 | 9 | .whoToAdd { 10 | &__title { 11 | h2 { 12 | font-size: 19px; 13 | font-weight: 800; 14 | padding: 0 10px 10px; 15 | } 16 | } 17 | &__usersBox { 18 | &__user { 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: flex-start; 22 | padding: 10px; 23 | 24 | &__leftSide { 25 | display: flex; 26 | flex-direction: row; 27 | /*width: calc(100% - 155px);*/ 28 | 29 | &__userImageBox { 30 | width: 50px; 31 | height: 50px; 32 | min-width: 50px; 33 | max-width: 50px; 34 | border-radius: 50%; 35 | margin-right: 10px; 36 | 37 | img { 38 | width: 100%; 39 | height: 100%; 40 | border-radius: 50%; 41 | object-fit: cover; 42 | } 43 | } 44 | } 45 | 46 | &__rightSide { 47 | width: 100%; 48 | overflow: hidden; 49 | display: flex; 50 | flex-direction: column; 51 | 52 | &__line1 { 53 | display: flex; 54 | flex-direction: row; 55 | justify-content: space-between; 56 | 57 | &__userName { 58 | &:hover > p { 59 | text-decoration: none; 60 | text-underline-position: under; 61 | } 62 | 63 | overflow: hidden; 64 | white-space: nowrap; 65 | text-overflow: ellipsis; 66 | font-weight: bold; 67 | font-size: 15px; 68 | margin-right: 5px; 69 | 70 | &__subName { 71 | overflow: hidden; 72 | white-space: nowrap; 73 | text-overflow: ellipsis; 74 | font-weight: 400; 75 | font-size: 15px; 76 | margin: 0; 77 | margin-right: 5px; 78 | margin-top: -5px; 79 | } 80 | } 81 | } 82 | 83 | &__line2 { 84 | font-size: 15px; 85 | margin-top: 5px; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Frontend/src/parts/MobileNavbar/MobileNavbar.scss: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 500px) { 2 | .MobileNavber { 3 | position: relative; 4 | z-index: 999; 5 | &__main { 6 | height: 50px; 7 | width: 100%; 8 | position: fixed; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | } 13 | } 14 | 15 | .modal__header { 16 | border-top-left-radius: 0 !important; 17 | border-top-right-radius: 0 !important; 18 | } 19 | 20 | .MobileNavber__box { 21 | height: 100%; 22 | 23 | // start twittern btn style 24 | &__twitternButton { 25 | position: absolute; 26 | right: 20px; 27 | top: -80px; 28 | 29 | .bigButton { 30 | display: none !important; 31 | } 32 | 33 | .smallButton { 34 | border-radius: 50%; 35 | width: 60px; 36 | height: 60px; 37 | box-shadow: rgba(136, 153, 166, 0.2) 0px 0px 10px, 38 | rgba(136, 153, 166, 0.25) 0px 1px 3px 1px; 39 | i { 40 | font-size: 20px; 41 | } 42 | } 43 | } 44 | 45 | &__tabs { 46 | display: flex; 47 | justify-content: space-around; 48 | flex-direction: row; 49 | align-items: center; 50 | vertical-align: middle; 51 | height: 100%; 52 | 53 | .MobileNavber__box__tab { 54 | &__icon { 55 | font-size: 22px; 56 | position: relative; 57 | 58 | span { 59 | position: absolute; 60 | top: -6px; 61 | right: -10px; 62 | font-size: 11px; 63 | display: inline-block; 64 | border-radius: 50%; 65 | width: 17px; 66 | height: 17px; 67 | box-sizing: content-box; 68 | text-align: center; 69 | } 70 | } 71 | 72 | .settingsBox { 73 | right: -10px; 74 | bottom: 39px; 75 | } 76 | 77 | .Navbar__box__tab__text { 78 | display: none; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | .buttons__box { 86 | display: flex; 87 | justify-content: space-around; 88 | align-items: center; 89 | height: 100%; 90 | } 91 | 92 | @media only screen and (min-width: 501px) { 93 | .MobileNavber { 94 | display: none; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Frontend/src/pages/Home/Home.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .home-box { 4 | height: auto; 5 | width: 45%; 6 | margin-left: 25%; 7 | padding-bottom: 60px; 8 | // overflow: auto; 9 | 10 | &__title { 11 | padding: 15px 10px; 12 | height: 53px; 13 | position: fixed; 14 | width: 45%; 15 | z-index: 100; 16 | h1 { 17 | font-size: 19px; 18 | font-weight: 800; 19 | margin: 0; 20 | } 21 | } 22 | 23 | &__content { 24 | margin-top: 53px; 25 | } 26 | 27 | &__addNewPostWrapper { 28 | padding: 10px; 29 | } 30 | 31 | &__SettingsButton { 32 | font-size: 15px; 33 | padding: 0 14px; 34 | margin-top: 20px; 35 | height: 40px; 36 | border-radius: $radius; 37 | 38 | &:focus { 39 | outline: 0; 40 | } 41 | 42 | &__icon { 43 | margin-right: 5px; 44 | } 45 | } 46 | 47 | &__note { 48 | text-align: center; 49 | font-size: 15px; 50 | margin-top: 20px; 51 | margin-bottom: 19px; 52 | 53 | i { 54 | font-size: 20px; 55 | } 56 | } 57 | 58 | &__spinner { 59 | .loader__box { 60 | margin-top: 0; 61 | } 62 | } 63 | 64 | .whoToAdd { 65 | display: none; 66 | 67 | &__title { 68 | h2 { 69 | padding: 15px 10px; 70 | margin: 0; 71 | } 72 | } 73 | } 74 | } 75 | 76 | @media only screen and (max-width: 500px) { 77 | /* For mobile phones: */ 78 | .home-box { 79 | width: 100%; 80 | margin-left: 0; 81 | margin-bottom: 49px; 82 | padding-bottom: 0; 83 | &__title { 84 | width: 100%; 85 | } 86 | 87 | .whoToAdd { 88 | display: block; 89 | } 90 | } 91 | } 92 | 93 | @media (min-width: 501px) and (max-width: 991px) { 94 | /* For tablet: */ 95 | .home-box { 96 | width: 75%; 97 | margin-left: 15%; 98 | &__title { 99 | width: 75%; 100 | } 101 | .whoToAdd { 102 | display: block; 103 | } 104 | } 105 | } 106 | 107 | @media (min-width: 992px) and (max-width: 1280px) { 108 | /* For small lab: */ 109 | .home-box { 110 | width: 50%; 111 | margin-left: 10%; 112 | &__title { 113 | width: 50%; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Frontend/src/pages/Login/Login.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables"; 2 | 3 | .main-box { 4 | min-height: 100vh; 5 | width: 100%; 6 | z-index: 9999999999; 7 | } 8 | 9 | .main { 10 | max-width: 600px; 11 | width: 100%; 12 | height: 100%; 13 | margin: 0 auto; 14 | padding-top: 20px; 15 | padding-left: 15px; 16 | padding-right: 15px; 17 | 18 | .title { 19 | text-align: center; 20 | font-weight: 700; 21 | margin-bottom: 20px; 22 | font-size: 23px; 23 | } 24 | 25 | #general-error { 26 | text-align: center; 27 | margin-bottom: 20px; 28 | } 29 | 30 | .logo { 31 | text-align: center; 32 | margin-bottom: 35px; 33 | &__svg { 34 | width: 35px; 35 | } 36 | } 37 | 38 | .signUp { 39 | text-align: center; 40 | font-size: 15px; 41 | 42 | .signUp__link:hover { 43 | text-decoration: underline; 44 | } 45 | } 46 | } 47 | 48 | .form { 49 | padding: 0 15px; 50 | 51 | .form__inputBox { 52 | padding: 0 10px; 53 | padding-top: 3px; 54 | border-radius: 2px; 55 | border-bottom-width: 3px; 56 | border-bottom-style: solid; 57 | } 58 | 59 | .form__inputBox__label { 60 | margin-bottom: 0; 61 | font-size: 15px; 62 | } 63 | 64 | .form__inputBox__input { 65 | background-color: transparent; 66 | padding: 0; 67 | border: 0; 68 | height: 30px; 69 | 70 | &:focus { 71 | background-color: transparent; 72 | border-color: transparent; 73 | outline: 0; 74 | box-shadow: none; 75 | } 76 | } 77 | 78 | .form__button { 79 | width: 100%; 80 | height: 47px; 81 | font-weight: 700; 82 | margin-bottom: 30px; 83 | border-radius: $radius; 84 | border-color: transparent; 85 | 86 | &:hover { 87 | border-color: transparent; 88 | } 89 | &:focus { 90 | border-color: transparent; 91 | box-shadow: none; 92 | } 93 | 94 | &:active { 95 | box-shadow: none; 96 | } 97 | 98 | &:not(:disabled):not(.disabled):active:focus { 99 | box-shadow: none; 100 | } 101 | 102 | &:not(:disabled):not(.disabled):active { 103 | border-color: transparent; 104 | } 105 | } 106 | 107 | small { 108 | margin-bottom: 0.25rem; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Frontend/src/pages/PostDetails/PostDetails.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .postDetails__main { 4 | height: auto; 5 | width: 45%; 6 | margin-left: 25%; 7 | overflow: auto; 8 | height: auto; 9 | 10 | // page header 11 | &__title { 12 | padding: 15px 10px; 13 | height: 53px; 14 | position: fixed; 15 | width: 45%; 16 | z-index: 100; 17 | display: flex; 18 | flex-direction: row; 19 | 20 | &__iconBox { 21 | position: relative; 22 | margin: 0 32px 0 10px; 23 | 24 | &:hover > &__background { 25 | display: block; 26 | } 27 | 28 | &:hover { 29 | color: $mainColor; 30 | cursor: pointer; 31 | } 32 | 33 | &__background { 34 | position: absolute; 35 | top: -3px; 36 | left: -8px; 37 | width: 30px; 38 | height: 30px; 39 | border-radius: 50%; 40 | display: none; 41 | } 42 | } 43 | 44 | h1 { 45 | font-size: 19px; 46 | font-weight: 800; 47 | margin: 0; 48 | } 49 | } 50 | } 51 | 52 | 53 | 54 | /* start comments box*/ 55 | .postComments { 56 | margin-bottom: 80px; 57 | &__empty { 58 | text-align: center; 59 | padding: 20px 10px 0; 60 | img { 61 | width: 100%; 62 | max-width: 100%; 63 | height: 150px; 64 | margin-bottom: 20px; 65 | } 66 | p { 67 | font-size: 15px; 68 | } 69 | } 70 | } 71 | 72 | @media only screen and (max-width: 500px) { 73 | /* For mobile phones: */ 74 | .postDetails__main { 75 | width: 100%; 76 | margin-left: 0; 77 | margin-bottom: 49px; 78 | &__title { 79 | width: 100%; 80 | } 81 | } 82 | .postDetails__post__content__line1__box { 83 | flex-direction: column; 84 | } 85 | } 86 | 87 | @media (min-width: 501px) and (max-width: 991px) { 88 | /* For tablet: */ 89 | .postDetails__main { 90 | width: 75%; 91 | margin-left: 15%; 92 | &__title { 93 | width: 75%; 94 | } 95 | } 96 | } 97 | 98 | @media (min-width: 992px) and (max-width: 1280px) { 99 | /* For small lab: */ 100 | .postDetails__main { 101 | width: 50%; 102 | margin-left: 10%; 103 | &__title { 104 | width: 50%; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/SettingsButton/SettingsButton.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style//CssVariables.scss"; 2 | 3 | .settingMain { 4 | position: relative; 5 | } 6 | 7 | .settingsBox { 8 | padding: 10px 0; 9 | position: absolute; 10 | bottom: 0; 11 | z-index: 999999; 12 | border-radius: 15px; 13 | min-width: 250px; 14 | box-shadow: rgba(136, 153, 166, 0.2) 0px 0px 15px, 15 | rgba(136, 153, 166, 0.15) 0px 0px 3px 1px; 16 | 17 | &__header { 18 | display: flex; 19 | flex-direction: row; 20 | padding: 0 15px; 21 | 22 | &__iconBox { 23 | position: relative; 24 | margin-right: 20px; 25 | 26 | &:hover > &__background { 27 | display: block; 28 | } 29 | 30 | &:hover { 31 | color: $mainColor; 32 | cursor: pointer; 33 | } 34 | 35 | &__background { 36 | position: absolute; 37 | top: -3px; 38 | left: -10px; 39 | width: 30px; 40 | height: 30px; 41 | border-radius: 50%; 42 | display: none; 43 | } 44 | } 45 | 46 | &__title { 47 | font-size: 19px; 48 | font-weight: 800; 49 | margin-bottom: 10px; 50 | } 51 | } 52 | 53 | &__body { 54 | table { 55 | width: 100%; 56 | 57 | tr td { 58 | padding: 10px 15px; 59 | min-width: 125px; 60 | 61 | } 62 | } 63 | 64 | .--title { 65 | font-size: 15px; 66 | i{ 67 | margin-right: 10px; 68 | } 69 | } 70 | 71 | .--choices { 72 | display: flex; 73 | flex-direction: row; 74 | 75 | div { 76 | width: 20px; 77 | height: 20px; 78 | margin-right: 15px; 79 | 80 | img { 81 | width: 100%; 82 | height: 100%; 83 | max-width: 100%; 84 | max-height: 100%; 85 | border-radius: 50%; 86 | box-sizing: content-box; 87 | padding: 1px; 88 | } 89 | } 90 | } 91 | 92 | &__logout { 93 | padding: 20px 15px 10px; 94 | text-align: center; 95 | 96 | button { 97 | border-radius: $radius; 98 | color: #fff; 99 | font-size: 14px; 100 | padding: 0px 14px; 101 | height: 37px; 102 | white-space: nowrap; 103 | padding-bottom: 3px; 104 | 105 | &:focus { 106 | outline: 0; 107 | } 108 | } 109 | 110 | i { 111 | margin-left: 10px; 112 | margin-top: 4px; 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Frontend/src/components/CommentCard/CommentCard.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .commentCard { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: flex-start; 7 | padding: 10px; 8 | 9 | &__userImage { 10 | &__wrapper { 11 | width: 50px; 12 | height: 50px; 13 | overflow: hidden; 14 | border-radius: 50%; 15 | 16 | &__image { 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 50%; 20 | object-fit: cover; 21 | } 22 | } 23 | } 24 | 25 | &__content { 26 | width: 100%; 27 | padding-left: 10px; 28 | overflow: hidden; 29 | 30 | &__line1 { 31 | font-size: 15px; 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: space-between; 35 | 36 | &__box { 37 | width: 90%; 38 | display: flex; 39 | flex-direction: row; 40 | } 41 | 42 | &__userName { 43 | font-weight: 700; 44 | white-space: nowrap; 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | margin-right: 5px; 48 | } 49 | 50 | &__time { 51 | font-size: 13px; 52 | white-space: nowrap; 53 | line-height: 1.8; 54 | } 55 | 56 | &__delete { 57 | position: relative; 58 | display: flex; 59 | justify-content: flex-end; 60 | &:hover { 61 | cursor: pointer; 62 | } 63 | &:hover > .background { 64 | visibility: visible; 65 | } 66 | i { 67 | position: absolute; 68 | right: 8px; 69 | top: 7px; 70 | display: inline-block; 71 | } 72 | 73 | .background { 74 | position: absolute; 75 | width: 30px; 76 | height: 30px; 77 | border-radius: 50%; 78 | visibility: hidden; 79 | } 80 | } 81 | } 82 | 83 | &__line2 { 84 | font-size: 15px; 85 | 86 | &__box { 87 | width: 100%; 88 | display: flex; 89 | flex-direction: row; 90 | } 91 | &__text { 92 | font-weight: 400; 93 | white-space: nowrap; 94 | margin-right: 5px; 95 | } 96 | 97 | &__authorName { 98 | white-space: nowrap; 99 | overflow: hidden; 100 | text-overflow: ellipsis; 101 | } 102 | 103 | &__content { 104 | overflow-wrap: break-word; 105 | font-size: 15px; 106 | font-weight: 400; 107 | line-height: 1.3125; 108 | white-space: pre-line; 109 | } 110 | } 111 | } 112 | } 113 | 114 | @media only screen and (max-width: 500px) { 115 | /* For mobile phones: */ 116 | } 117 | -------------------------------------------------------------------------------- /Frontend/src/parts/CurrentUser/CurrentUser.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // style file 5 | import "./CurrentUser.scss"; 6 | 7 | // assets 8 | import default_pp from "../../assets/Images/default_pp.png"; 9 | 10 | // components 11 | import Spinner from "../../components/Spinner/Spinner"; 12 | import CheckVerifiedUserName from "../../components/CheckVerifiedUserName"; 13 | 14 | // context (global state) 15 | import { ThemeContext } from "../../context/ThemeContext"; 16 | import { LanguageContext } from "../../context/LanguageContext"; 17 | import UserContext from "../../context/UserContext"; 18 | 19 | const CurrentUser = () => { 20 | // ******* start global state ******* // 21 | 22 | // theme context 23 | const { isLightTheme, light, dark } = useContext(ThemeContext); 24 | const theme = isLightTheme ? light : dark; 25 | 26 | // language context 27 | const { isEnglish, english, german } = useContext(LanguageContext); 28 | var language = isEnglish ? english : german; 29 | 30 | // user context 31 | const { userData, setUserData } = useContext(UserContext); 32 | 33 | // ******* end global state ******* // 34 | 35 | useEffect(() => {}, [userData.isAuth, setUserData]); 36 | 37 | return ( 38 |
39 | {userData.isAuth ? ( 40 | <> 41 |
42 | 43 | profile 51 |
55 | 56 |
57 |
58 | 62 |

63 | 66 |

67 | 68 |

69 | {userData.user.credentials.friendsCount}{" "} 70 | {language.userProfile.friends} 71 |

72 |
73 | 74 | ) : ( 75 | 76 | )} 77 |
78 | ); 79 | }; 80 | 81 | export default CurrentUser; 82 | -------------------------------------------------------------------------------- /Backend/README.md: -------------------------------------------------------------------------------- 1 | # Twirrer | Backend 2 | 3 | Using 'Firebase cloud function' + 'Express.js' to build an API, to handle all operations with database in Twirrer. 4 | 5 | ## API Documentation: 6 | 7 | API-BASE-URL: `https://europe-west3-twirrer-app.cloudfunctions.net/api` 8 | 9 | ### 1- Posts Endpoints: 10 | 11 | - `'/postsFirstFetch'`: retrieve first batch of posts (20 post). 12 | - `'/postsNextFetch'`: retrieve next batch of posts (20 post). 13 | - `'/post/:postId/get'`: retrieve one post (details, likes and comments). 14 | - `'/pinedPost'`: retrieve one post by id, to pinned it at the start in home page. 15 | - `'/addNewPost'`: add new post, 16 | receive body: {postData} 17 | receive headers: { Authorization: 'Bearer ' + userToken } 18 | - `'/uploadPostImage'`: add image with post, 19 | receive body: {formData} 20 | receive headers: { Authorization: 'Bearer ' + userToken } 21 | - `'/post/:postId/delete'`: delete post by id, 22 | receive headers: { Authorization: 'Bearer ' + userToken } 23 | - `'/post/:postId/comment'`: add comment to post, 24 | receive body: {comment} 25 | receive headers: { Authorization: 'Bearer ' + userToken } 26 | - `'/post/:postId/like'`: like a post, 27 | receive headers: { Authorization: 'Bearer ' + userToken } 28 | - `'/post/:postId/unlike'`: unlike a post, 29 | receive headers: { Authorization: 'Bearer ' + userToken } 30 | 31 | ### 2- User Endpoints: 32 | 33 | - `'/login'`: login the user, 34 | receive body: {userData} 35 | - `'/signup'`: signup the user, 36 | receive body: {userData} 37 | - `'/getAuthenticatedUser'`: get all data of current auth user (profile data, friends, likes, notifications), 38 | receive headers: { Authorization: 'Bearer ' + userToken } 39 | - `'/user/:userName/getUserDetails'`: retrieve single user details, to show them in user profile. 40 | - `'/addUserDetails'`: user add extra data to profile like (bio, location, website), 41 | receive body: {extraUserData} 42 | receive headers: { Authorization: 'Bearer ' + userToken } 43 | - `'/uploadProfileImage'`: user add new profile image, 44 | receive body: {formData} 45 | receive headers: { Authorization: 'Bearer ' + userToken } 46 | - `'/uploadCoverImage'`: user add new cover image, 47 | receive body: {formData} 48 | receive headers: { Authorization: 'Bearer ' + userToken } 49 | - `'/user/:userName/addFriend'`: user add another user as friend, 50 | receive headers: { Authorization: 'Bearer ' + userToken } 51 | - `'/user/:userName/unFriend'`: user delete another user from friends list, 52 | receive headers: { Authorization: 'Bearer ' + userToken } 53 | - `'/usersToAdd'`: retrieve random user to show them as suggestion friends, 54 | receive headers: { Authorization: 'Bearer ' + userToken } 55 | - `'/markNotificationsAsRead'`: mark notifications as read, when open notifications tap, 56 | receive body: {notificationsIds} 57 | receive headers: { Authorization: 'Bearer ' + userToken } 58 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/EditProfile/EditProfile.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/CssVariables.scss"; 2 | 3 | .editProfile__main { 4 | &__button { 5 | padding: 0 14px; 6 | height: 37px; 7 | font-weight: bold; 8 | font-size: 14px; 9 | border-radius: $radius; 10 | 11 | &:focus { 12 | outline: 0; 13 | } 14 | } 15 | } 16 | 17 | .editProfile__main__modal { 18 | .modal-content { 19 | border: 0; 20 | background: transparent; 21 | 22 | .modal-body { 23 | border-bottom-right-radius: 20px; 24 | border-bottom-left-radius: 20px; 25 | } 26 | } 27 | &__header { 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | padding-top: 10px; 32 | padding-bottom: 10px; 33 | align-items: center !important; 34 | border-top-left-radius: 20px !important; 35 | border-top-right-radius: 20px !important; 36 | 37 | .left { 38 | display: flex; 39 | flex-direction: row; 40 | } 41 | 42 | &__iconBox { 43 | position: relative; 44 | margin-right: 25px; 45 | 46 | &:hover > &__background { 47 | display: block; 48 | } 49 | 50 | &:hover { 51 | color: $mainColor; 52 | cursor: pointer; 53 | } 54 | 55 | &__background { 56 | position: absolute; 57 | top: -3px; 58 | left: -10px; 59 | width: 30px; 60 | height: 30px; 61 | border-radius: 50%; 62 | display: none; 63 | } 64 | } 65 | 66 | &__title { 67 | font-size: 18px; 68 | font-weight: 800; 69 | margin-bottom: 0; 70 | line-height: 1.5; 71 | } 72 | 73 | &__saveButton { 74 | padding: 0 14px; 75 | height: 37px; 76 | font-weight: bold; 77 | font-size: 14px; 78 | white-space: nowrap; 79 | border-radius: $radius; 80 | 81 | &:focus { 82 | outline: 0; 83 | } 84 | } 85 | } 86 | 87 | .form .form__inputBox { 88 | padding: 3px 10px 0; 89 | border-radius: 2px; 90 | border-bottom-width: 3px; 91 | border-bottom-style: solid; 92 | 93 | input { 94 | padding-left: 0; 95 | padding-right: 0; 96 | 97 | &:focus { 98 | outline: 0; 99 | border: 0; 100 | } 101 | } 102 | } 103 | } 104 | 105 | @media only screen and (max-width: 500px) { 106 | // mobile screen 107 | .modal-dialog { 108 | margin: 0 !important; 109 | 110 | .modal-content { 111 | min-height: 100vh; 112 | 113 | .editProfile__main__modal__header { 114 | border-top-left-radius: 0 !important; 115 | border-top-right-radius: 0 !important; 116 | } 117 | 118 | .modal-body { 119 | border-bottom-right-radius: 0; 120 | border-bottom-left-radius: 0; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Twirrer 50 | 56 | 57 | 58 |
59 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Backend/functions/util/validators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **************************************************************** 3 | * helper functions 4 | * **************************************************************** 5 | */ 6 | const isEmail = (email) => { 7 | const emailRegEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 8 | if (email.match(emailRegEx)) return true; 9 | else return false; 10 | }; 11 | 12 | const isEmpty = (string) => { 13 | if (string.trim() === "") return true; 14 | // string is either empty or has space only 15 | else return false; 16 | }; 17 | 18 | /** 19 | * **************************************************************** 20 | * signup data validation 21 | * **************************************************************** 22 | */ 23 | exports.validateSignupData = (data) => { 24 | let errors = {}; 25 | 26 | if (isEmpty(data.email)) { 27 | errors.email = "Must not be empty"; 28 | } else if (!isEmail(data.email)) { 29 | errors.email = "Must be a valid email"; 30 | } 31 | 32 | if (isEmpty(data.password) || data.password.length < 6) 33 | errors.password = "Must be 6 char or more"; 34 | 35 | if (data.password !== data.confirmPassword) 36 | errors.confirmPassword = "Passwords must be match"; 37 | 38 | if (isEmpty(data.userName)) errors.userName = "Must not be empty"; 39 | 40 | return { 41 | errors, 42 | valid: Object.keys(errors).length === 0 ? true : false, 43 | }; 44 | }; 45 | 46 | /** 47 | * **************************************************************** 48 | * login data validation 49 | * **************************************************************** 50 | */ 51 | exports.validateLoginData = (data) => { 52 | let errors = {}; 53 | 54 | if (isEmpty(data.email)) errors.email = "Must not be empty"; 55 | if (isEmpty(data.password)) errors.password = "Must not be empty"; 56 | 57 | return { 58 | errors, 59 | valid: Object.keys(errors).length === 0 ? true : false, 60 | }; 61 | }; 62 | 63 | /** 64 | * **************************************************************** 65 | * user extra data validation 66 | * **************************************************************** 67 | */ 68 | exports.reduceUserDetails = (data) => { 69 | let userDetails = {}; 70 | 71 | //if(!isEmpty(data.bio.trim())) 72 | userDetails.bio = data.bio.trim(); 73 | 74 | /*if (!isEmpty(data.website.trim())) { 75 | }*/ 76 | 77 | // ex: https://website.com - it's better to convert it to http, because if the host has no ssl, the app will crash 78 | /*if (data.website.trim().substring(0, 4) !== "http") { 79 | userDetails.website = `http://${data.website.trim()}`; 80 | } else userDetails.website = data.website.trim();*/ 81 | 82 | userDetails.website = data.website.trim(); 83 | 84 | // if (!isEmpty(data.location.trim())) 85 | userDetails.location = data.location.trim(); 86 | 87 | return userDetails; 88 | }; 89 | -------------------------------------------------------------------------------- /Frontend/src/services/PostService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const PostService = { 4 | PinnedPost: async function () { 5 | try { 6 | const response = await axios.get("/pinedPost"); 7 | return response; 8 | } catch (error) { 9 | throw error; 10 | } 11 | }, 12 | 13 | postsFirstFetch: async function () { 14 | try { 15 | const response = await axios.get("/postsFirstFetch"); 16 | return response; 17 | } catch (error) { 18 | throw error; 19 | } 20 | }, 21 | 22 | postsNextFetch: async function (lastKey) { 23 | try { 24 | const response = await axios.post("/postsNextFetch", lastKey); 25 | return response; 26 | } catch (error) { 27 | throw error; 28 | } 29 | }, 30 | 31 | getPostDetails: async function (postId) { 32 | try { 33 | const response = await axios.get(`/post/${postId}/get`); 34 | return response; 35 | } catch (error) { 36 | throw error; 37 | } 38 | }, 39 | 40 | addNewPost: async function (post, token) { 41 | try { 42 | const response = await axios.post("/addNewPost", post, { 43 | headers: { Authorization: `Bearer ${token}` }, 44 | }); 45 | return response; 46 | } catch (error) { 47 | throw error; 48 | } 49 | }, 50 | 51 | uploadPostImage: async function (fromData, token) { 52 | try { 53 | const response = await axios.post("/uploadPostImage", fromData, { 54 | headers: { Authorization: `Bearer ${token}` }, 55 | }); 56 | return response; 57 | } catch (error) { 58 | throw error; 59 | } 60 | }, 61 | 62 | deletePost: async function (postId, token) { 63 | try { 64 | const response = await axios.delete(`/post/${postId}/delete`, { 65 | headers: { Authorization: `Bearer ${token}` }, 66 | }); 67 | return response; 68 | } catch (error) { 69 | throw error; 70 | } 71 | }, 72 | 73 | LikePost: async function (postId, token) { 74 | try { 75 | const response = await axios.get(`/post/${postId}/like`, { 76 | headers: { Authorization: `Bearer ${token}` }, 77 | }); 78 | return response; 79 | } catch (error) { 80 | throw error; 81 | } 82 | }, 83 | 84 | unlikePost: async function (postId, token) { 85 | try { 86 | const response = await axios.get(`/post/${postId}/unlike`, { 87 | headers: { Authorization: `Bearer ${token}` }, 88 | }); 89 | return response; 90 | } catch (error) { 91 | throw error; 92 | } 93 | }, 94 | 95 | addComment: async function (postId, comment, token) { 96 | try { 97 | const response = await axios.post(`/post/${postId}/comment`, comment, { 98 | headers: { Authorization: `Bearer ${token}` }, 99 | }); 100 | return response; 101 | } catch (error) { 102 | throw error; 103 | } 104 | }, 105 | }; 106 | 107 | export default PostService; 108 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/TwitternBtnNavbar/TwitternBtnNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, Fragment } from "react"; 2 | 3 | // style 4 | import "./TwitternBtnNavbar.scss"; 5 | // Global vars import 6 | import variables from "../../../style/CssVariables.scss"; 7 | 8 | // libraries 9 | import { Modal } from "react-bootstrap"; 10 | 11 | // context (global state) 12 | import { ThemeContext } from "../../../context/ThemeContext"; 13 | import { LanguageContext } from "../../../context/LanguageContext"; 14 | import AddNewPost from "../../../parts/AddNewPost/AddNewPost"; 15 | 16 | const TwitternBtnNavbar = () => { 17 | // ******* start global state *******// 18 | // theme context 19 | const { isLightTheme, light, dark } = useContext(ThemeContext); 20 | const theme = isLightTheme ? light : dark; 21 | 22 | // language context 23 | const { isEnglish, english, german } = useContext(LanguageContext); 24 | var language = isEnglish ? english : german; 25 | 26 | // ******* end global state *******// 27 | 28 | // local state 29 | const [isOpen, setOpen] = useState(false); 30 | 31 | let closeModal = () => setOpen(false); 32 | 33 | let openModal = () => setOpen(true); 34 | 35 | return ( 36 | 37 |
38 | 48 | 57 |
58 | 59 | 60 | 67 |
closeModal()} 70 | > 71 | 72 |
78 |
79 |
80 | 85 | 86 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default TwitternBtnNavbar; 93 | -------------------------------------------------------------------------------- /Frontend/.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | logo.ico,1594495875378,f7b03a462f525b5e705fb7201ef5f00489b70f5be49442b9268319c604960d77 2 | logo.svg,1594495824767,7976d4e08f70631e79a317dec48da404681a03ff8023c9b04726ceff718e42ca 3 | logo192.png,1594496197871,b94cfd073dcc42fab4b148a1fdf47616c0ed261c08461cdbb42b6cbb4d20fd2e 4 | logo512.png,1594496219823,8434830d907f982c85c457316f359421857023d317e2fd0f100e8c678dce036a 5 | robots.txt,499162500000,bfe106a3fb878dc83461c86818bf74fc1bdc7f28538ba613cd3e775516ce8b49 6 | asset-manifest.json,1598535032224,847534f1530cca6d886cc2f79342e4d372b7668deefd604a49e0f0b7e04cde7d 7 | index.html,1598535541682,8e1e188f5c2e51e503091e6b1d049a1800861aa15c84e74fa39e70e35ec7d0ce 8 | manifest.json,1598535385941,d958fce10dfdf391b5c95caf2c1e67b3db70bfa93515ae57db6400cc0da5c343 9 | precache-manifest.056f58b3758a8e9c4793e2d5d150cccf.js,1598535032223,d7ca1cd08ba9a54de54b03038635ff708d7442a226a82e767002872e7efcd411 10 | service-worker.js,1598535032223,9a4339366f17dfb392d026b113b6414f63c12a81634ab327adf3210a7f11a81d 11 | static/css/main.a9179716.chunk.css,1598535032210,ae9e978a777ad989eebb9bdb280005b472e5e478b390d3297c9004e7a352ed78 12 | static/js/2.6ee47bae.chunk.js.LICENSE.txt,1598535032202,1d839cc60b10b91c371a3a1d3de6e1e2b77773d75d4692fc70f69d1ec9fc4f60 13 | static/js/runtime-main.ec337310.js,1598535032199,b109ce3d1414c4b4f37f45c3d4f457a740a9740c460946602e4eefe057b47f06 14 | static/js/runtime-main.ec337310.js.map,1598535032225,fd955389d0fe2aa63db4b74dd911e69287f3b54680b4f762726d268a75d58791 15 | static/media/dark.70e51d89.svg,1598535032196,2eac6d519bc5d64e6cf870f03af56c1df33af9f8aedccd8750ee85cbd1105407 16 | static/media/DE.d810f621.svg,1598535032201,8186232c412405aa284e5cdd081a9acc4cc545f5d1b4f72b2f4c62578b2ce4d8 17 | static/media/empty.baa5cf6b.svg,1598535032196,fd543b32dbc9f2d91ca8da7bd6758e68ec3d42283736e8b68e4582a8449ed9e7 18 | static/media/default_pp.5a021ab9.png,1598535032211,e3f49c3333aa67938315a2d484e2678263d54afb1cf237a2a93e8a99d9973a8b 19 | static/media/light.da20b792.svg,1598535032198,78089c889220195091aa9e1bd67b5098d1e80082321d9b1657bb5b52702b0278 20 | static/media/joinTwirrer.d1bad45a.svg,1598535032201,cdc318648d42d8e3dbda838fa5125af78db8bb880ccbd37a83532ad39f4aa61d 21 | static/media/UK.b6e5300d.svg,1598535032198,96ac248b735fc09c55136ab6376726ff9c38ac78785d88b25552615a9f4ad0e8 22 | static/media/page404.fae88b9c.svg,1598535032196,aa029e943bb74c46d2c7e8b43373155b80c7c493b30ffa799e460add086333a0 23 | static/css/main.a9179716.chunk.css.map,1598535032203,b6ec81b8d79a83706877e67d42e75996363b45f9699dbacddfa8a3181bf437f3 24 | static/js/main.7c1d6c6c.chunk.js,1598535032194,bfc6092a7526d83b758d263f25d8a614d26cdb65bf1a777450ccd009faffdbdc 25 | static/css/2.af3c1da9.chunk.css,1598535032200,bc2b276730bac99e3f270322dbf26d91ec4a8bbcb6e40cf4b870409435e7793e 26 | static/js/main.7c1d6c6c.chunk.js.map,1598535032225,129f2711f00e809eae8c10e7ef3dfe1147c4eb7ca76048ff9265689671d7fd3f 27 | static/js/2.6ee47bae.chunk.js,1598535032202,f9cf864935555a8b5546bc5bd0eb46a1122eb8168530cd76c12d15f147a68487 28 | static/css/2.af3c1da9.chunk.css.map,1598535032225,3463671eca27f87e72b9ff46f92bfa171ad0582ce0c696975651e8116eb6e3f4 29 | static/js/2.6ee47bae.chunk.js.map,1598535032227,12782c2b56f29e225ef7d1c463fa632c37a5bcf6dcc5c268a79ad63c2ac11921 30 | -------------------------------------------------------------------------------- /Frontend/src/assets/IconsSvg/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Frontend/src/util/Languages/English.js: -------------------------------------------------------------------------------- 1 | const English = { 2 | login: { 3 | pageTitle: "Login | Twirrer", 4 | header: "Log in to Twirrer", 5 | emailLabel: "Email address", 6 | passwordLabel: "Password", 7 | logInButton: "Log in", 8 | loading: "Loading...", 9 | question: "Don't have an account?", 10 | link: "Sign up for Twirrer", 11 | }, 12 | signup: { 13 | pageTitle: "Signup | Twirrer", 14 | header: "Sign up for Twirrer", 15 | userNameLabel: "User name", 16 | emailLabel: "Email address", 17 | passwordLabel: "Password", 18 | confirmPasswordLabel: "Confirm Password", 19 | signupButton: "Sign up", 20 | loading: "Loading...", 21 | question: "Already have an account?", 22 | link: "login to Twirrer", 23 | }, 24 | navbar: { 25 | home: "Home", 26 | notifications: "Notifications", 27 | profile: "Profile", 28 | more: "More", 29 | tweetButton: "Tweet", 30 | }, 31 | rightSide: { 32 | WhoToFAdd: "Who to add?", 33 | JoinTwirrer: "Join Twirrer", 34 | JoinTwirrerSub: 35 | "Sign up now or login to get your own personalized timeline!", 36 | }, 37 | home: { 38 | pageTitle: "Home | Twirrer", 39 | title: "Home", 40 | SettingsButton: "More Tweets", 41 | bottomHint: "Cool, you are up to date", 42 | addPostPlaceholder: "What's new?", 43 | addPostButton: "Twittern", 44 | addPostButtonLoading: "Loading..", 45 | pinnedPost: "Pinned Tweet", 46 | }, 47 | postDetails: { 48 | pageTitle: "Tweet | Twirrer", 49 | title: "Tweet", 50 | comments: "Comments", 51 | likes: "Likes", 52 | likesModalTitle: "Liked by", 53 | noCommentHint: "Be the first to comment on this", 54 | replyingTo: "Replying to", 55 | commentPlaceholder: "Write a comment...", 56 | }, 57 | userProfile: { 58 | pageTitle: "Profile | Twirrer", 59 | joined: "Joined", 60 | friends: "Friends", 61 | noPosts: "This user hasn't posted anything yet.", 62 | editProfileButton: "Edit profile", 63 | addFriendButton: "Add friend", 64 | deleteFriendButton: "Delete friend", 65 | modalTitle: "Edit profile", 66 | modalSaveButton: "Save", 67 | modalSaveButtonLoading: "Loading..", 68 | modalBioLabel: "Bio", 69 | modalLocationLabel: "Location", 70 | modalWebsiteLabel: "Website", 71 | friendsModalTitle: "friends", 72 | }, 73 | SettingsButton: { 74 | title: "Settings", 75 | theme: "Theme", 76 | language: "Language", 77 | logoutButton: "Log out", 78 | }, 79 | notifications: { 80 | pageTitle: "Notifications | Twirrer", 81 | title: "Notifications", 82 | likeHint: "liked your Tweet", 83 | commentHint: "commented on your Tweet", 84 | addFriendHint: "added you as a friend", 85 | emptyHint: "You don't have any notifications yet", 86 | }, 87 | page404: { 88 | hint: "Unfortunately, this page does not exist.", 89 | }, 90 | deletePostModal: { 91 | title: "Delete Tweet?", 92 | message: 93 | "This can’t be undone and it will be removed from your profile, the timeline of any accounts that follow you, and from Twitter search results.", 94 | deleteButton: "Delete", 95 | cancelButton: "Cancel", 96 | }, 97 | }; 98 | 99 | export default English; 100 | -------------------------------------------------------------------------------- /Frontend/src/pages/Notifications/Notifications.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .notificationsBox { 4 | height: auto; 5 | width: 45%; 6 | margin-left: 25%; 7 | overflow: auto; 8 | padding-bottom: 80px; 9 | 10 | &__title { 11 | padding: 15px 10px; 12 | height: 53px; 13 | position: fixed; 14 | width: 45%; 15 | z-index: 90; 16 | h1 { 17 | font-size: 19px; 18 | font-weight: 800; 19 | margin: 0; 20 | } 21 | } 22 | 23 | &__Wrapper { 24 | margin-top: 53px; 25 | 26 | .link { 27 | &:hover { 28 | text-decoration: none; 29 | } 30 | } 31 | 32 | &__singleNotBox { 33 | display: flex; 34 | flex-direction: row; 35 | padding: 10px; 36 | 37 | &:hover { 38 | background: transparent; 39 | } 40 | 41 | &__userImageBox { 42 | margin-right: 10px; 43 | max-height: 50px; 44 | position: relative; 45 | &__imageWrapper { 46 | width: 50px; 47 | min-width: 50px; 48 | height: 50px; 49 | border-radius: 50%; 50 | 51 | img { 52 | width: 100%; 53 | height: 100%; 54 | border-radius: 50%; 55 | object-fit: cover; 56 | } 57 | } 58 | 59 | &__iconWrapper { 60 | position: absolute; 61 | bottom: 0; 62 | right: -7px; 63 | i { 64 | border-radius: 50%; 65 | width: 25px; 66 | height: 25px; 67 | font-size: 13px; 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | } 72 | } 73 | } 74 | 75 | &__content { 76 | width: 100%; 77 | &__header { 78 | display: flex; 79 | flex-direction: row; 80 | flex-wrap: wrap; 81 | 82 | p { 83 | font-size: 15px; 84 | font-weight: bold; 85 | margin-bottom: 0; 86 | margin-right: 5px; 87 | word-break: break-word; 88 | } 89 | 90 | span { 91 | font-size: 15px; 92 | } 93 | } 94 | 95 | &__time { 96 | font-size: 15px; 97 | } 98 | } 99 | } 100 | 101 | // style of empty state 102 | &__emptyState { 103 | text-align: center; 104 | padding: 30px 10px 0; 105 | &__image { 106 | width: 100%; 107 | max-width: 100%; 108 | height: 150px; 109 | margin-bottom: 20px; 110 | } 111 | 112 | &__hint { 113 | font-size: 15px; 114 | } 115 | } 116 | } 117 | } 118 | 119 | @media only screen and (max-width: 500px) { 120 | /* For mobile phones: */ 121 | .notificationsBox { 122 | width: 100%; 123 | margin-left: 0; 124 | margin-bottom: 49px; 125 | &__title { 126 | width: 100%; 127 | } 128 | } 129 | } 130 | 131 | @media (min-width: 501px) and (max-width: 991px) { 132 | /* For tablet: */ 133 | .notificationsBox { 134 | width: 75%; 135 | margin-left: 15%; 136 | &__title { 137 | width: 75%; 138 | } 139 | } 140 | } 141 | 142 | @media (min-width: 992px) and (max-width: 1280px) { 143 | /* For small lab: */ 144 | .notificationsBox { 145 | width: 50%; 146 | margin-left: 10%; 147 | &__title { 148 | width: 50%; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Frontend/src/util/Languages/German.js: -------------------------------------------------------------------------------- 1 | const German = { 2 | login: { 3 | pageTitle: "Anmeldung | Twirrer", 4 | header: "Melden Sie sich bei Twirrer an", 5 | emailLabel: "E-Mail Adresse", 6 | passwordLabel: "Passwort", 7 | logInButton: "Anmeldung", 8 | loading: "Wird geladen...", 9 | question: "Haben Sie kein Konto?", 10 | link: "Melden Sie sich für Twitter an", 11 | }, 12 | signup: { 13 | pageTitle: "Signup | Twirrer", 14 | header: "Melden Sie sich für Twirrer an", 15 | userNameLabel: "Nutzername", 16 | emailLabel: "E-Mail Adresse", 17 | passwordLabel: "Passwort", 18 | confirmPasswordLabel: "Bestätige das Passwort", 19 | signupButton: "Anmelden", 20 | loading: "Wird geladen...", 21 | question: "Sie haben bereits ein Konto?", 22 | link: "Melden Sie sich bei Twirrer an", 23 | }, 24 | navbar: { 25 | home: "Startseite", 26 | notifications: "Mitteilungen", 27 | profile: "Profil", 28 | more: "Mehr", 29 | tweetButton: "Twittern", 30 | }, 31 | rightSide: { 32 | WhoToFAdd: "Wer hinzufügen?", 33 | JoinTwirrer: "Beitreten Twirrer", 34 | JoinTwirrerSub: 35 | "Melden Sie sich jetzt an oder melden Sie sich an, um Ihre eigene timeline zu erhalten!", 36 | }, 37 | home: { 38 | pageTitle: "Startseite | Twirrer", 39 | title: "Startseite", 40 | SettingsButton: "Mehr Tweets", 41 | bottomHint: "Cool, du bist auf dem neuesten Stand", 42 | addPostPlaceholder: "Was gibt's Neues?", 43 | addPostButton: "Twittern", 44 | addPostButtonLoading: "Wird geladen..", 45 | pinnedPost: "Angehefteter Tweet", 46 | }, 47 | postDetails: { 48 | pageTitle: "Twittern | Twitter", 49 | title: "Twittern", 50 | comments: "Kommentare", 51 | likes: "„Gefällt mir“-Angaben", 52 | likesModalTitle: "Gefällt", 53 | noCommentHint: "Sei der erste, der dies kommentiert", 54 | replyingTo: "Antwort an", 55 | commentPlaceholder: "Schreibe einen Kommentar...", 56 | }, 57 | userProfile: { 58 | pageTitle: "Profil | Twirrer", 59 | joined: "Beitritt", 60 | friends: "Freunde", 61 | noPosts: "Dieser Benutzer hat noch nichts gepostet.", 62 | editProfileButton: "Profil bearbeiten", 63 | addFriendButton: "Freund hinzufügen", 64 | deleteFriendButton: "Freund löschen", 65 | modalTitle: "Profil bearbeiten", 66 | modalSaveButton: "Speichern", 67 | modalSaveButtonLoading: "Wird geladen..", 68 | modalBioLabel: "Biografie", 69 | modalLocationLabel: "Standort", 70 | modalWebsiteLabel: "Website", 71 | friendsModalTitle: "freunde", 72 | }, 73 | SettingsButton: { 74 | title: "Einstellung", 75 | theme: "Thema", 76 | language: "Sprache", 77 | logoutButton: "Abmelden", 78 | }, 79 | notifications: { 80 | pageTitle: "Mitteilungen | Twirrer", 81 | title: "Mitteilungen", 82 | likeHint: "gefällt dein Tweet", 83 | commentHint: "kommentierte Ihren Tweet", 84 | addFriendHint: "Hinzugefügt Sie als Freund", 85 | emptyHint: "Sie haben noch keine Mitteilungen", 86 | }, 87 | page404: { 88 | hint: "Leider existiert diese Seite nicht.", 89 | }, 90 | deletePostModal: { 91 | title: "Tweet löschen?", 92 | message: 93 | "Das kann nicht rückgängig gemacht werden und er wird aus deinem Profil, der Timeline aller Accounts, die dir folgen, und den Twitter Suchergebnissen entfernt.", 94 | deleteButton: "Löschen", 95 | cancelButton: "Abbrechen", 96 | }, 97 | }; 98 | 99 | export default German; 100 | -------------------------------------------------------------------------------- /Frontend/src/parts/AddNewPost/AddNewPost.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .addNewPost { 4 | display: flex; 5 | flex-direction: row; 6 | 7 | &__leftSide { 8 | &__imageBox { 9 | width: 50px; 10 | min-width: 50px; 11 | height: 50px; 12 | border-radius: 50%; 13 | margin-right: 10px; 14 | img { 15 | width: 100%; 16 | height: 100%; 17 | border-radius: 50%; 18 | box-sizing: border-box; 19 | object-fit: cover; 20 | } 21 | } 22 | } 23 | 24 | &__rightSide { 25 | width: 100%; 26 | 27 | // line1 28 | &__inputBox { 29 | width: 100%; 30 | margin-bottom: 10px; 31 | margin-top: 10px; 32 | ::-webkit-scrollbar { 33 | width: 0px; /* Remove scrollbar space */ 34 | background: transparent; /* Optional: just make scrollbar invisible */ 35 | } 36 | /* Optional: show position indicator in red */ 37 | ::-webkit-scrollbar-thumb { 38 | background: $mainColor; 39 | } 40 | 41 | &__textarea { 42 | width: 100%; 43 | resize: none; 44 | padding: 5px 10px; 45 | padding-left: 0; 46 | font-size: 19px; 47 | font-weight: 400; 48 | overflow-x: hidden; 49 | background-color: transparent; 50 | 51 | &:focus { 52 | outline: 0; 53 | } 54 | 55 | &::placeholder { 56 | font-size: 19px; 57 | font-weight: 400; 58 | color: #8899A6 59 | } 60 | } 61 | } 62 | 63 | // line2 64 | &__postImageBox { 65 | position: relative; 66 | 67 | &__imageWrapper { 68 | border-radius: 15px; 69 | width: 100%; 70 | height: auto; 71 | max-height: 300px; 72 | overflow: hidden; 73 | img { 74 | width: 100%; 75 | max-width: 100%; 76 | max-height: 100%; 77 | object-fit: cover; 78 | } 79 | } 80 | 81 | &__iconBox { 82 | position: absolute; 83 | left: 17px; 84 | top: 12px; 85 | display: flex; 86 | flex-direction: revert; 87 | justify-content: center; 88 | align-items: center; 89 | 90 | &:hover { 91 | cursor: pointer; 92 | } 93 | 94 | &__background { 95 | position: absolute; 96 | width: 30px; 97 | height: 30px; 98 | border-radius: 50%; 99 | } 100 | } 101 | } 102 | 103 | // line3 104 | &__buttonsBox { 105 | display: flex; 106 | flex-direction: row; 107 | justify-content: space-between; 108 | align-items: center; 109 | margin-top: 20px; 110 | 111 | &__imageUpload { 112 | input { 113 | display: none; 114 | } 115 | 116 | .button { 117 | position: relative; 118 | display: flex; 119 | justify-content: center; 120 | align-items: center; 121 | width: 20px; 122 | height: 20px; 123 | 124 | &:hover { 125 | cursor: pointer; 126 | } 127 | 128 | &:hover > .background { 129 | visibility: visible; 130 | } 131 | 132 | i { 133 | position: absolute; 134 | font-size: 20px; 135 | } 136 | 137 | .background { 138 | position: absolute; 139 | width: 33px; 140 | height: 33px; 141 | border-radius: 50%; 142 | visibility: hidden; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/EditCoverImageButton/EditCoverImageButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | // style 4 | import "./EditCoverImageButton.scss"; 5 | 6 | // api service 7 | import UserService from "../../../services/UserService"; 8 | 9 | // components 10 | import Spinner from "../../Spinner/Spinner"; 11 | 12 | // context (global state) 13 | import UserContext from "../../../context/UserContext"; 14 | import { ThemeContext } from "../../../context/ThemeContext"; 15 | import UserProfileContext from "../../../context/UserProfileContext"; 16 | 17 | const EditCoverImageButton = () => { 18 | // userData context 19 | const { userData } = useContext(UserContext); 20 | 21 | // theme context 22 | const { isLightTheme, light, dark } = useContext(ThemeContext); 23 | const theme = isLightTheme ? light : dark; 24 | 25 | // user profile data context 26 | const { userProfileData, setUserProfileData } = 27 | useContext(UserProfileContext); 28 | 29 | // local state 30 | const [loading, setLoading] = useState(false); 31 | 32 | // On selecting new image, upload this image directly to the server 33 | const handleImageChange = (event) => { 34 | const image = event.target.files[0]; 35 | const formData = new FormData(); 36 | if (image.name) { 37 | formData.append("image", image, image.name); 38 | // send the image to server 39 | setLoading(true); 40 | UserService.uploadCoverImage(formData, userData.token) 41 | .then((res) => { 42 | let url = res.data.imageURL; 43 | 44 | // 1- update user data in UserProfile page (state), to show new profile image 45 | setUserProfileData({ 46 | friends: userProfileData.friends, 47 | posts: userProfileData.posts, 48 | user: { 49 | ...userProfileData.user, 50 | coverPicture: url, 51 | }, 52 | }); 53 | 54 | // update user profile data in cache also with new cover image 55 | let cachedCurrentUser = JSON.parse( 56 | window.sessionStorage.getItem(userData.user.credentials.userName) 57 | ); 58 | if (cachedCurrentUser) { 59 | window.sessionStorage.setItem( 60 | userData.user.credentials.userName, 61 | JSON.stringify({ 62 | friends: cachedCurrentUser.friends, 63 | posts: cachedCurrentUser.posts, 64 | user: { 65 | ...cachedCurrentUser.user, 66 | coverPicture: url, 67 | }, 68 | }) 69 | ); 70 | } 71 | }) 72 | .then(() => setLoading(false)) 73 | .catch((err) => { 74 | console.log(err); 75 | setLoading(false); 76 | }); 77 | } 78 | }; 79 | 80 | // on click, fire 'click event' of input (type file) 81 | const handleEditPicture = () => { 82 | const fileInput = document.getElementById("coverImageInput"); 83 | fileInput.click(); 84 | }; 85 | 86 | return loading ? ( 87 |
88 | 89 |
90 | ) : ( 91 |
92 | handleImageChange(event)} 97 | /> 98 | handleEditPicture()} 101 | style={{ border: `2px solid ${theme.background}` }} 102 | > 103 |
104 | ); 105 | }; 106 | 107 | export default EditCoverImageButton; 108 | -------------------------------------------------------------------------------- /Frontend/src/services/UserService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const UserService = { 4 | loginUser: async function (userData) { 5 | try { 6 | const response = await axios.post("/login", userData); 7 | return response; 8 | } catch (error) { 9 | throw error; 10 | } 11 | }, 12 | 13 | signupUser: async function (userData) { 14 | try { 15 | const response = await axios.post("/signup", userData); 16 | return response; 17 | } catch (error) { 18 | throw error; 19 | } 20 | }, 21 | 22 | logoutUser: async function () { 23 | try { 24 | const response = await axios.get("/logout"); 25 | return response; 26 | } catch (error) { 27 | throw error; 28 | } 29 | }, 30 | 31 | getAuthenticatedUser: async function (token) { 32 | try { 33 | const response = await axios.get("/getAuthenticatedUser", { 34 | headers: { Authorization: `Bearer ${token}` }, 35 | }); 36 | return response; 37 | } catch (error) { 38 | throw error; 39 | } 40 | }, 41 | 42 | getUserDetails: async function (userName) { 43 | try { 44 | const response = await axios.get(`/user/${userName}/getUserDetails`); 45 | return response; 46 | } catch (error) { 47 | throw error; 48 | } 49 | }, 50 | 51 | uploadProfileImage: async function (fromData, token) { 52 | try { 53 | const response = await axios.post("/uploadProfileImage", fromData, { 54 | headers: { Authorization: `Bearer ${token}` }, 55 | }); 56 | return response; 57 | } catch (error) { 58 | throw error; 59 | } 60 | }, 61 | 62 | uploadCoverImage: async function (fromData, token) { 63 | try { 64 | const response = await axios.post("/uploadCoverImage", fromData, { 65 | headers: { Authorization: `Bearer ${token}` }, 66 | }); 67 | return response; 68 | } catch (error) { 69 | throw error; 70 | } 71 | }, 72 | 73 | addUserDetails: async function (data, token) { 74 | try { 75 | const response = await axios.post("/addUserDetails", data, { 76 | headers: { Authorization: `Bearer ${token}` }, 77 | }); 78 | return response; 79 | } catch (error) { 80 | throw error; 81 | } 82 | }, 83 | 84 | addFriend: async function (userName, token) { 85 | try { 86 | const response = await axios.get(`/user/${userName}/addFriend`, { 87 | headers: { Authorization: `Bearer ${token}` }, 88 | }); 89 | return response; 90 | } catch (error) { 91 | throw error; 92 | } 93 | }, 94 | 95 | unFriend: async function (userName, token) { 96 | try { 97 | const response = await axios.get(`/user/${userName}/unFriend`, { 98 | headers: { Authorization: `Bearer ${token}` }, 99 | }); 100 | return response; 101 | } catch (error) { 102 | throw error; 103 | } 104 | }, 105 | 106 | usersToAdd: async function (token) { 107 | try { 108 | const response = await axios.get("/usersToAdd", { 109 | headers: { Authorization: `Bearer ${token}` }, 110 | }); 111 | return response; 112 | } catch (error) { 113 | throw error; 114 | } 115 | }, 116 | 117 | markNotificationsAsRead: async function (notificationsIds, token) { 118 | try { 119 | const response = await axios.post( 120 | `/markNotificationsAsRead`, 121 | notificationsIds, 122 | { 123 | headers: { Authorization: `Bearer ${token}` }, 124 | } 125 | ); 126 | return response; 127 | } catch (error) { 128 | throw error; 129 | } 130 | }, 131 | }; 132 | 133 | export default UserService; 134 | -------------------------------------------------------------------------------- /Frontend/src/components/LikesModal/LikesModal.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .postDetails__post__content__counters__likes { 4 | &:hover { 5 | cursor: pointer; 6 | text-decoration: underline; 7 | color: inherit; 8 | } 9 | } 10 | 11 | .likesBox { 12 | &__like { 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 10px; 18 | 19 | &__leftSide { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | width: calc(100% - 155px); 24 | 25 | &__userImageBox { 26 | width: 50px; 27 | height: 50px; 28 | min-width: 50px; 29 | border-radius: 50%; 30 | margin-right: 10px; 31 | 32 | img { 33 | width: 100%; 34 | height: 100%; 35 | border-radius: 50%; 36 | object-fit: cover; 37 | } 38 | } 39 | 40 | &__userName { 41 | width: calc(100% - 60px); 42 | overflow: hidden; 43 | 44 | a { 45 | font-size: 15px; 46 | font-weight: bold; 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | margin-right: 5px; 51 | display: block; 52 | } 53 | 54 | p { 55 | font-size: 15px; 56 | font-weight: 400; 57 | white-space: nowrap; 58 | overflow: hidden; 59 | text-overflow: ellipsis; 60 | margin: 0; 61 | margin-right: 5px; 62 | margin-top: -5px; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /* Modal edit */ 70 | .likes__main__modal { 71 | padding-right: 0 !important; 72 | .modal-content { 73 | border: 0; 74 | background: transparent; 75 | min-height: 500px; 76 | 77 | .modal-body { 78 | border-bottom-right-radius: 20px; 79 | border-bottom-left-radius: 20px; 80 | padding: 0 !important; 81 | min-height: 200px; 82 | padding-bottom: 20px !important; 83 | } 84 | } 85 | &__header { 86 | display: flex; 87 | flex-direction: row; 88 | justify-content: space-between; 89 | padding-top: 10px; 90 | padding-bottom: 10px; 91 | align-items: flex-end; 92 | border-top-left-radius: 20px !important; 93 | border-top-right-radius: 20px !important; 94 | 95 | .left { 96 | display: flex; 97 | flex-direction: row; 98 | } 99 | 100 | &__iconBox { 101 | position: relative; 102 | margin-right: 25px; 103 | 104 | &:hover > &__background { 105 | display: block; 106 | } 107 | 108 | &:hover { 109 | color: $mainColor; 110 | cursor: pointer; 111 | } 112 | 113 | &__background { 114 | position: absolute; 115 | top: -3px; 116 | left: -10px; 117 | width: 30px; 118 | height: 30px; 119 | border-radius: 50%; 120 | display: none; 121 | } 122 | } 123 | 124 | &__title { 125 | font-size: 18px; 126 | font-weight: 800; 127 | margin-bottom: 0; 128 | line-height: 1.5; 129 | } 130 | 131 | &__saveButton { 132 | font-size: 14px; 133 | font-weight: bold; 134 | height: 28px; 135 | padding: 0 14px; 136 | 137 | &:focus { 138 | outline: 0; 139 | } 140 | } 141 | } 142 | } 143 | 144 | @media only screen and (max-width: 500px) { 145 | // mobile screen 146 | .modal-dialog { 147 | margin: 0 !important; 148 | 149 | .modal-content { 150 | min-height: 100vh; 151 | 152 | .likes__main__modal__header { 153 | border-top-left-radius: 0 !important; 154 | border-top-right-radius: 0 !important; 155 | } 156 | 157 | .modal-body { 158 | border-bottom-right-radius: 0; 159 | border-bottom-left-radius: 0; 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Frontend/src/components/FriendsModal/FriendsModal.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .userProfile__main__userDetails__userData__friends { 4 | &:hover { 5 | cursor: pointer; 6 | text-decoration: underline; 7 | color: inherit; 8 | } 9 | } 10 | 11 | .friendsBox { 12 | &__friend { 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 10px; 18 | 19 | &__leftSide { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | width: calc(100% - 155px); 24 | 25 | &__userImageBox { 26 | width: 50px; 27 | height: 50px; 28 | min-width: 50px; 29 | border-radius: 50%; 30 | margin-right: 10px; 31 | 32 | img { 33 | width: 100%; 34 | height: 100%; 35 | border-radius: 50%; 36 | object-fit: cover; 37 | } 38 | } 39 | 40 | &__userName { 41 | width: calc(100% - 60px); 42 | overflow: hidden; 43 | 44 | a { 45 | font-size: 15px; 46 | font-weight: bold; 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | margin-right: 5px; 51 | display: block; 52 | } 53 | 54 | p { 55 | font-size: 15px; 56 | font-weight: 400; 57 | white-space: nowrap; 58 | overflow: hidden; 59 | text-overflow: ellipsis; 60 | margin: 0; 61 | margin-right: 5px; 62 | margin-top: -5px; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /* Modal edit */ 70 | .friends__main__modal { 71 | padding-left: 0 !important; 72 | .modal-content { 73 | border: 0; 74 | background: transparent; 75 | min-height: 500px; 76 | 77 | .modal-body { 78 | border-bottom-right-radius: 20px; 79 | border-bottom-left-radius: 20px; 80 | padding: 0 !important; 81 | min-height: 200px; 82 | padding-bottom: 20px !important; 83 | } 84 | } 85 | &__header { 86 | display: flex; 87 | flex-direction: row; 88 | justify-content: space-between; 89 | padding-top: 10px; 90 | padding-bottom: 10px; 91 | align-items: flex-end; 92 | border-top-left-radius: 20px !important; 93 | border-top-right-radius: 20px !important; 94 | 95 | .left { 96 | display: flex; 97 | flex-direction: row; 98 | width: 100%; 99 | } 100 | 101 | &__iconBox { 102 | position: relative; 103 | margin-right: 25px; 104 | 105 | &:hover > &__background { 106 | display: block; 107 | } 108 | 109 | &:hover { 110 | color: $mainColor; 111 | cursor: pointer; 112 | } 113 | 114 | &__background { 115 | position: absolute; 116 | top: -3px; 117 | left: -10px; 118 | width: 30px; 119 | height: 30px; 120 | border-radius: 50%; 121 | display: none; 122 | } 123 | } 124 | 125 | &__title { 126 | font-size: 18px; 127 | font-weight: 800; 128 | margin-bottom: 0; 129 | line-height: 1.5; 130 | white-space: nowrap; 131 | overflow: hidden; 132 | text-overflow: ellipsis; 133 | } 134 | 135 | &__saveButton { 136 | font-size: 14px; 137 | font-weight: bold; 138 | height: 28px; 139 | padding: 0 14px; 140 | 141 | &:focus { 142 | outline: 0; 143 | } 144 | } 145 | } 146 | } 147 | 148 | @media only screen and (max-width: 500px) { 149 | // mobile screen 150 | .modal-dialog { 151 | margin: 0 !important; 152 | 153 | .modal-content { 154 | min-height: 100vh; 155 | 156 | .friends__main__modal__header { 157 | border-top-left-radius: 0 !important; 158 | border-top-right-radius: 0 !important; 159 | } 160 | .modal-body { 161 | border-bottom-right-radius: 0; 162 | border-bottom-left-radius: 0; 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Backend/DB_schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this schema will not be user in thi app, 3 | * this is just to give you quick look of how the database look. 4 | */ 5 | 6 | let DB_schema = { 7 | users: [{ 8 | userId: 'dh23ggj5h32g543j5gf43', 9 | email: 'user@email.com', 10 | userName: 'user', 11 | createdAt: '2019-03-15T10:59:52.798Z', 12 | profilePicture: 'https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media', 13 | coverPicture: 'https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media', 14 | bio: 'Hello, my name is user, nice to meet you', 15 | website: 'https://user.com', 16 | location: 'Berlin, DE', 17 | friendsCount: 0 18 | }], 19 | posts: [{ 20 | postContent: "Hi friends!", 21 | postImage: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media", 22 | createdAt: "2020-06-16T20:46:50.192Z", 23 | likeCount: 5, 24 | commentCount: 3, 25 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media", 26 | userId: "563xm6hNAuMmmdKb5e54jvynNiR2", 27 | userName: "user" 28 | }], 29 | comments: [{ 30 | userName: 'user', 31 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 32 | postId: 'kdjsfgdksuufhgkdsufky', 33 | commentContent: 'nice one mate!', 34 | createdAt: '2019-03-15T10:59:52.798Z' 35 | }], 36 | notifications: [{ 37 | recipient: 'user', 38 | sender: 'john', 39 | read: 'true | false', 40 | senderProfilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 41 | postId: 'kdjsfgdksuufhgkdsufky', 42 | type: 'like | comment', 43 | createdAt: '2019-03-15T10:59:52.798Z' 44 | }], 45 | likes: [{ 46 | postId: "kdjsfgdksuufhgkdsufky", 47 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 48 | userName: "user" 49 | }], 50 | friends: [{ 51 | user: { 52 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 53 | userName: "user" 54 | }, 55 | user2: { 56 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 57 | userName: "user2" 58 | } 59 | }] 60 | }; 61 | ///////////////////////////////////////////////////////////////////////////////////////// 62 | ///////////////////////////////////////////////////////////////////////////////////////// 63 | ///////////////////////////////////////////////////////////////////////////////////////// 64 | 65 | // this data will retrieved for each user, will be globally 66 | const userDetails = { 67 | // context api data 68 | credentials: { 69 | userId: 'dh23ggj5h32g543j5gf43', 70 | email: 'user@email.com', 71 | userName: 'user', 72 | createdAt: '2019-03-15T10:59:52.798Z', 73 | profilePicture: 'https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media', 74 | coverPicture: 'https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/750854615984.png?alt=media', 75 | bio: 'Hello, my name is user, nice to meet you', 76 | website: 'https://user.com', 77 | location: 'Berlin, DE', 78 | friendsCount: 0 79 | }, 80 | // retrieve likes that this user have made, to colored the heart of posts were liked by this user 81 | likes: [{ 82 | userName: 'user', 83 | postId: 'hh7O5oWfWucVzGbHH2pa' 84 | }, 85 | { 86 | userName: 'user', 87 | postId: '3IOnFoQexRcofs5OhBXO' 88 | } 89 | ], 90 | 91 | // retrieve friends of this user 92 | friends: [{ 93 | user: { 94 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 95 | userName: "user" 96 | }, 97 | user2: { 98 | profilePicture: "https://firebasestorage.googleapis.com/v0/b/twirrer-app.appspot.com/o/394413876440.jpeg?alt=media", 99 | userName: "user2" 100 | } 101 | }] 102 | }; -------------------------------------------------------------------------------- /Frontend/src/components/CommentCard/CommentCard.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // style 5 | import "./CommentCard.scss"; 6 | 7 | // libraries 8 | import moment from "moment-twitter"; 9 | import Linkify from "react-linkify"; 10 | 11 | // context (global state) 12 | import { ThemeContext } from "../../context/ThemeContext"; 13 | import { LanguageContext } from "../../context/LanguageContext"; 14 | import CheckVerifiedUserName from "../CheckVerifiedUserName"; 15 | import UserContext from "../../context/UserContext"; 16 | 17 | const CommentCard = ({ comment, authorName }) => { 18 | // ******* start global state ******* // 19 | // theme context 20 | const { isLightTheme, light, dark } = useContext(ThemeContext); 21 | const theme = isLightTheme ? light : dark; 22 | 23 | // language context 24 | const { isEnglish, english, german } = useContext(LanguageContext); 25 | var language = isEnglish ? english : german; 26 | 27 | // user context 28 | const { userData } = useContext(UserContext); 29 | 30 | // ******* end global state ******* // 31 | 32 | var arabic = /[\u0600-\u06FF]/; 33 | 34 | const ProfilePicture = userData.isAuth 35 | ? comment.userName === userData.user.credentials.userName 36 | ? userData.user.credentials.profilePicture 37 | : comment.profilePicture 38 | : comment.profilePicture; 39 | 40 | return ( 41 |
47 |
48 |
49 | 50 | profile 55 | 56 |
57 |
58 |
59 |
60 |
61 | 69 | 70 | 71 | 77 | {moment(comment.createdAt).twitterShort()} 78 | 79 |
80 |
81 |
82 |
83 | 89 | {language.postDetails.replyingTo} 90 | 91 | 98 | {"@"} 99 | 100 | 101 |
102 |
103 |
113 | {comment.commentContent} 114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export default CommentCard; 121 | -------------------------------------------------------------------------------- /Frontend/src/components/PostCard/PostCard.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .postCard { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: flex-start; 7 | padding: 10px 10px 0; 8 | cursor: pointer; 9 | 10 | &__userImage { 11 | &__wrapper { 12 | width: 50px; 13 | height: 50px; 14 | overflow: hidden; 15 | border-radius: 50%; 16 | 17 | &__image { 18 | width: 100%; 19 | height: 100%; 20 | border-radius: 50%; 21 | object-fit: cover; 22 | } 23 | } 24 | } 25 | 26 | &__content { 27 | width: 100%; 28 | padding-left: 10px; 29 | overflow: hidden; 30 | 31 | &__line1 { 32 | font-size: 15px; 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: space-between; 36 | 37 | &__box { 38 | width: 80%; 39 | display: flex; 40 | flex-direction: row; 41 | } 42 | 43 | &__userName { 44 | font-weight: 700; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | margin-right: 5px; 49 | } 50 | 51 | &__time { 52 | font-size: 13px; 53 | white-space: nowrap; 54 | line-height: 1.8; 55 | } 56 | 57 | &__delete { 58 | position: relative; 59 | display: flex; 60 | justify-content: flex-end; 61 | &:hover { 62 | cursor: pointer; 63 | } 64 | &:hover > .background { 65 | visibility: visible; 66 | } 67 | i { 68 | position: absolute; 69 | right: 8px; 70 | top: 8px; 71 | display: inline-block; 72 | } 73 | 74 | .background { 75 | position: absolute; 76 | width: 30px; 77 | height: 30px; 78 | border-radius: 50%; 79 | visibility: hidden; 80 | } 81 | } 82 | } 83 | 84 | &__line2 { 85 | font-size: 15px; 86 | margin-bottom: 10px; 87 | line-height: 1.3em; 88 | overflow: hidden; 89 | text-overflow: ellipsis; 90 | display: -webkit-box; 91 | -webkit-line-clamp: 12; 92 | -webkit-box-orient: vertical; 93 | white-space: pre-line; 94 | } 95 | 96 | &__line3 { 97 | width: 100%; 98 | height: auto; 99 | max-height: 300px; 100 | overflow: hidden; 101 | border-radius: 15px; 102 | 103 | 104 | &__image { 105 | width: 100%; 106 | max-width: 100%; 107 | max-height: 100%; 108 | object-fit: cover; 109 | } 110 | } 111 | 112 | &__line4 { 113 | font-size: 13px; 114 | display: flex; 115 | flex-direction: row; 116 | justify-content: space-around; 117 | margin-top: 10px; 118 | padding-bottom: 10px; 119 | 120 | i { 121 | font-size: 15px; 122 | margin-right: 5px; 123 | } 124 | 125 | &__comment { 126 | display: flex; 127 | &:hover > .comment__box > .comment__background { 128 | display: block; 129 | } 130 | &:hover { 131 | color: $mainColor; 132 | cursor: pointer; 133 | } 134 | .comment__box { 135 | position: relative; 136 | } 137 | 138 | .comment__background { 139 | position: absolute; 140 | top: -7px; 141 | left: -7px; 142 | width: 30px; 143 | height: 30px; 144 | border-radius: 50%; 145 | display: none; 146 | } 147 | } 148 | 149 | &__like { 150 | display: flex; 151 | &:hover > .like__box > .like__background { 152 | display: block; 153 | } 154 | &:hover > .likesCount{ 155 | color: $red !important; 156 | cursor: pointer; 157 | } 158 | &:hover > .like__box > i { 159 | color: $red !important; 160 | } 161 | .like__box { 162 | position: relative; 163 | } 164 | 165 | .like__background { 166 | position: absolute; 167 | top: -7px; 168 | left: -7px; 169 | width: 30px; 170 | height: 30px; 171 | border-radius: 50%; 172 | display: none; 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /Frontend/src/parts/PinnedPost/PinnedPost.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .PinnedPostCard { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: flex-start; 7 | padding: 10px 10px 0; 8 | cursor: pointer; 9 | 10 | .loader__box{ 11 | margin: 0 auto; 12 | } 13 | 14 | &__userImage { 15 | &__wrapper { 16 | width: 50px; 17 | height: 50px; 18 | overflow: hidden; 19 | border-radius: 50%; 20 | 21 | &__image { 22 | width: 100%; 23 | height: 100%; 24 | border-radius: 50%; 25 | object-fit: cover; 26 | } 27 | } 28 | } 29 | 30 | &__content { 31 | width: 100%; 32 | padding-left: 10px; 33 | overflow: hidden; 34 | 35 | &__line1 { 36 | font-size: 15px; 37 | display: flex; 38 | flex-direction: row; 39 | justify-content: space-between; 40 | 41 | &__box { 42 | width: 80%; 43 | display: flex; 44 | flex-direction: row; 45 | } 46 | 47 | &__userName { 48 | font-weight: 700; 49 | white-space: nowrap; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | margin-right: 5px; 53 | } 54 | 55 | &__time { 56 | font-size: 13px; 57 | white-space: nowrap; 58 | line-height: 1.8; 59 | } 60 | 61 | &__delete { 62 | position: relative; 63 | display: flex; 64 | justify-content: flex-end; 65 | &:hover { 66 | cursor: pointer; 67 | } 68 | &:hover > .background { 69 | visibility: visible; 70 | } 71 | i { 72 | position: absolute; 73 | right: 8px; 74 | top: 8px; 75 | display: inline-block; 76 | } 77 | 78 | .background { 79 | position: absolute; 80 | width: 30px; 81 | height: 30px; 82 | border-radius: 50%; 83 | visibility: hidden; 84 | } 85 | } 86 | } 87 | 88 | &__pinnedHint { 89 | font-size: 13px; 90 | margin-top: -5px; 91 | margin-bottom: 5px; 92 | } 93 | 94 | &__line2 { 95 | font-size: 15px; 96 | margin-bottom: 10px; 97 | line-height: 1.3em; 98 | overflow: hidden; 99 | text-overflow: ellipsis; 100 | display: -webkit-box; 101 | -webkit-line-clamp: 12; 102 | -webkit-box-orient: vertical; 103 | white-space: pre-line; 104 | } 105 | 106 | &__line3 { 107 | width: 100%; 108 | height: auto; 109 | max-height: 300px; 110 | overflow: hidden; 111 | border-radius: 15px; 112 | 113 | &__image { 114 | width: 100%; 115 | max-width: 100%; 116 | max-height: 100%; 117 | object-fit: cover; 118 | } 119 | } 120 | 121 | &__line4 { 122 | font-size: 13px; 123 | display: flex; 124 | flex-direction: row; 125 | justify-content: space-around; 126 | margin-top: 10px; 127 | padding-bottom: 10px; 128 | 129 | i { 130 | font-size: 15px; 131 | margin-right: 10px; 132 | } 133 | 134 | &__comment { 135 | display: flex; 136 | &:hover > .comment__box > .comment__background { 137 | display: block; 138 | } 139 | &:hover { 140 | color: $mainColor; 141 | cursor: pointer; 142 | } 143 | .comment__box { 144 | position: relative; 145 | } 146 | 147 | .comment__background { 148 | position: absolute; 149 | top: -7px; 150 | left: -7px; 151 | width: 30px; 152 | height: 30px; 153 | border-radius: 50%; 154 | display: none; 155 | } 156 | } 157 | 158 | &__like { 159 | display: flex; 160 | &:hover > .like__box > .like__background { 161 | display: block; 162 | } 163 | &:hover { 164 | color: $red !important; 165 | cursor: pointer; 166 | } 167 | .like__box { 168 | position: relative; 169 | } 170 | 171 | .like__background { 172 | position: absolute; 173 | top: -7px; 174 | left: -7px; 175 | width: 30px; 176 | height: 30px; 177 | border-radius: 50%; 178 | display: none; 179 | } 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Frontend/src/parts/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables"; 2 | 3 | .Navbar { 4 | position: relative; 5 | z-index: 100; 6 | &__main { 7 | height: 100vh; 8 | width: 25%; 9 | position: fixed; 10 | } 11 | } 12 | 13 | .Navbar__box { 14 | padding: 20px 10% 20px 30%; 15 | 16 | &__logo { 17 | display: flex; 18 | justify-content: flex-start; 19 | margin-bottom: 20px; 20 | padding-left: 5px; 21 | 22 | &__box { 23 | width: 30px; 24 | height: 30px; 25 | } 26 | } 27 | 28 | &__tabs { 29 | .Navbar__box__tab { 30 | margin-bottom: 20px; 31 | a { 32 | text-decoration: none; 33 | padding-left: 5px; 34 | &:hover { 35 | background: $secondaryColor; 36 | border-radius: $radius; 37 | padding: 12px 15px 12px 5px; 38 | } 39 | &:hover .Navbar__box__tab__icon > i { 40 | color: $mainColor !important; 41 | } 42 | &:hover .Navbar__box__tab__text { 43 | color: $mainColor !important; 44 | } 45 | } 46 | &__icon { 47 | width: 30px; 48 | text-align: center; 49 | font-size: 22px; 50 | margin-right: 15px; 51 | display: inline-block; 52 | position: relative; 53 | z-index: 1; 54 | 55 | span { 56 | position: absolute; 57 | top: -6px; 58 | right: -4px; 59 | font-size: 12px; 60 | display: inline-block; 61 | border-radius: 50%; 62 | width: 18px; 63 | height: 18px; 64 | box-sizing: content-box; 65 | text-align: center; 66 | } 67 | } 68 | &__text { 69 | font-size: 19px; 70 | font-weight: 700; 71 | display: inline-block; 72 | } 73 | } 74 | .--twitternButton { 75 | .smallButton { 76 | display: none; 77 | border-radius: 50%; 78 | width: 100%; 79 | height: 46px; 80 | } 81 | } 82 | } 83 | } 84 | 85 | .buttons__box { 86 | display: flex; 87 | justify-content: space-around; 88 | } 89 | 90 | @media only screen and (max-width: 500px) { 91 | /* For mobile phones: */ 92 | .Navbar__main { 93 | width: 0; 94 | display: none; 95 | } 96 | 97 | .--twitternButton { 98 | .bigButton { 99 | display: none; 100 | } 101 | } 102 | 103 | .--twitternButton { 104 | .smallButton { 105 | display: block !important; 106 | } 107 | } 108 | } 109 | 110 | @media (min-width: 501px) and (max-width: 991px) { 111 | /* For tablet: */ 112 | .Navbar__main { 113 | width: 15%; 114 | } 115 | .Navbar__box { 116 | padding: 20px 30% 20px 30%; 117 | display: flex; 118 | flex-direction: column; 119 | align-items: flex-end; 120 | &__logo { 121 | padding-right: 8px; 122 | } 123 | } 124 | 125 | .Navbar__box__tabs { 126 | .Navbar__box__tab { 127 | a { 128 | padding: 12px 8px; 129 | 130 | &:hover { 131 | background: rgba(29, 161, 242, 0.1); 132 | border-radius: 9999px; 133 | padding: 12px 8px; 134 | } 135 | } 136 | &__icon { 137 | margin-right: 0; 138 | } 139 | &__text { 140 | display: none; 141 | } 142 | } 143 | } 144 | 145 | .--twitternButton { 146 | .bigButton { 147 | display: none; 148 | } 149 | } 150 | .--twitternButton { 151 | .smallButton { 152 | display: block !important; 153 | } 154 | } 155 | } 156 | 157 | @media (min-width: 992px) and (max-width: 1280px) { 158 | /* For small lab: */ 159 | .Navbar__main { 160 | width: 10%; 161 | } 162 | 163 | .Navbar__box { 164 | padding: 20px 20% 20px 50%; 165 | display: flex; 166 | flex-direction: column; 167 | align-items: flex-end; 168 | &__logo { 169 | padding-right: 8px; 170 | } 171 | } 172 | 173 | .Navbar__box__tabs { 174 | .Navbar__box__tab { 175 | a { 176 | padding: 12px 8px; 177 | 178 | &:hover { 179 | background: rgba(29, 161, 242, 0.1); 180 | border-radius: 9999px; 181 | padding: 12px 8px; 182 | } 183 | } 184 | &__icon { 185 | margin-right: 0; 186 | } 187 | &__text { 188 | display: none; 189 | } 190 | } 191 | } 192 | 193 | .--twitternButton { 194 | .bigButton { 195 | display: none; 196 | } 197 | } 198 | .--twitternButton { 199 | .smallButton { 200 | display: block !important; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Frontend/src/components/PostCard/PostCard.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | // style 4 | import "./PostCard.scss"; 5 | 6 | // libraries 7 | import ImageModal from "../ImageModal/ImageModal"; 8 | import moment from "moment-twitter"; 9 | import Linkify from "react-linkify"; 10 | 11 | // context (global state) 12 | import { ThemeContext } from "../../context/ThemeContext"; 13 | import UserContext from "../../context/UserContext"; 14 | 15 | // component 16 | import DeletePostButton from "../Buttons/DeletePostButton/DeletePostButton"; 17 | import LikeButton from "../Buttons/LikeButton"; 18 | import CommentButton from "../Buttons/CommentButton"; 19 | import CheckVerifiedUserName from "../CheckVerifiedUserName"; 20 | 21 | const PostCard = ({ post }) => { 22 | // ******* start global state ******* // 23 | // theme context 24 | const { isLightTheme, light, dark } = useContext(ThemeContext); 25 | const theme = isLightTheme ? light : dark; 26 | 27 | // user context 28 | const { userData } = useContext(UserContext); 29 | // ******* end global state ******* // 30 | 31 | // local state 32 | const [isHover, setHover] = useState(false); 33 | 34 | var arabic = /[\u0600-\u06FF]/; 35 | 36 | // add dynamic style on hover on post card 37 | const toggleHover = () => { 38 | setHover(!isHover); 39 | }; 40 | 41 | // dynamic style on hover 42 | var linkStyle = { borderBottom: `1px solid ${theme.border}` }; 43 | if (isHover) { 44 | if (isLightTheme) { 45 | linkStyle.backgroundColor = "#F5F8FA"; 46 | } else { 47 | linkStyle.backgroundColor = "#172430"; 48 | } 49 | } 50 | 51 | const ProfilePicture = userData.isAuth 52 | ? post.userName === userData.user.credentials.userName 53 | ? userData.user.credentials.profilePicture 54 | : post.profilePicture 55 | : post.profilePicture; 56 | 57 | return ( 58 |
toggleHover()} 62 | onMouseLeave={() => toggleHover()} 63 | > 64 |
65 |
66 | profile 71 |
72 |
73 |
74 |
75 |
76 | 83 | 84 | 85 | 91 | {moment(post.createdAt).twitterShort()} 92 | 93 |
94 |
95 | 96 |
97 |
98 |
106 | {post.postContent} 107 |
108 | {post.postImage ? ( 109 |
{ 116 | event.stopPropagation(); 117 | }} 118 | > 119 | 123 |
124 | ) : ( 125 | "" 126 | )} 127 | 128 |
134 | 135 | 136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default PostCard; 143 | -------------------------------------------------------------------------------- /Frontend/src/components/PostCardDetails/PostCardDetails.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | 4 | /* start post box */ 5 | .postDetails__post { 6 | display: flex; 7 | flex-direction: column; 8 | padding: 10px 10px 0; 9 | margin-top: 53px; 10 | 11 | // post header (avatar, userName, time, delete button) 12 | &__header { 13 | // user image 14 | &__userImage { 15 | float: left; 16 | 17 | &__wrapper { 18 | width: 50px; 19 | height: 50px; 20 | overflow: hidden; 21 | border-radius: 50%; 22 | margin-right: 10px; 23 | 24 | &__image { 25 | width: 100%; 26 | height: 100%; 27 | border-radius: 50%; 28 | object-fit: cover; 29 | } 30 | } 31 | } 32 | 33 | // user name + time 34 | &__col2 { 35 | font-size: 15px; 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: space-between; 39 | 40 | &__box { 41 | width: 90%; 42 | display: flex; 43 | flex-direction: column; 44 | } 45 | 46 | &__userName { 47 | font-weight: 700; 48 | margin-right: 5px; 49 | white-space: nowrap; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | } 53 | 54 | &__time { 55 | font-size: 13px; 56 | } 57 | 58 | // delete button 59 | &__delete { 60 | position: relative; 61 | display: flex; 62 | justify-content: flex-end; 63 | &:hover { 64 | cursor: pointer; 65 | } 66 | &:hover > .background { 67 | visibility: visible; 68 | } 69 | i { 70 | position: absolute; 71 | right: 8px; 72 | top: 7px; 73 | display: inline-block; 74 | } 75 | 76 | .background { 77 | position: absolute; 78 | width: 30px; 79 | height: 30px; 80 | border-radius: 50%; 81 | visibility: hidden; 82 | } 83 | } 84 | } 85 | } 86 | 87 | // start post content itself image, text 88 | &__content { 89 | width: 100%; 90 | 91 | &__line2 { 92 | font-size: 15px; 93 | margin-top: 5px; 94 | word-break: break-word; 95 | line-height: 1.3125; 96 | font-size: 23px; 97 | font-weight: 400; 98 | margin-bottom: 15px; 99 | white-space: pre-line; 100 | } 101 | 102 | &__line3 { 103 | width: 100%; 104 | height: auto; 105 | max-height: 300px; 106 | overflow: hidden; 107 | border-radius: 15px; 108 | margin-top: 10px; 109 | 110 | &__image { 111 | width: 100%; 112 | max-width: 100%; 113 | max-height: 100%; 114 | object-fit: cover; 115 | } 116 | } 117 | 118 | &__counters { 119 | display: flex; 120 | flex-direction: row; 121 | justify-content: flex-start; 122 | padding: 10px 0; 123 | font-size: 15px; 124 | font-weight: 400; 125 | 126 | &__comments { 127 | margin-right: 20px; 128 | } 129 | &__numbers { 130 | font-weight: bold; 131 | line-height: 1.3125; 132 | margin-right: 5px; 133 | } 134 | } 135 | 136 | &__line4 { 137 | font-size: 13px; 138 | display: flex; 139 | flex-direction: row; 140 | justify-content: space-around; 141 | margin-top: 10px; 142 | padding-bottom: 10px; 143 | 144 | i { 145 | font-size: 15px; 146 | margin-right: 5px; 147 | } 148 | 149 | &__comment { 150 | display: flex; 151 | &:hover > .comment__box > .comment__background { 152 | display: block; 153 | } 154 | &:hover { 155 | color: $mainColor; 156 | cursor: pointer; 157 | } 158 | .comment__box { 159 | position: relative; 160 | } 161 | 162 | .comment__background { 163 | position: absolute; 164 | top: -7px; 165 | left: -7px; 166 | width: 30px; 167 | height: 30px; 168 | border-radius: 50%; 169 | display: none; 170 | } 171 | } 172 | 173 | &__like { 174 | display: flex; 175 | &:hover > .like__box > .like__background { 176 | display: block; 177 | } 178 | &:hover > .like__box > i { 179 | color: $red; 180 | cursor: pointer; 181 | } 182 | .like__box { 183 | position: relative; 184 | } 185 | 186 | .like__background { 187 | position: absolute; 188 | top: -7px; 189 | left: -7px; 190 | width: 30px; 191 | height: 30px; 192 | border-radius: 50%; 193 | display: none; 194 | } 195 | } 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/EditProfileImageButton/EditProfileImageButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | // style 4 | import "./EditProfileImageButton.scss"; 5 | 6 | // api service 7 | import UserService from "../../../services/UserService"; 8 | 9 | // components 10 | import Spinner from "../../Spinner/Spinner"; 11 | 12 | // context (global state) 13 | import { ThemeContext } from "../../../context/ThemeContext"; 14 | import UserContext from "../../../context/UserContext"; 15 | import UserProfileContext from "../../../context/UserProfileContext"; 16 | 17 | const EditProfileImageButton = () => { 18 | // userData context 19 | const { userData, setUserData } = useContext(UserContext); 20 | 21 | // theme context 22 | const { isLightTheme, light, dark } = useContext(ThemeContext); 23 | const theme = isLightTheme ? light : dark; 24 | 25 | // user profile data context 26 | const { userProfileData, setUserProfileData } = 27 | useContext(UserProfileContext); 28 | 29 | // local state 30 | const [loading, setLoading] = useState(false); 31 | 32 | // on selecting an image, upload it direct to server 33 | const handleImageChange = (event) => { 34 | const image = event.target.files[0]; 35 | const formData = new FormData(); 36 | if (image.name) { 37 | formData.append("image", image, image.name); 38 | setLoading(true); 39 | // send the image to server 40 | UserService.uploadProfileImage(formData, userData.token) 41 | .then((res) => { 42 | let url = res.data.imageURL; 43 | 44 | // 1- update user data in UserProfile page (state), to show new profile image 45 | setUserProfileData({ 46 | friends: userProfileData.friends, 47 | posts: userProfileData.posts, 48 | user: { 49 | ...userProfileData.user, 50 | profilePicture: url, 51 | }, 52 | }); 53 | 54 | // 2- update user's avatar image in cache 55 | let cachedCurrentUser = JSON.parse( 56 | window.sessionStorage.getItem(userData.user.credentials.userName) 57 | ); 58 | if (cachedCurrentUser) { 59 | window.sessionStorage.setItem( 60 | userData.user.credentials.userName, 61 | JSON.stringify({ 62 | friends: cachedCurrentUser.friends, 63 | posts: userProfileData.posts, 64 | user: { 65 | ...cachedCurrentUser.user, 66 | profilePicture: url, 67 | }, 68 | }) 69 | ); 70 | } 71 | return url; 72 | }) 73 | .then((url) => { 74 | // 3- update user data in global context with new profile image 75 | setUserData({ 76 | isAuth: userData.isAuth, 77 | token: userData.token, 78 | user: { 79 | ...userData.user, 80 | credentials: { 81 | ...userData.user.credentials, 82 | profilePicture: url, 83 | }, 84 | }, 85 | }); 86 | 87 | // 4- update cache of user data (global state) 88 | let CacheUserData = JSON.parse( 89 | window.sessionStorage.getItem("CacheUserData") 90 | ); 91 | if (CacheUserData) { 92 | window.sessionStorage.setItem( 93 | "CacheUserData", 94 | JSON.stringify({ 95 | isAuth: userData.isAuth, 96 | token: userData.token, 97 | user: { 98 | ...userData.user, 99 | credentials: { 100 | ...userData.user.credentials, 101 | profilePicture: url, 102 | }, 103 | }, 104 | }) 105 | ); 106 | } 107 | }) 108 | .then(() => setLoading(false)) 109 | .catch((err) => { 110 | console.log(err); 111 | setLoading(false); 112 | }); 113 | } 114 | }; 115 | 116 | // on click, fire 'click event' of input (type file) 117 | const handleEditPicture = () => { 118 | const fileInput = document.getElementById("profileImageInput"); 119 | fileInput.click(); 120 | }; 121 | return loading ? ( 122 |
123 | 124 |
125 | ) : ( 126 |
127 | handleImageChange(event)} 132 | /> 133 | handleEditPicture()} 137 | > 138 |
139 | ); 140 | }; 141 | 142 | export default EditProfileImageButton; 143 | -------------------------------------------------------------------------------- /Frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Frontend/src/components/Buttons/TwitternButton/TwitternButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | // style 4 | import "./TwitternButton.scss"; 5 | // Global vars import 6 | import variables from "../../../style/CssVariables.scss"; 7 | 8 | // api service 9 | import PostService from "../../../services/PostService"; 10 | 11 | // context (global state) 12 | import { ThemeContext } from "../../../context/ThemeContext"; 13 | import { LanguageContext } from "../../../context/LanguageContext"; 14 | import UserContext from "../../../context/UserContext"; 15 | import PostsContext from "../../../context/PostsContext"; 16 | import UserProfileContext from "../../../context/UserProfileContext"; 17 | 18 | const TwitternButton = ({ 19 | textarea, 20 | setTextarea, 21 | imageStatus, 22 | setImageStatus, 23 | setOpen, 24 | }) => { 25 | // ******* start global state *******// 26 | // theme context 27 | const { isLightTheme, light, dark } = useContext(ThemeContext); 28 | const theme = isLightTheme ? light : dark; 29 | 30 | // language context 31 | const { isEnglish, english, german } = useContext(LanguageContext); 32 | var language = isEnglish ? english : german; 33 | 34 | // user context 35 | const { userData } = useContext(UserContext); 36 | 37 | // posts context 38 | const { posts, setPostsData } = useContext(PostsContext); 39 | 40 | // user profile data context 41 | const { userProfileData, setUserProfileData } = 42 | useContext(UserProfileContext); 43 | // ******* end global state *******// 44 | 45 | // local state 46 | const [isLoading, setLoading] = useState(false); 47 | 48 | var ButtonDisabledFlag = 49 | textarea.value.trim().length > 0 || imageStatus.select ? false : true; 50 | 51 | // add new post 52 | const sharePost = async () => { 53 | setLoading(true); 54 | let postTextContent = textarea.value.trim(); 55 | let post = { 56 | postContent: postTextContent.length > 0 ? postTextContent : "", 57 | postImage: null, 58 | }; 59 | 60 | // check if the post contain image also 61 | if (imageStatus.select) { 62 | // the post has image 63 | const formData = new FormData(); 64 | formData.append("image", imageStatus.image, imageStatus.image.name); 65 | // upload image to server and get url 66 | await PostService.uploadPostImage(formData, userData.token) 67 | .then((res) => { 68 | let url = res.data.postImage; 69 | post.postImage = url; 70 | }) 71 | .catch((err) => { 72 | console.log(err); 73 | }); 74 | } 75 | 76 | // add post to database 77 | PostService.addNewPost(post, userData.token) 78 | .then((res) => { 79 | return res; 80 | }) 81 | .then((res) => { 82 | let newPosts = [...posts]; 83 | // add the new post at index 1 in posts state (global state), because index 0 is reserved for pinned post 84 | newPosts.splice(1, 0, res.data); 85 | 86 | // 1- add the new post to global state to show it immediately in home page 87 | setPostsData(newPosts); 88 | 89 | // 2- update posts in session storage (cache) 90 | window.sessionStorage.setItem("posts", JSON.stringify(newPosts)); 91 | 92 | // 3- add this post to user profile data (global state), 93 | // only if current profile belongs to the logged in user. 94 | if ( 95 | userProfileData.user.userName === userData.user.credentials.userName 96 | ) { 97 | let userNewPosts = [...userProfileData.posts]; 98 | userNewPosts.unshift(res.data); 99 | setUserProfileData({ 100 | ...userProfileData, 101 | posts: userNewPosts, 102 | }); 103 | } 104 | 105 | // 4- update user profile data in session storage (cache) 106 | // get user profile data from cache 107 | let cachedUserProfileData = JSON.parse( 108 | window.sessionStorage.getItem(userData.user.credentials.userName) 109 | ); 110 | if (cachedUserProfileData) { 111 | let userNewPostsCache = [...cachedUserProfileData.posts]; 112 | userNewPostsCache.unshift(res.data); 113 | window.sessionStorage.setItem( 114 | userData.user.credentials.userName, 115 | JSON.stringify({ 116 | ...cachedUserProfileData, 117 | posts: userNewPostsCache, 118 | }) 119 | ); 120 | } 121 | 122 | // 5- clear inputs 123 | setTextarea({ 124 | value: "", 125 | rows: 1, 126 | minRows: 1, 127 | maxRows: 100, 128 | }); 129 | setImageStatus({ 130 | select: false, 131 | imagePath: null, 132 | image: "", 133 | }); 134 | setLoading(false); 135 | // close the modal 136 | if (setOpen) { 137 | setOpen(false); 138 | } 139 | }) 140 | .catch((err) => { 141 | console.log(err); 142 | setLoading(false); 143 | // close the modal 144 | if (setOpen) { 145 | setOpen(false); 146 | } 147 | }); 148 | }; 149 | 150 | return ( 151 | 166 | ); 167 | }; 168 | 169 | export default TwitternButton; 170 | -------------------------------------------------------------------------------- /Frontend/src/components/LikesModal/LikesModal.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, Fragment } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // style 5 | import "./LikesModal.scss"; 6 | 7 | // libraries 8 | import { Modal } from "react-bootstrap"; 9 | 10 | // comps 11 | import AddFriendButton from "../Buttons/AddFriendButton/AddFriendButton"; 12 | import CheckVerifiedUserName from "../CheckVerifiedUserName"; 13 | 14 | // context (global state) 15 | import { ThemeContext } from "../../context/ThemeContext"; 16 | import { LanguageContext } from "../../context/LanguageContext"; 17 | import UserContext from "../../context/UserContext"; 18 | 19 | const LikesModal = ({ postData, likes }) => { 20 | // ******* start global state ******* // 21 | // theme context 22 | const { isLightTheme, light, dark } = useContext(ThemeContext); 23 | const theme = isLightTheme ? light : dark; 24 | 25 | // language context 26 | const { isEnglish, english, german } = useContext(LanguageContext); 27 | var language = isEnglish ? english : german; 28 | 29 | // user context 30 | const { userData } = useContext(UserContext); 31 | 32 | // ******* end global state ******* // 33 | // local state 34 | const [isOpen, setOpen] = useState(false); 35 | 36 | // utils 37 | let closeModal = () => setOpen(false); 38 | 39 | let openModal = () => { 40 | if (postData.likeCount === 0) return; 41 | setOpen(true); 42 | }; 43 | 44 | return ( 45 | 46 |
51 | 57 | {postData.likeCount} 58 | 59 | 64 | {language.postDetails.likes} 65 | 66 |
67 | 68 | 75 | 82 |
83 |
closeModal()} 86 | > 87 | 91 |
97 |
98 |

104 | {language.postDetails.likesModalTitle} 105 |

106 |
107 |
108 | 113 |
114 | {likes.map((like) => { 115 | return ( 116 |
121 |
122 |
123 | 124 | profile 125 | 126 |
127 |
131 | 135 | 136 | 137 |

138 | @{like.userName} 139 |

140 |
141 |
142 | {/* add friend button */} 143 | {userData.isAuth ? ( 144 | like.userName !== userData.user.credentials.userName ? ( 145 |
149 | 152 |
153 | ) : ( 154 | "" 155 | ) 156 | ) : ( 157 | 158 | 163 | 164 | )} 165 |
166 | ); 167 | })} 168 |
169 |
170 |
171 |
172 | ); 173 | }; 174 | 175 | export default LikesModal; 176 | -------------------------------------------------------------------------------- /Frontend/src/components/PostCardDetails/PostCardDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // style 5 | import "./PostCardDetails.scss"; 6 | 7 | // libraries 8 | import moment from "moment-twitter"; 9 | import Linkify from "react-linkify"; 10 | 11 | // component 12 | import DeletePostButton from "../../components/Buttons/DeletePostButton/DeletePostButton"; 13 | import CommentButton from "../../components/Buttons/CommentButton"; 14 | import LikeButton from "../../components/Buttons/LikeButton"; 15 | import LikesModal from "../../components/LikesModal/LikesModal"; 16 | import CheckVerifiedUserName from "../CheckVerifiedUserName"; 17 | 18 | // context (global state) 19 | import { ThemeContext } from "../../context/ThemeContext"; 20 | import { LanguageContext } from "../../context/LanguageContext"; 21 | import ImageModal from "../../components/ImageModal/ImageModal"; 22 | import UserContext from "../../context/UserContext"; 23 | 24 | const PostCardDetails = ({ postData, likes, setLikes, setPostData }) => { 25 | // ******* start global state *******// 26 | // theme context 27 | const { isLightTheme, light, dark } = useContext(ThemeContext); 28 | const theme = isLightTheme ? light : dark; 29 | 30 | // language context 31 | const { isEnglish, english, german } = useContext(LanguageContext); 32 | var language = isEnglish ? english : german; 33 | 34 | // user context 35 | const { userData } = useContext(UserContext); 36 | // ******* end global state *******// 37 | 38 | var arabic = /[\u0600-\u06FF]/; 39 | 40 | const ProfilePicture = userData.isAuth 41 | ? postData.userName === userData.user.credentials.userName 42 | ? userData.user.credentials.profilePicture 43 | : postData.profilePicture 44 | : postData.profilePicture; 45 | 46 | return ( 47 |
53 |
54 |
55 |
56 | 57 | profile 62 | 63 |
64 |
65 |
66 |
67 | 75 | 76 | 77 | 83 | {moment(postData.createdAt).twitterShort()} 84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |
102 | {postData.postContent} 103 |
104 | {postData.postImage ? ( 105 |
{ 112 | event.stopPropagation(); 113 | //child(); 114 | }} 115 | > 116 | 120 |
121 | ) : ( 122 | "" 123 | )} 124 | 125 |
131 |
132 | 138 | {postData.commentCount} 139 | 140 | 145 | {language.postDetails.comments} 146 | 147 |
148 | 149 |
150 |
156 | 157 | 164 |
165 |
166 |
167 | ); 168 | }; 169 | 170 | export default PostCardDetails; 171 | -------------------------------------------------------------------------------- /Frontend/src/pages/UserProfile/UserProfile.scss: -------------------------------------------------------------------------------- 1 | @import "../../style/CssVariables.scss"; 2 | 3 | .userProfile__main { 4 | height: auto; 5 | width: 45%; 6 | margin-left: 25%; 7 | overflow: auto; 8 | height: auto; 9 | 10 | // page header 11 | &__title { 12 | padding: 5px 10px; 13 | height: 53px; 14 | position: fixed; 15 | width: 45%; 16 | z-index: 100; 17 | display: flex; 18 | flex-direction: row; 19 | 20 | &__iconBox { 21 | position: relative; 22 | margin: 11px 32px 0 10px; 23 | 24 | &:hover > &__background { 25 | display: block; 26 | } 27 | 28 | &:hover { 29 | color: $mainColor; 30 | cursor: pointer; 31 | } 32 | 33 | &__background { 34 | position: absolute; 35 | top: -3px; 36 | left: -8px; 37 | width: 30px; 38 | height: 30px; 39 | border-radius: 50%; 40 | display: none; 41 | } 42 | } 43 | 44 | &__textBox { 45 | width: 100%; 46 | overflow: hidden; 47 | h2 { 48 | font-size: 19px; 49 | font-weight: 800; 50 | margin: 0; 51 | white-space: nowrap; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | } 55 | 56 | p { 57 | font-size: 13px; 58 | margin: 0; 59 | white-space: nowrap; 60 | overflow: hidden; 61 | text-overflow: ellipsis; 62 | } 63 | } 64 | } 65 | 66 | // user data 67 | &__userDetails { 68 | margin-top: 53px; 69 | margin-bottom: 80px; 70 | 71 | // cover image 72 | &__headerImageBox { 73 | position: relative; 74 | overflow: hidden; 75 | width: 100%; 76 | max-width: 100%; 77 | max-height: 200px; 78 | 79 | &__image { 80 | max-width: 100%; 81 | min-width: 100%; 82 | max-height: 100%; 83 | height: 200px; 84 | object-fit: cover; 85 | } 86 | } 87 | 88 | // rest user data 89 | &__userData { 90 | position: relative; 91 | padding: 10px; 92 | 93 | // user profile image line 94 | &__pp { 95 | position: absolute; 96 | top: -50px; 97 | 98 | &__userImageBox { 99 | width: 134px; 100 | height: 134px; 101 | overflow: hidden; 102 | border-radius: 50%; 103 | box-sizing: content-box; 104 | 105 | > div:first-child { 106 | height: 100%; 107 | } 108 | 109 | &__userImage { 110 | /* user image it self */ 111 | width: 100%; 112 | height: 100%; 113 | object-fit: cover; 114 | } 115 | 116 | /* make user image rounder also in modal full view 117 | .__react_modal_image__modal_content img, 118 | .__react_modal_image__modal_content svg { 119 | border-radius: 50% !important; 120 | }*/ 121 | } 122 | } 123 | 124 | // main button 125 | &__buttonBox { 126 | display: flex; 127 | justify-content: flex-end; 128 | height: 50px; 129 | } 130 | 131 | // userName 132 | &__userName { 133 | margin-top: 30px; 134 | margin-bottom: 10px; 135 | h2 { 136 | font-size: 19px; 137 | font-weight: 800; 138 | margin: 0; 139 | white-space: nowrap; 140 | overflow: hidden; 141 | text-overflow: ellipsis; 142 | } 143 | } 144 | 145 | // bio 146 | &__bio { 147 | margin-bottom: 10px; 148 | p { 149 | font-size: 15px; 150 | margin: 0; 151 | } 152 | } 153 | 154 | //extra data 155 | &__extraData { 156 | display: flex; 157 | flex-direction: row; 158 | flex-wrap: wrap; 159 | margin-bottom: 10px; 160 | 161 | div { 162 | margin-right: 15px; 163 | font-size: 15px; 164 | word-break: break-all; 165 | 166 | i { 167 | font-size: 16px; 168 | margin-right: 5px; 169 | } 170 | 171 | a { 172 | &:hover { 173 | color: #007bff; 174 | } 175 | } 176 | } 177 | } 178 | 179 | // friend counter 180 | &__friends { 181 | margin-bottom: 10px; 182 | font-size: 15px; 183 | display: inline-block; 184 | 185 | &__number { 186 | font-weight: bold; 187 | } 188 | } 189 | } 190 | 191 | &__posts { 192 | /* start no posts box*/ 193 | .posts__empty { 194 | text-align: center; 195 | padding: 30px 10px 0; 196 | img { 197 | width: 100%; 198 | max-width: 100%; 199 | height: 150px; 200 | margin-bottom: 20px; 201 | } 202 | p { 203 | font-size: 15px; 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | @media only screen and (max-width: 500px) { 211 | /* For mobile phones: */ 212 | .userProfile__main { 213 | width: 100%; 214 | margin-left: 0; 215 | margin-bottom: 49px; 216 | &__title { 217 | width: 100%; 218 | } 219 | &__userDetails__headerImageBox { 220 | max-height: 120px; 221 | 222 | &__image{ 223 | height: 120px; 224 | } 225 | } 226 | &__userDetails__userData__pp { 227 | top: -25px; 228 | &__userImageBox { 229 | width: 68px; 230 | height: 68px; 231 | } 232 | } 233 | &__userDetails__userData__userName { 234 | margin-top: -8px; 235 | } 236 | } 237 | } 238 | 239 | @media (min-width: 501px) and (max-width: 991px) { 240 | /* For tablet: */ 241 | .userProfile__main { 242 | width: 75%; 243 | margin-left: 15%; 244 | &__title { 245 | width: 75%; 246 | } 247 | &__userDetails__userData__pp__userImageBox { 248 | width: 110px; 249 | height: 110px; 250 | } 251 | &__userDetails__userData__userName { 252 | margin-top: 8px; 253 | } 254 | } 255 | } 256 | 257 | @media (min-width: 992px) and (max-width: 1280px) { 258 | /* For small lab: */ 259 | .userProfile__main { 260 | width: 50%; 261 | margin-left: 10%; 262 | &__title { 263 | width: 50%; 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Frontend/src/pages/PostDetails/PostDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect, Fragment } from "react"; 2 | 3 | // style 4 | import "./PostDetails.scss"; 5 | 6 | // assets 7 | import Empty from "../../assets/Images/empty.svg"; 8 | 9 | // api service 10 | import PostService from "../../services/PostService"; 11 | 12 | // component 13 | import CommentCard from "../../components/CommentCard/CommentCard"; 14 | import AddComment from "../../components/AddComment/AddComment"; 15 | import PostCardDetails from "../../components/PostCardDetails/PostCardDetails"; 16 | import Spinner from "../../components/Spinner/Spinner"; 17 | 18 | // context (global state) 19 | import { ThemeContext } from "../../context/ThemeContext"; 20 | import { LanguageContext } from "../../context/LanguageContext"; 21 | import UserContext from "../../context/UserContext"; 22 | 23 | const PostDetails = (props) => { 24 | // ******* start global state *******// 25 | // theme context 26 | const { isLightTheme, light, dark } = useContext(ThemeContext); 27 | const theme = isLightTheme ? light : dark; 28 | 29 | // language context 30 | const { isEnglish, english, german } = useContext(LanguageContext); 31 | var language = isEnglish ? english : german; 32 | 33 | // user context 34 | const { userData } = useContext(UserContext); 35 | // ******* end global state *******// 36 | 37 | //local state 38 | const [postId, setPostId] = useState( 39 | props.match.params.postId ? props.match.params.postId : "" 40 | ); 41 | const [postData, setPostData] = useState([]); 42 | const [likes, setLikes] = useState([]); 43 | const [comments, setComments] = useState([]); 44 | const [loading, setLoading] = useState(false); 45 | 46 | // set page title 47 | document.title = language.postDetails.pageTitle; 48 | 49 | useEffect(() => { 50 | setPostId(props.match.params.postId); 51 | let postID = props.match.params.postId; 52 | 53 | // get cache (current post), each post will be cached with key (postID) 54 | let cachedCurrentPost = JSON.parse(window.sessionStorage.getItem(postID)); 55 | 56 | if (cachedCurrentPost) { 57 | // current post's data are cached 58 | setPostData(cachedCurrentPost.postContent); 59 | setComments(cachedCurrentPost.postComments); 60 | setLikes(cachedCurrentPost.postLikes); 61 | setLoading(false); 62 | } else { 63 | // get current post from DB 64 | setLoading(true); 65 | if (postID) { 66 | // get all details of current post 67 | PostService.getPostDetails(postID) 68 | .then((res) => { 69 | let postContent = res.data.post; 70 | postContent.postId = res.data.postId; 71 | setPostData(postContent); 72 | setComments(res.data.comments); 73 | setLikes(res.data.likes); 74 | // add current post's data to session storage (cache) 75 | window.sessionStorage.setItem( 76 | postID, 77 | JSON.stringify({ 78 | postContent, 79 | postComments: res.data.comments, 80 | postLikes: res.data.likes, 81 | }) 82 | ); 83 | setLoading(false); 84 | }) 85 | .catch((err) => { 86 | console.log(err); 87 | setLoading(false); 88 | }); 89 | } 90 | } 91 | }, [props.match.params.postId]); 92 | 93 | const goToBack = () => { 94 | props.history.goBack(); 95 | }; 96 | 97 | return ( 98 |
104 |
111 |
goToBack()} 114 | > 115 | 119 |
125 |
126 |

131 | {language.postDetails.title} 132 |

133 |
134 | {loading ? ( 135 | 136 | ) : ( 137 | 138 | {/* post data */} 139 |
140 | 146 |
147 | 148 | {/* add comment input */} 149 | {userData.isAuth ? ( 150 | 157 | ) : ( 158 | "" 159 | )} 160 | 161 | {/* comments */} 162 |
163 | {comments.length > 0 ? ( 164 | [...comments].map((comment) => ( 165 | 170 | )) 171 | ) : ( 172 |
173 | empty 174 |

179 | {language.postDetails.noCommentHint} 180 |

181 |
182 | )} 183 |
184 |
185 | )} 186 |
187 | ); 188 | }; 189 | 190 | export default PostDetails; 191 | -------------------------------------------------------------------------------- /Frontend/src/parts/PinnedPost/PinnedPost.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, Fragment } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | // style file 5 | import "./PinnedPost.scss"; 6 | 7 | // libraries 8 | import ImageModal from "../../components/ImageModal/ImageModal"; 9 | import moment from "moment-twitter"; 10 | import Linkify from "react-linkify"; 11 | 12 | // context (global state) 13 | import { ThemeContext } from "../../context/ThemeContext"; 14 | import { LanguageContext } from "../../context/LanguageContext"; 15 | import UserContext from "../../context/UserContext"; 16 | 17 | // component 18 | import LikeButton from "../../components/Buttons/LikeButton"; 19 | import CommentButton from "../../components/Buttons/CommentButton"; 20 | import CheckVerifiedUserName from "../../components/CheckVerifiedUserName"; 21 | import Spinner from "../../components/Spinner/Spinner"; 22 | 23 | const PinnedPost = ({ pinnedPost, PinnedPostLoad }) => { 24 | // ******* start global state ******* // 25 | 26 | // theme context 27 | const { isLightTheme, light, dark } = useContext(ThemeContext); 28 | const theme = isLightTheme ? light : dark; 29 | 30 | // language context 31 | const { isEnglish, english, german } = useContext(LanguageContext); 32 | var language = isEnglish ? english : german; 33 | 34 | // user context 35 | const { userData } = useContext(UserContext); 36 | 37 | // ******* end global state ******* // 38 | 39 | // local state 40 | const [isHover, setHover] = useState(false); 41 | 42 | // lib init 43 | const history = useHistory(); 44 | 45 | var arabic = /[\u0600-\u06FF]/; 46 | 47 | // add dynamic style on hover on post card 48 | const toggleHover = () => { 49 | setHover(!isHover); 50 | }; 51 | 52 | // dynamic style on hover 53 | var linkStyle = { borderBottom: `10px solid ${theme.PinnedPostBorder}` }; 54 | if (isHover) { 55 | if (isLightTheme) { 56 | linkStyle.backgroundColor = "#F5F8FA"; 57 | } else { 58 | linkStyle.backgroundColor = "#172430"; 59 | } 60 | } 61 | 62 | // direct to post details page on click on post 63 | const toPostDetails = (postID) => { 64 | history.push("/posts/" + postID); 65 | }; 66 | 67 | const ProfilePicture = userData.isAuth 68 | ? userData.user.credentials.profilePicture 69 | : pinnedPost.profilePicture; 70 | 71 | return ( 72 |
toggleHover()} 76 | onMouseLeave={() => toggleHover()} 77 | onClick={() => toPostDetails(pinnedPost.postId)} 78 | > 79 | {PinnedPostLoad ? ( 80 | 81 | ) : ( 82 | 83 |
84 |
85 | profile 90 |
91 |
92 |
93 |
94 |
95 | 104 | 105 | 106 | 112 | {/*" · " + dayjs(PinnedPost.createdAt).fromNow(true)*/} 113 | {moment(pinnedPost.createdAt).twitterShort()} 114 | 115 |
116 |
117 | 121 |
122 |
123 |
124 | 125 | {language.home.pinnedPost} 126 | 127 |
128 |
140 | {pinnedPost.postContent} 141 |
142 | {pinnedPost.postImage ? ( 143 |
{ 149 | event.stopPropagation(); 150 | }} 151 | > 152 | 156 |
157 | ) : ( 158 | "" 159 | )} 160 | 161 |
167 | 168 | 169 |
170 |
171 |
172 | )} 173 |
174 | ); 175 | }; 176 | 177 | export default PinnedPost; 178 | -------------------------------------------------------------------------------- /Frontend/src/assets/Images/empty.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | no data 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | --------------------------------------------------------------------------------