├── 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
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 |
--------------------------------------------------------------------------------
/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 |

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 |
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 |
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 |
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 | 
10 | 
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 |

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 |
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 |

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 |

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 |

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 |

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 |
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 |

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 |
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 |

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 |

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 |
--------------------------------------------------------------------------------