├── .eslintignore ├── netlify.toml ├── .prettierrc ├── .vscode └── settings.json ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── assets │ ├── screenshots │ │ ├── newsfeed.PNG │ │ └── profilepage.PNG │ └── icons │ │ ├── ellipsis-horizontal.svg │ │ ├── close.svg │ │ ├── caret-down-outline.svg │ │ ├── chatbox.svg │ │ ├── play.svg │ │ ├── share-social.svg │ │ ├── thumbs-up.svg │ │ ├── logo-markdown.svg │ │ ├── arrow-back-outline.svg │ │ ├── alert-circle.svg │ │ ├── search-outline.svg │ │ ├── briefcase.svg │ │ ├── add-circle-outline.svg │ │ ├── camera.svg │ │ ├── person.svg │ │ ├── school.svg │ │ ├── images.svg │ │ ├── home.svg │ │ ├── logo.svg │ │ ├── person-add.svg │ │ ├── notifications.svg │ │ └── calendar.svg ├── index.jsx ├── components │ ├── Image │ │ ├── Image.styles.js │ │ ├── ChangeAvatarPhoto.styles.js │ │ ├── ChangeCoverPhoto.styles.js │ │ ├── Image.jsx │ │ ├── ChangeAvatarPhoto.jsx │ │ └── ChangeCoverPhoto.jsx │ ├── Notification │ │ ├── NotificationList.styles.js │ │ ├── NotificationList.jsx │ │ ├── Notification.styles.js │ │ └── Notification.jsx │ ├── Message │ │ ├── MessageList.styles.js │ │ ├── Message.styles.js │ │ ├── MessageList.jsx │ │ ├── Message.jsx │ │ ├── SingleChat.styles.js │ │ └── SingleChat.jsx │ ├── Comment │ │ ├── CreateComment.styles.js │ │ ├── Comment.styles.js │ │ ├── CreateComment.jsx │ │ └── Comment.jsx │ ├── Post │ │ ├── CreatePostDefault.jsx │ │ ├── CreatePostDefault.styles.js │ │ ├── CreatePostActive.styles.js │ │ ├── CreatePostActive.jsx │ │ ├── Post.styles.js │ │ └── Post.jsx │ ├── About │ │ ├── About.jsx │ │ └── About.styles.js │ ├── Friend │ │ ├── Friend.jsx │ │ └── Friend.styles.js │ ├── LoginForm │ │ ├── LoginForm.styles.js │ │ └── LoginForm.jsx │ ├── RegisterForm │ │ ├── RegisterForm.styles.js │ │ └── RegisterForm.jsx │ └── Navbar │ │ └── Navbar.jsx ├── globalStyles │ ├── theme.js │ └── index.js ├── routes │ ├── layouts │ │ ├── ProfileLayout.styles.js │ │ └── ProfileLayout.jsx │ ├── Routes.jsx │ ├── PrivateRoute.jsx │ └── ProfileRoute.jsx ├── App.jsx ├── pages │ ├── AuthPages │ │ ├── LoginPage.jsx │ │ ├── RegisterPage.jsx │ │ ├── LoginPage.styles.js │ │ └── RegisterPage.styles.js │ ├── SinglePostPage │ │ ├── SinglePostPage.styles.js │ │ └── SinglePostPage.jsx │ ├── PhotosPage │ │ ├── PhotosPage.styles.js │ │ └── PhotosPage.jsx │ ├── FriendsPage │ │ ├── FriendsPage.styles.js │ │ └── FriendsPage.jsx │ ├── AboutPage │ │ ├── AboutOverview.styles.js │ │ ├── AboutWorkAndEducation.styles.js │ │ └── AboutOverview.jsx │ ├── NewsfeedPage │ │ ├── NewsfeedPage.styles.js │ │ └── NewsfeedPage.jsx │ └── ProfilePage │ │ └── ProfilePage.styles.js ├── utils │ └── alerts.js └── ApolloProvider.jsx ├── .env.example ├── .gitignore ├── .github ├── workflows │ └── nodejs.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .eslintrc ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "tabWidth": 2, 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.enable": true 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristianWEB/fakebooker-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristianWEB/fakebooker-frontend/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristianWEB/fakebooker-frontend/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/screenshots/newsfeed.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristianWEB/fakebooker-frontend/HEAD/src/assets/screenshots/newsfeed.PNG -------------------------------------------------------------------------------- /src/assets/screenshots/profilepage.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristianWEB/fakebooker-frontend/HEAD/src/assets/screenshots/profilepage.PNG -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_WSS_LINK=WSSBACKENDURL 2 | REACT_APP_HTTP_LINK=HTTPBACKENDURL 3 | REACT_APP_CLOUDINARY_URL=CLOUDINARYURL 4 | REACT_APP_CLOUDINARY_PRESET=CLOUDINARYPRESET -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import ApolloProvider from "./ApolloProvider"; 3 | 4 | ReactDOM.render(ApolloProvider, document.getElementById("root")); 5 | -------------------------------------------------------------------------------- /src/assets/icons/ellipsis-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/caret-down-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/chatbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/share-social.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-f -------------------------------------------------------------------------------- /src/assets/icons/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo-markdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-back-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Image/Image.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ImageContainer = styled.div` 4 | padding: 8px 5px; 5 | && { 6 | span { 7 | width: 20px; 8 | height: 20px; 9 | display: block; 10 | } 11 | } 12 | `; 13 | 14 | export const ImageUpload = styled.input` 15 | display: none; 16 | `; 17 | -------------------------------------------------------------------------------- /src/assets/icons/alert-circle.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-a -------------------------------------------------------------------------------- /src/assets/icons/search-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/assets/icons/briefcase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/add-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/person.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-j -------------------------------------------------------------------------------- /src/assets/icons/school.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-q -------------------------------------------------------------------------------- /src/globalStyles/theme.js: -------------------------------------------------------------------------------- 1 | // when every button with backgroundColor is pressed => transform: scale(0,96) 2 | 3 | const theme = { 4 | primaryText: "#1876f2", 5 | secondaryText: "#050505", 6 | tertiaryText: "#65676b", 7 | errorText: "#D93025", 8 | inputColor: "#f0f2f5", 9 | placeholderColor: "#90949c", 10 | primaryBackground: "#e7f3ff", 11 | secondaryBackground: "#e4e6eb", 12 | secondaryHoverBackground: "#d8dadf", 13 | tertiaryBackground: "#F2F2F2", 14 | boxShadow1: "0 1px 2px 0 rgba(0, 0, 0, 0.1)", 15 | boxShadow2: "0 1px 2px 0 rgba(0, 0, 0, 0.2)", 16 | }; 17 | 18 | export default theme; 19 | -------------------------------------------------------------------------------- /src/routes/layouts/ProfileLayout.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const ProfileHeaderSkeleton = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | background-color: #fff; 8 | padding: 13px; 9 | 10 | svg { 11 | width: 940px; 12 | height: 350px; 13 | } 14 | `; 15 | 16 | export const NavbarSkeleton = styled.div` 17 | display: flex; 18 | 19 | svg { 20 | width: 100%; 21 | height: 58px; 22 | rect { 23 | width: 100%; 24 | height: 50px; 25 | } 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Notification/NotificationList.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const NotificationListContainer = styled.div` 4 | font-family: "Roboto"; 5 | display: flex; 6 | flex-direction: column; 7 | width: 100%; 8 | `; 9 | 10 | export const NotificationListHeading = styled.h3` 11 | color: ${(props) => props.theme.secondaryText}; 12 | font-size: 2.2rem; 13 | font-weight: bold; 14 | line-height: 1; 15 | padding: 16px 18px 12px 18px; 16 | `; 17 | 18 | export const NotificationRow = styled.div` 19 | padding: 8px; 20 | max-height: 350px; 21 | overflow-y: auto; 22 | `; 23 | -------------------------------------------------------------------------------- /src/components/Message/MessageList.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const MessageListContainer = styled.div` 4 | font-family: "Roboto"; 5 | width: 100%; 6 | `; 7 | 8 | export const MessageListHeading = styled.h3` 9 | color: ${(props) => props.theme.secondaryText}; 10 | font-size: 2.2rem; 11 | font-weight: bold; 12 | line-height: 1; 13 | padding: 16px 18px 12px 18px; 14 | `; 15 | export const MessageRow = styled.div` 16 | padding: 8px; 17 | max-height: 350px; 18 | overflow-y: auto; 19 | `; 20 | 21 | export const MessageListSkeleton = styled.div` 22 | padding: 16px; 23 | `; 24 | -------------------------------------------------------------------------------- /src/assets/icons/images.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Fakebooker-Frontend 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-16.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v1 11 | - name: ESLint 12 | uses: stefanoeb/eslint-action@1.0.2 13 | with: 14 | files: src/ 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and lint ( fix ) 20 | run: | 21 | npm run lint:fix 22 | npm run lint 23 | npm run build 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /src/assets/icons/person-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/notifications.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-j -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "@apollo/react-hooks"; 3 | import { ThemeProvider } from "styled-components"; 4 | import Routes from "./routes/Routes"; 5 | import { GlobalStyle } from "./globalStyles/index"; 6 | import theme from "./globalStyles/theme"; 7 | import { OPEN_CHAT } from "./utils/queries"; 8 | import SingleChat from "./components/Message/SingleChat"; 9 | 10 | const App = () => { 11 | const { data: chatData } = useQuery(OPEN_CHAT); 12 | 13 | return ( 14 | 15 | 16 | 17 | {chatData.chat.visible && } 18 | 19 | ); 20 | }; 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Comment/CreateComment.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CommentForm = styled.form` 4 | display: flex; 5 | position: relative; 6 | font-family: Roboto; 7 | width: 100%; 8 | `; 9 | export const UserAvatar = styled.img` 10 | width: 32px; 11 | height: 32px; 12 | border-radius: 100%; 13 | `; 14 | 15 | export const CommentInput = styled.input` 16 | border-radius: 18px; 17 | margin-left: 5px; 18 | height: 35px; 19 | border: none; 20 | width: 100%; 21 | padding-left: 8px; 22 | font-size: 1.5rem; 23 | box-sizing: border-box; 24 | background-color: ${(props) => props.theme.inputColor}; 25 | 26 | &::placeholder { 27 | color: ${(props) => props.theme.placeholderColor}; 28 | } 29 | 30 | &:focus { 31 | outline: none; 32 | border: 2px solid ${(props) => props.theme.primaryText}; 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/components/Image/ChangeAvatarPhoto.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ChangeAvatarContainer = styled.button` 4 | background-color: ${(props) => props.theme.secondaryBackground}; 5 | display: flex; 6 | justify-content: center; 7 | justify-content: center; 8 | align-items: center; 9 | border-radius: 50%; 10 | position: absolute; 11 | padding: 0.5rem 0.8rem; 12 | height: 36px; 13 | top: 110px; 14 | right: 0; 15 | border: 0; 16 | cursor: pointer; 17 | transition: 0.1s; 18 | 19 | &&:focus { 20 | background-color: ${(props) => props.theme.secondaryBackground}; 21 | outline: none; 22 | } 23 | &&:hover { 24 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 25 | outline: none; 26 | } 27 | &&:active { 28 | transform: scale(0.96); 29 | } 30 | `; 31 | 32 | export const AvatarImageUpload = styled.input` 33 | display: none; 34 | `; 35 | -------------------------------------------------------------------------------- /src/components/Notification/NotificationList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "@apollo/react-hooks"; 3 | import Notification from "./Notification"; 4 | import { 5 | NotificationListContainer, 6 | NotificationListHeading, 7 | NotificationRow, 8 | } from "./NotificationList.styles"; 9 | import { GET_NOTIFICATIONS } from "../../utils/queries"; 10 | 11 | const NotificationList = () => { 12 | const { data } = useQuery(GET_NOTIFICATIONS); 13 | 14 | return ( 15 | 16 | Notifications 17 | {data && ( 18 | 19 | {data.getNotifications.map((notification) => ( 20 | 21 | ))} 22 | 23 | )} 24 | 25 | ); 26 | }; 27 | 28 | export default NotificationList; 29 | -------------------------------------------------------------------------------- /src/pages/AuthPages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import { useQuery } from "@apollo/react-hooks"; 4 | import LoginForm from "../../components/LoginForm/LoginForm"; 5 | import { 6 | LoginPageContainer, 7 | LoginPageBackground, 8 | FormContainer, 9 | SVGImgBackground, 10 | ActionsContainer, 11 | } from "./LoginPage.styles"; 12 | import { LOAD_USER } from "../../utils/queries"; 13 | 14 | const LoginPage = () => { 15 | const { data: userData } = useQuery(LOAD_USER); 16 | 17 | if (userData) { 18 | return ; 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default LoginPage; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/utils/alerts.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { toast, Slide } from "react-toastify"; 3 | import Notification from "../components/Notification/Notification"; 4 | import { CloseContainer } from "../components/Post/CreatePostActive.styles"; 5 | import { ReactComponent as CloseBtn } from "../assets/icons/close.svg"; 6 | 7 | // eslint-disable-next-line import/prefer-default-export 8 | export const notificationAlert = (notification) => { 9 | toast( 10 |
11 | 12 |
, 13 | { 14 | position: toast.POSITION.BOTTOM_LEFT, 15 | hideProgressBar: true, 16 | autoClose: 5000, 17 | className: "notificationToast", 18 | transition: Slide, 19 | closeButton: ( 20 | 23 | 24 | 25 | ), 26 | } 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/pages/AuthPages/RegisterPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import { useQuery } from "@apollo/react-hooks"; 4 | import RegisterForm from "../../components/RegisterForm/RegisterForm"; 5 | import { 6 | RegisterPageContainer, 7 | RegisterPageBackground, 8 | FormContainer, 9 | SVGImgBackground, 10 | ActionsContainer 11 | } from "./RegisterPage.styles"; 12 | import { LOAD_USER } from "../../utils/queries"; 13 | 14 | const RegisterPage = () => { 15 | const { data: userData } = useQuery(LOAD_USER); 16 | 17 | if (userData) { 18 | return ; 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default RegisterPage; 36 | -------------------------------------------------------------------------------- /src/pages/SinglePostPage/SinglePostPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoContainer = styled.div` 4 | display: flex; 5 | height: 100%; 6 | width: 100%; 7 | justify-content: center; 8 | padding-top: 8px; 9 | font-family: Roboto; 10 | `; 11 | 12 | export const PostContainer = styled.div` 13 | display: flex; 14 | padding: 0 32px; 15 | justify-content: center; 16 | 17 | @media only screen and (max-width: 992px) { 18 | padding: 0; 19 | width: 100%; 20 | } 21 | `; 22 | export const PostsSection = styled.div` 23 | width: 680px; 24 | `; 25 | 26 | export const NavbarSkeleton = styled.div` 27 | display: flex; 28 | 29 | svg { 30 | width: 100%; 31 | height: 58px; 32 | rect { 33 | width: 100%; 34 | height: 50px; 35 | } 36 | } 37 | `; 38 | 39 | export const PostSkeleton = styled.div` 40 | display: flex; 41 | margin-top: 20px; 42 | background: #fff; 43 | border-radius: 8px; 44 | padding: 13px; 45 | svg { 46 | width: 100%; 47 | height: 100%; 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /src/components/Image/ChangeCoverPhoto.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ChangePhotoContainer = styled.button` 4 | background-color: ${(props) => props.theme.secondaryBackground}; 5 | display: inline-flex; 6 | justify-content: center; 7 | align-items: center; 8 | border-radius: 6px; 9 | margin-right: 40px; 10 | margin-bottom: 15px; 11 | cursor: pointer; 12 | position: absolute; 13 | right: 0; 14 | top: 85%; 15 | border: 0; 16 | padding: 5px 8px; 17 | transition: 0.1s; 18 | &&:focus { 19 | background-color: ${(props) => props.theme.secondaryBackground}; 20 | outline: none; 21 | } 22 | &&:hover { 23 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 24 | outline: none; 25 | } 26 | &&:active { 27 | transform: scale(0.96); 28 | } 29 | `; 30 | 31 | export const ChangeBackgroundHeading = styled.h3` 32 | margin-left: 4px; 33 | margin-bottom: 0; 34 | font-size: 1.5rem; 35 | `; 36 | 37 | export const CoverImageUpload = styled.input` 38 | display: none; 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/Post/CreatePostDefault.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | CreatePostContainer, 5 | UserAvatar, 6 | CreatePostButton, 7 | StyledPopup, 8 | } from "./CreatePostDefault.styles"; 9 | import CreatePostActive from "./CreatePostActive"; 10 | 11 | const CreatePostDefault = ({ user, onNewsfeed }) => ( 12 | 13 | 14 | Add a Post} 16 | modal 17 | closeOnDocumentClick 18 | > 19 | {(close) => ( 20 | 25 | )} 26 | 27 | 28 | ); 29 | 30 | export default CreatePostDefault; 31 | 32 | CreatePostDefault.propTypes = { 33 | user: PropTypes.shape({ 34 | avatarImage: PropTypes.string, 35 | }), 36 | onNewsfeed: PropTypes.bool, 37 | }; 38 | 39 | CreatePostDefault.defaultProps = { 40 | user: null, 41 | onNewsfeed: null, 42 | }; 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": [ 10 | "airbnb", 11 | "plugin:react/recommended", 12 | "plugin:jsx-a11y/strict", 13 | "plugin:prettier/recommended", 14 | "prettier/react" 15 | ], 16 | "parser": "babel-eslint", 17 | "parserOptions": { 18 | "ecmaVersion": 6, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true, 22 | "modules": true, 23 | "experimentalObjectRestSpread": true 24 | } 25 | }, 26 | "plugins": ["react", "react-hooks", "jsx-a11y", "prettier"], 27 | "rules": { 28 | "react/jsx-filename-extension": [ 29 | 1, 30 | { 31 | "extensions": [".js", ".jsx"] 32 | } 33 | ], 34 | "react-hooks/rules-of-hooks": "error", 35 | "react-hooks/exhaustive-deps": "warn", 36 | "no-console": "off", 37 | "react/prop-types": 0, 38 | "prettier/prettier": ["error"] 39 | }, 40 | "globals": { 41 | "window": true, 42 | "document": true, 43 | "localStorage": true, 44 | "FormData": true, 45 | "FileReader": true, 46 | "Blob": true, 47 | "navigator": true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Message/Message.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const MessageContainer = styled.div` 4 | display: flex; 5 | padding: 8px; 6 | height: 100%; 7 | border-radius: 8px; 8 | transition: 0.1s; 9 | cursor: pointer; 10 | 11 | &:hover { 12 | background-color: ${(props) => props.theme.tertiaryBackground}; 13 | } 14 | 15 | &:last-child { 16 | margin-bottom: 12px; 17 | } 18 | &:active { 19 | background-color: ${(props) => props.theme.secondaryBackground}; 20 | } 21 | `; 22 | 23 | export const NotifierAvatar = styled.img` 24 | width: 56px; 25 | height: 56px; 26 | border-radius: 100%; 27 | object-fit: cover; 28 | margin-right: 12px; 29 | `; 30 | 31 | export const NotifierFullName = styled.span` 32 | font-weight: bold; 33 | color: ${(props) => props.theme.secondaryText}; 34 | font-size: 1.5rem; 35 | `; 36 | 37 | export const Body = styled.div` 38 | font-size: 1.5rem; 39 | color: ${(props) => props.theme.secondaryText}; 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: center; 43 | `; 44 | 45 | export const Footer = styled.div` 46 | display: flex; 47 | color: ${(props) => props.theme.tertiaryText}; 48 | font-size: 1.3rem; 49 | font-weight: 500; 50 | `; 51 | -------------------------------------------------------------------------------- /src/pages/PhotosPage/PhotosPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoContainer = styled.div` 4 | display: flex; 5 | height: 100%; 6 | padding: 28px 0; 7 | font-family: Roboto; 8 | `; 9 | 10 | export const FixedContainer = styled.div` 11 | width: 876px; 12 | box-sizing: border-box; 13 | height: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | background-color: #fff; 17 | @media only screen and (max-width: 767px) { 18 | justify-content: center; 19 | } 20 | margin: 0 auto; 21 | padding: 16px; 22 | box-shadow: ${(props) => props.theme.boxShadow2}; 23 | border-radius: 6px; 24 | @media only screen and (max-width: 575px) { 25 | border-radius: 0; 26 | } 27 | `; 28 | 29 | export const PhotosHeading = styled.h1` 30 | font-size: 2rem; 31 | font-weight: bold; 32 | margin-bottom: 30px; 33 | color: ${(props) => props.theme.secondaryText}; 34 | `; 35 | 36 | export const PhotosContainer = styled.div` 37 | display: grid; 38 | grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); 39 | grid-gap: 10px; 40 | `; 41 | export const Photo = styled.img` 42 | width: 100%; 43 | border-radius: 8px; 44 | min-height: 20rem; 45 | object-fit: cover; 46 | `; 47 | 48 | export const PhotosSkeleton = styled.div` 49 | svg { 50 | width: 100%; 51 | height: 100%; 52 | rect { 53 | width: 100%; 54 | height: 100%; 55 | } 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/Image/Image.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from "axios"; 3 | import PropTypes from "prop-types"; 4 | import { ImageContainer, ImageUpload } from "./Image.styles"; 5 | import { ReactComponent as ImageIcon } from "../../assets/icons/images.svg"; 6 | 7 | const Image = ({ setImage, loading }) => { 8 | const uploadImage = async (e) => { 9 | const formData = new FormData(); 10 | const { 11 | target: { files }, 12 | } = e; 13 | 14 | formData.append("file", files[0]); 15 | formData.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET); 16 | 17 | const { data } = await axios.request({ 18 | method: "POST", 19 | url: process.env.REACT_APP_CLOUDINARY_URL, 20 | data: formData, 21 | onUploadProgress: (p) => { 22 | const progress = p.loaded / p.total; 23 | loading(progress); 24 | }, 25 | }); 26 | 27 | setImage(data); 28 | }; 29 | 30 | return ( 31 | 32 | 36 | 37 | ); 38 | }; 39 | 40 | export default Image; 41 | 42 | Image.propTypes = { 43 | setImage: PropTypes.func, 44 | loading: PropTypes.func, 45 | }; 46 | 47 | Image.defaultProps = { 48 | setImage: null, 49 | loading: null, 50 | }; 51 | -------------------------------------------------------------------------------- /src/pages/FriendsPage/FriendsPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoContainer = styled.div` 4 | display: flex; 5 | height: 100%; 6 | font-family: Roboto; 7 | padding: 28px 0; 8 | `; 9 | export const FriendsContainer = styled.div` 10 | display: flex; 11 | flex-wrap: wrap; 12 | justify-content: space-between; 13 | `; 14 | 15 | export const FixedContainer = styled.div` 16 | width: 876px; 17 | height: 100%; 18 | box-sizing: border-box; 19 | display: flex; 20 | flex-direction: column; 21 | background-color: #fff; 22 | @media only screen and (max-width: 767px) { 23 | justify-content: center; 24 | width: 100%; 25 | } 26 | margin: 0 auto; 27 | padding: 16px; 28 | box-shadow: ${(props) => props.theme.boxShadow2}; 29 | border-radius: 6px; 30 | @media only screen and (max-width: 575px) { 31 | border-radius: 0; 32 | } 33 | `; 34 | 35 | export const FriendsHeading = styled.h1` 36 | font-size: 2rem; 37 | font-weight: bold; 38 | margin-bottom: 30px; 39 | color: ${(props) => props.theme.secondaryText}; 40 | `; 41 | 42 | export const FriendSkeleton = styled.div` 43 | display: flex; 44 | width: calc(50% - 10px); 45 | height: 112px; 46 | border-radius: 6px; 47 | box-sizing: border-box; 48 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 49 | padding: 13px 0; 50 | 51 | @media only screen and (max-width: 700px) { 52 | margin-top: 10px; 53 | width: 100%; 54 | } 55 | 56 | svg { 57 | width: 100%; 58 | height: 100%; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/Notification/Notification.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const NotificationContainer = styled.div` 4 | display: flex; 5 | padding: 8px; 6 | border-radius: 8px; 7 | transition: 0.1s; 8 | font-family: Roboto; 9 | cursor: pointer; 10 | 11 | &:hover { 12 | background-color: ${(props) => props.theme.tertiaryBackground}; 13 | } 14 | 15 | &:last-child { 16 | margin-bottom: 12px; 17 | } 18 | 19 | &:active { 20 | background-color: ${(props) => props.theme.secondaryBackground}; 21 | } 22 | `; 23 | 24 | export const CreatorAvatar = styled.img` 25 | && { 26 | width: 56px; 27 | height: 56px; 28 | border-radius: 100%; 29 | object-fit: cover; 30 | margin-right: 12px; 31 | } 32 | `; 33 | 34 | export const CreatorFullName = styled.span` 35 | font-weight: bold; 36 | color: ${(props) => props.theme.secondaryText}; 37 | `; 38 | 39 | export const TextContainer = styled.div` 40 | font-size: 1.5rem; 41 | color: ${(props) => props.theme.secondaryText}; 42 | `; 43 | 44 | export const Body = styled(TextContainer)` 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: center; 48 | font-weight: normal; 49 | `; 50 | 51 | export const Timestamp = styled.h3` 52 | color: ${(props) => props.theme.primaryText}; 53 | font-weight: bold; 54 | font-size: 1.3rem; 55 | display: flex; 56 | margin-top: 5px; 57 | `; 58 | 59 | export const NotificationHeading = styled(TextContainer)` 60 | font-weight: medium; 61 | padding: 0 16px; 62 | margin-bottom: 8px; 63 | margin-top: 8px; 64 | `; 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | 21 | ## Pull Request Squashing 22 | 23 | Please, go through these steps before you submit a PR. 24 | 25 | 1. Make sure that your PR is not a duplicate. 26 | 2. If not, then make sure that: 27 | 28 | 2.1. You have done your changes in a separate branch. Branches MUST have descriptive names that start with either the `fix/` or `feature/` prefixes. Good examples are: `fix/signin-issue` or `feature/issue-templates`. 29 | 30 | 2.2. You have a descriptive commit message with a short title (first line). 31 | 32 | 2.3. You have only one commit (if not, squash them into one commit). 33 | 34 | 2.4. `npm run lint` doesn't throw any error. If it does, fix them first and amend your commit (`git commit --amend`). 35 | 36 | 3. **After** these steps, you're ready to open a pull request. 37 | 38 | 3.1. Give a descriptive title to your PR. 39 | 40 | 3.2. Provide a description of your changes. 41 | 42 | 3.3. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). 43 | -------------------------------------------------------------------------------- /src/components/About/About.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { 5 | AboutContainer, 6 | AboutHeading, 7 | HomeContainer, 8 | InfoBody, 9 | StyledButton, 10 | WorkplaceContainer, 11 | } from "./About.styles"; 12 | import { ReactComponent as HomeIcon } from "../../assets/icons/home.svg"; 13 | import { ReactComponent as WorkplaceIcon } from "../../assets/icons/briefcase.svg"; 14 | 15 | const About = ({ readOnly, user }) => { 16 | return ( 17 | 18 | Intro 19 | {user.workPlace && ( 20 | 21 | 22 | 23 | Works at{" "} 24 | {user.workPlace} 25 | 26 | 27 | )} 28 | {user.homePlace && ( 29 | 30 | 31 | 32 | Lives in{" "} 33 | {user.homePlace} 34 | 35 | 36 | )} 37 | {!readOnly && ( 38 | 39 | Edit Details 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default About; 47 | 48 | About.propTypes = { 49 | user: PropTypes.shape({ 50 | coverImage: PropTypes.string, 51 | avatarImage: PropTypes.string, 52 | firstName: PropTypes.string, 53 | lastName: PropTypes.string, 54 | workPlace: PropTypes.string, 55 | homePlace: PropTypes.string, 56 | username: PropTypes.string, 57 | }), 58 | readOnly: PropTypes.bool, 59 | }; 60 | 61 | About.defaultProps = { 62 | user: null, 63 | readOnly: null, 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/react-hooks": "^3.1.5", 7 | "apollo-cache-inmemory": "^1.6.5", 8 | "apollo-client": "^2.6.8", 9 | "apollo-link": "^1.2.14", 10 | "apollo-link-context": "^1.0.20", 11 | "apollo-link-http": "^1.5.17", 12 | "apollo-link-ws": "^1.0.20", 13 | "apollo-utilities": "^1.3.3", 14 | "axios": "^0.19.2", 15 | "graphql": "^15.0.0", 16 | "graphql-tag": "^2.10.3", 17 | "moment": "^2.25.1", 18 | "prop-types": "^15.7.2", 19 | "react": "^16.13.1", 20 | "react-content-loader": "^5.0.4", 21 | "react-dom": "^16.13.1", 22 | "react-hook-form": "^5.6.1", 23 | "react-loader-spinner": "^3.1.14", 24 | "react-markdown": "^4.3.1", 25 | "react-router-dom": "^5.1.2", 26 | "react-scripts": "3.4.1", 27 | "react-toastify": "^5.5.0", 28 | "reactjs-popup": "^1.5.0", 29 | "styled-components": "^5.1.0", 30 | "styled-normalize": "^8.0.7", 31 | "subscriptions-transport-ws": "^0.9.16" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test", 37 | "eject": "react-scripts eject", 38 | "lint": "eslint .", 39 | "lint:fix": "eslint . --fix" 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 | "devDependencies": { 57 | "eslint-config-airbnb": "^18.1.0", 58 | "eslint-config-prettier": "^6.11.0", 59 | "eslint-plugin-import": "^2.20.2", 60 | "eslint-plugin-jsx-a11y": "^6.2.3", 61 | "eslint-plugin-prettier": "^3.1.3", 62 | "eslint-plugin-react": "^7.19.0", 63 | "prettier": "^2.0.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/About/About.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AboutContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | width: 370px; 7 | box-sizing: border-box; 8 | height: 100%; 9 | background-color: #fff; 10 | box-shadow: ${(props) => props.theme.boxShadow2}; 11 | border-radius: 6px; 12 | margin-right: 16px; 13 | padding: 12px; 14 | font-family: Roboto; 15 | 16 | @media only screen and (max-width: 1000px) { 17 | width: 100%; 18 | } 19 | @media only screen and (max-width: 575px) { 20 | border-radius: 0; 21 | } 22 | `; 23 | 24 | export const AboutHeading = styled.h1` 25 | font-size: 2rem; 26 | font-weight: bold; 27 | color: ${(props) => props.theme.secondaryText}; 28 | margin-bottom: 12px; 29 | `; 30 | 31 | export const WorkplaceContainer = styled.div` 32 | display: flex; 33 | align-items: center; 34 | margin-bottom: 10px; 35 | `; 36 | 37 | export const HomeContainer = styled.div` 38 | display: flex; 39 | align-items: center; 40 | margin-bottom: 10px; 41 | `; 42 | 43 | export const InfoBody = styled.p` 44 | margin: 0; 45 | margin-left: 12px; 46 | margin-top: 3px; 47 | line-height: 1; 48 | font-size: 1.5rem; 49 | color: ${(props) => props.theme.secondaryText}; 50 | `; 51 | 52 | export const StyledButton = styled.button` 53 | background-color: ${(props) => props.theme.secondaryBackground}; 54 | border-radius: 4px; 55 | border: 0; 56 | font-size: 1.5rem; 57 | font-weight: 600; 58 | color: ${(props) => props.theme.secondaryText}; 59 | cursor: pointer; 60 | transition: 0.1s; 61 | height: 3.5rem; 62 | margin-top: 12px; 63 | width: 100%; 64 | 65 | &&:focus { 66 | background-color: ${(props) => props.theme.secondaryBackground}; 67 | outline: none; 68 | } 69 | &&:active { 70 | transform: scale(0.96); 71 | } 72 | 73 | &&:hover { 74 | color: ${(props) => props.theme.secondaryText}; 75 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Fakebooker 28 | 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/pages/AuthPages/LoginPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { ReactComponent as BgImg } from "../../assets/icons/auth-background.svg"; 3 | 4 | const inverseRotate = keyframes` 5 | from { 6 | transform: rotate(0); 7 | } 8 | to { 9 | transform: rotate(-360deg) ; 10 | } 11 | `; 12 | 13 | const rotate = keyframes` 14 | from { 15 | transform: rotate(0); 16 | } 17 | to { 18 | transform: rotate(360deg) ; 19 | } 20 | `; 21 | 22 | const dash = keyframes` 23 | to { 24 | stroke-dashoffset: 1000; 25 | } 26 | `; 27 | 28 | export const LoginPageContainer = styled.div` 29 | display: flex; 30 | width: 100%; 31 | height: 100%; 32 | background-color: #fff; 33 | `; 34 | export const SVGImgBackground = styled(BgImg)` 35 | display: none; 36 | @media only screen and (min-width: 1100px) { 37 | width: 100%; 38 | height: 100vh; 39 | display: block; 40 | } 41 | .gear1 { 42 | animation: ${inverseRotate} infinite 5s linear; 43 | transform-origin: center; 44 | transform-box: fill-box; 45 | height: 25rem; 46 | width: 25rem; 47 | display: inline-block; 48 | margin: auto; 49 | } 50 | .gear3 { 51 | animation: ${rotate} infinite 5s linear; 52 | transform-origin: center; 53 | transform-box: fill-box; 54 | height: 25rem; 55 | width: 25rem; 56 | display: inline-block; 57 | margin: auto; 58 | } 59 | 60 | .line { 61 | stroke-dasharray: 30; 62 | animation: ${dash} infinite 10s linear; 63 | } 64 | `; 65 | 66 | export const LoginPageBackground = styled.div` 67 | @media only screen and (min-width: 1100px) { 68 | display: flex; 69 | width: 85%; 70 | height: 100%; 71 | background-color: rgba(25, 119, 243, 0.1); 72 | } 73 | `; 74 | 75 | export const FormContainer = styled.div` 76 | display: flex; 77 | width: 100%; 78 | height: 100%; 79 | justify-content: center; 80 | align-items: center; 81 | `; 82 | 83 | export const ActionsContainer = styled.div` 84 | display: flex; 85 | flex-direction: column; 86 | width: 100%; 87 | height: 100vh; 88 | justify-content: center; 89 | align-items: center; 90 | `; 91 | -------------------------------------------------------------------------------- /src/pages/AuthPages/RegisterPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { ReactComponent as BgImg } from "../../assets/icons/auth-background.svg"; 3 | 4 | const inverseRotate = keyframes` 5 | from { 6 | transform: rotate(0); 7 | } 8 | to { 9 | transform: rotate(-360deg) ; 10 | } 11 | `; 12 | 13 | const rotate = keyframes` 14 | from { 15 | transform: rotate(0); 16 | } 17 | to { 18 | transform: rotate(360deg) ; 19 | } 20 | `; 21 | 22 | const dash = keyframes` 23 | to { 24 | stroke-dashoffset: 1000; 25 | } 26 | `; 27 | 28 | export const RegisterPageContainer = styled.div` 29 | display: flex; 30 | width: 100%; 31 | height: 100%; 32 | background-color: #fff; 33 | `; 34 | export const SVGImgBackground = styled(BgImg)` 35 | display: none; 36 | @media only screen and (min-width: 1100px) { 37 | width: 100%; 38 | height: 100vh; 39 | display: block; 40 | } 41 | .gear1 { 42 | animation: ${inverseRotate} infinite 5s linear; 43 | transform-origin: center; 44 | transform-box: fill-box; 45 | height: 25rem; 46 | width: 25rem; 47 | display: inline-block; 48 | margin: auto; 49 | } 50 | .gear3 { 51 | animation: ${rotate} infinite 5s linear; 52 | transform-origin: center; 53 | transform-box: fill-box; 54 | height: 25rem; 55 | width: 25rem; 56 | display: inline-block; 57 | margin: auto; 58 | } 59 | 60 | .line { 61 | stroke-dasharray: 30; 62 | animation: ${dash} infinite 10s linear; 63 | } 64 | `; 65 | 66 | export const RegisterPageBackground = styled.div` 67 | @media only screen and (min-width: 1100px) { 68 | display: flex; 69 | width: 85%; 70 | height: 100%; 71 | background-color: rgba(25, 119, 243, 0.1); 72 | } 73 | `; 74 | 75 | export const FormContainer = styled.div` 76 | display: flex; 77 | width: 100%; 78 | height: 100%; 79 | justify-content: center; 80 | align-items: center; 81 | `; 82 | 83 | export const ActionsContainer = styled.div` 84 | display: flex; 85 | flex-direction: column; 86 | width: 100%; 87 | height: 100vh; 88 | justify-content: center; 89 | align-items: center; 90 | `; 91 | -------------------------------------------------------------------------------- /src/ApolloProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WebSocketLink } from "apollo-link-ws"; 3 | import { split } from "apollo-link"; 4 | import { getMainDefinition } from "apollo-utilities"; 5 | import ApolloClient from "apollo-client"; 6 | import { InMemoryCache } from "apollo-cache-inmemory"; 7 | import { createHttpLink } from "apollo-link-http"; 8 | import { setContext } from "apollo-link-context"; 9 | import { ApolloProvider } from "@apollo/react-hooks"; 10 | import App from "./App"; 11 | 12 | const cache = new InMemoryCache(); 13 | 14 | const wsLink = new WebSocketLink({ 15 | uri: process.env.REACT_APP_WSS_LINK, 16 | options: { 17 | reconnect: true, 18 | connectionParams: { 19 | headers: { 20 | Authorization: localStorage.getItem("token") 21 | ? `JWT ${localStorage.getItem("token")}` 22 | : "", 23 | }, 24 | }, 25 | }, 26 | }); 27 | 28 | const httpLink = createHttpLink({ 29 | uri: process.env.REACT_APP_HTTP_LINK, 30 | }); 31 | 32 | const authLink = setContext((_, { headers }) => { 33 | const token = localStorage.getItem("token"); 34 | 35 | // return the headers to the context so httpLink can read them 36 | return { 37 | headers: { 38 | ...headers, 39 | authorization: token ? `JWT ${token}` : "", 40 | }, 41 | }; 42 | }); 43 | 44 | const link = split( 45 | // split based on operation type 46 | ({ query }) => { 47 | const definition = getMainDefinition(query); 48 | return ( 49 | definition.kind === "OperationDefinition" && 50 | definition.operation === "subscription" 51 | ); 52 | }, 53 | wsLink, 54 | authLink.concat(httpLink) 55 | ); 56 | 57 | const client = new ApolloClient({ 58 | link, 59 | cache, 60 | resolvers: {}, 61 | }); 62 | 63 | cache.writeData({ 64 | data: { 65 | chat: { 66 | visible: false, 67 | user: { 68 | firstName: null, 69 | lastName: null, 70 | avatarImage: null, 71 | id: null, 72 | __typename: "User", 73 | }, 74 | __typename: "Chat", 75 | }, 76 | }, 77 | }); 78 | export default ( 79 | 80 | 81 | 82 | ); 83 | -------------------------------------------------------------------------------- /src/components/Comment/Comment.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CommentContainer = styled.div` 4 | display: flex; 5 | margin: 10px 0; 6 | position: relative; 7 | font-family: Roboto; 8 | align-items: center; 9 | `; 10 | 11 | export const CommentAvatar = styled.img` 12 | border-radius: 100%; 13 | width: 32px; 14 | height: 32px; 15 | flex-shrink: 0; 16 | align-self: flex-start; 17 | `; 18 | 19 | export const ActionsContainer = styled.div` 20 | display: flex; 21 | border-radius: 8px; 22 | width: 100%; 23 | z-index: 20; 24 | flex-direction: column; 25 | 26 | @media only screen and (max-width: 575px) { 27 | width: 200px; 28 | } 29 | `; 30 | 31 | export const PopButton = styled.button` 32 | text-align: left; 33 | font-size: 1.5rem; 34 | border: none; 35 | padding: 8px; 36 | border-radius: 6px; 37 | display: flex; 38 | width: 100%; 39 | background-color: #fff; 40 | color: ${(props) => props.theme.secondaryText}; 41 | transition: 0.1s; 42 | font-weight: 500; 43 | cursor: pointer; 44 | 45 | &:hover { 46 | background-color: ${(props) => props.theme.tertiaryBackground}; 47 | color: ${(props) => props.theme.secondaryText}; 48 | } 49 | 50 | &::after, 51 | &:focus { 52 | outline: none; 53 | color: ${(props) => props.theme.secondaryText}; 54 | } 55 | 56 | &:active { 57 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 58 | color: ${(props) => props.theme.secondaryText}; 59 | } 60 | `; 61 | 62 | export const BodyContainer = styled.div` 63 | display: flex; 64 | flex-direction: column; 65 | border: none; 66 | border-radius: 18px; 67 | width: auto; 68 | padding: 8px 12px; 69 | color: ${(props) => props.theme.secondaryText}; 70 | background-color: ${(props) => props.theme.inputColor}; 71 | margin-left: 5px; 72 | margin-right: 10px; 73 | `; 74 | 75 | export const Username = styled.span` 76 | color: ${(props) => props.theme.secondaryText}; 77 | font-weight: 600; 78 | font-size: 1.5rem; 79 | cursor: pointer; 80 | 81 | &:hover { 82 | text-decoration: underline; 83 | } 84 | `; 85 | 86 | export const Body = styled.span` 87 | font-size: 1.5rem; 88 | word-break: break-word; 89 | `; 90 | -------------------------------------------------------------------------------- /src/routes/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Switch, 6 | Redirect, 7 | } from "react-router-dom"; 8 | import { ToastContainer } from "react-toastify"; 9 | import "react-toastify/dist/ReactToastify.css"; 10 | import RegisterPage from "../pages/AuthPages/RegisterPage"; 11 | import LoginPage from "../pages/AuthPages/LoginPage"; 12 | import ProfilePage from "../pages/ProfilePage/ProfilePage"; 13 | import PhotosPage from "../pages/PhotosPage/PhotosPage"; 14 | import FriendsPage from "../pages/FriendsPage/FriendsPage"; 15 | import AboutOverview from "../pages/AboutPage/AboutOverview"; 16 | import AboutWorkAndEducation from "../pages/AboutPage/AboutWorkAndEducation"; 17 | import AboutContactAndBasicInfo from "../pages/AboutPage/AboutContactAndBasicInfo"; 18 | import PrivateRoute from "./PrivateRoute"; 19 | import ProfileRoute from "./ProfileRoute"; 20 | import NewsfeedPage from "../pages/NewsfeedPage/NewsfeedPage"; 21 | import SinglePostPage from "../pages/SinglePostPage/SinglePostPage"; 22 | 23 | const Routes = () => { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 41 | 46 | 47 | 48 | } /> 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default Routes; 56 | -------------------------------------------------------------------------------- /src/components/Notification/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import moment from "moment/moment"; 4 | import PropTypes from "prop-types"; 5 | import { 6 | NotificationContainer, 7 | NotificationHeading, 8 | CreatorFullName, 9 | CreatorAvatar, 10 | Body, 11 | Timestamp, 12 | } from "./Notification.styles"; 13 | 14 | const Notification = ({ 15 | notification: { action, creator, actionId, createdAt }, 16 | alert, 17 | }) => ( 18 | <> 19 | {alert && New Notification} 20 | {actionId ? ( 21 | 22 | 23 | 24 | 25 |
26 | 27 | {creator.firstName} {creator.lastName}{" "} 28 | 29 | {action} 30 |
31 | {moment(Number(createdAt)).fromNow()} 32 | 33 |
34 | 35 | ) : ( 36 | 37 | 38 | 39 | 40 |
41 | 42 | {creator.firstName} {creator.lastName}{" "} 43 | 44 | {action} 45 |
46 | {moment(Number(createdAt)).fromNow()} 47 | 48 |
49 | 50 | )} 51 | 52 | ); 53 | export default Notification; 54 | 55 | Notification.propTypes = { 56 | notification: PropTypes.shape({ 57 | action: PropTypes.string, 58 | creator: PropTypes.shape({ 59 | firstName: PropTypes.string, 60 | lastName: PropTypes.string, 61 | avatarImage: PropTypes.string, 62 | username: PropTypes.string, 63 | }), 64 | actionId: PropTypes.shape({ 65 | body: PropTypes.string, 66 | id: PropTypes.string, 67 | }), 68 | createdAt: PropTypes.string, 69 | }), 70 | alert: PropTypes.bool, 71 | }; 72 | 73 | Notification.defaultProps = { 74 | notification: null, 75 | alert: null, 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/Image/ChangeAvatarPhoto.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-for */ 2 | import React, { useState } from "react"; 3 | import axios from "axios"; 4 | import { useMutation } from "@apollo/react-hooks"; 5 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 6 | import Loader from "react-loader-spinner"; 7 | import { CHANGE_AVATAR_IMAGE } from "../../utils/queries"; 8 | import { 9 | ChangeAvatarContainer, 10 | AvatarImageUpload, 11 | } from "./ChangeAvatarPhoto.styles"; 12 | import { ReactComponent as CameraIcon } from "../../assets/icons/camera.svg"; 13 | 14 | const ChangeAvatarPhoto = () => { 15 | const [avatarImageLoading, setAvatarImageLoading] = useState(null); 16 | const [changeAvatarImage] = useMutation(CHANGE_AVATAR_IMAGE); 17 | 18 | const uploadImage = async (e) => { 19 | const formData = new FormData(); 20 | const { 21 | target: { files }, 22 | } = e; 23 | 24 | formData.append("file", files[0]); 25 | formData.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET); 26 | 27 | const { data } = await axios.request({ 28 | method: "POST", 29 | url: process.env.REACT_APP_CLOUDINARY_URL, 30 | data: formData, 31 | onUploadProgress: (p) => { 32 | const progress = p.loaded / p.total; 33 | setAvatarImageLoading(progress); 34 | }, 35 | }); 36 | 37 | changeAvatarImage({ 38 | variables: { 39 | avatarImage: data.secure_url, 40 | }, 41 | }); 42 | }; 43 | 44 | return ( 45 | 46 | 70 | 71 | ); 72 | }; 73 | 74 | export default ChangeAvatarPhoto; 75 | -------------------------------------------------------------------------------- /src/components/Friend/Friend.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMutation } from "@apollo/react-hooks"; 3 | import PropTypes from "prop-types"; 4 | import Popup from "reactjs-popup"; 5 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 6 | import Loader from "react-loader-spinner"; 7 | import { 8 | FriendContainer, 9 | UserContainer, 10 | UserImg, 11 | UserName, 12 | FriendBtn, 13 | ActionsContainer, 14 | RemoveFriendBtn, 15 | } from "./Friend.styles"; 16 | import { REMOVE_FRIEND } from "../../utils/queries"; 17 | 18 | const Friend = ({ user, readOnly }) => { 19 | const [removeFriend, { loading }] = useMutation(REMOVE_FRIEND, { 20 | variables: { 21 | creator: user.username, 22 | }, 23 | }); 24 | 25 | const RemoveFriend = () => ( 26 | 27 | 28 | Unfriend 29 | {loading && ( 30 | 41 | )} 42 | 43 | 44 | ); 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | {user.firstName} {user.lastName} 52 | 53 | 54 | {!readOnly ? ( 55 | Friends} 59 | closeOnDocumentClick 60 | on="click" 61 | > 62 | 63 | 64 | ) : ( 65 | Friends 66 | )} 67 | 68 | ); 69 | }; 70 | 71 | export default Friend; 72 | 73 | Friend.propTypes = { 74 | user: PropTypes.shape({ 75 | firstName: PropTypes.string, 76 | lastName: PropTypes.string, 77 | avatarImage: PropTypes.string, 78 | username: PropTypes.string, 79 | }), 80 | readOnly: PropTypes.bool, 81 | }; 82 | 83 | Friend.defaultProps = { 84 | user: null, 85 | readOnly: null, 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/Image/ChangeCoverPhoto.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-for */ 2 | import React, { useState } from "react"; 3 | import axios from "axios"; 4 | import { useMutation } from "@apollo/react-hooks"; 5 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 6 | import Loader from "react-loader-spinner"; 7 | import { CHANGE_COVER_IMAGE } from "../../utils/queries"; 8 | import { 9 | ChangePhotoContainer, 10 | ChangeBackgroundHeading, 11 | CoverImageUpload, 12 | } from "./ChangeCoverPhoto.styles"; 13 | import { ReactComponent as CameraIcon } from "../../assets/icons/camera.svg"; 14 | 15 | const ChangeCoverPhoto = () => { 16 | const [coverImageLoading, setCoverImageLoading] = useState(null); 17 | const [changeCoverImage] = useMutation(CHANGE_COVER_IMAGE); 18 | 19 | const uploadImage = async (e) => { 20 | const formData = new FormData(); 21 | const { 22 | target: { files }, 23 | } = e; 24 | 25 | formData.append("file", files[0]); 26 | formData.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET); 27 | 28 | const { data } = await axios.request({ 29 | method: "POST", 30 | url: process.env.REACT_APP_CLOUDINARY_URL, 31 | data: formData, 32 | onUploadProgress: (p) => { 33 | const progress = p.loaded / p.total; 34 | setCoverImageLoading(progress); 35 | }, 36 | }); 37 | 38 | changeCoverImage({ 39 | variables: { 40 | coverImage: data.secure_url, 41 | }, 42 | }); 43 | }; 44 | 45 | return ( 46 | 47 | 72 | 73 | ); 74 | }; 75 | 76 | export default ChangeCoverPhoto; 77 | -------------------------------------------------------------------------------- /src/routes/layouts/ProfileLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import ContentLoader from "react-content-loader"; 4 | import PropTypes from "prop-types"; 5 | import { useQuery } from "@apollo/react-hooks"; 6 | import Navbar from "../../components/Navbar/Navbar"; 7 | import ProfileHeader from "../../components/ProfileHeader/ProfileHeader"; 8 | import { LOAD_USER, LOAD_FROM_URL_USER } from "../../utils/queries"; 9 | import { ProfileHeaderSkeleton, NavbarSkeleton } from "./ProfileLayout.styles"; 10 | 11 | const ProfileLayout = ({ children }) => { 12 | const [pageLoading, setPageLoading] = useState(true); 13 | 14 | const { data: userData } = useQuery(LOAD_USER); 15 | useEffect(() => { 16 | setPageLoading(false); 17 | }, []); 18 | 19 | const { username } = useParams(); 20 | 21 | /* eslint-disable consistent-return */ 22 | const readOnly = () => { 23 | if (userData) { 24 | if (userData.loadUser.username !== username) { 25 | return true; 26 | // eslint-disable-next-line no-else-return 27 | } else { 28 | return false; 29 | } 30 | } 31 | }; 32 | 33 | // skip this when on auth profile 34 | const { data: profileData } = useQuery(LOAD_FROM_URL_USER, { 35 | variables: { 36 | username, 37 | }, 38 | }); 39 | 40 | return ( 41 | <> 42 | {!pageLoading && userData ? ( 43 | 44 | ) : ( 45 | 46 | 47 | 48 | 49 | 50 | )} 51 | {!pageLoading && userData ? ( 52 | 57 | ) : ( 58 | 59 | 60 | 61 | 62 | 63 | )} 64 | {children} 65 | 66 | ); 67 | }; 68 | 69 | export default ProfileLayout; 70 | 71 | ProfileLayout.propTypes = { 72 | children: PropTypes.node, 73 | }; 74 | 75 | ProfileLayout.defaultProps = { 76 | children: null, 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/Friend/Friend.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const FriendContainer = styled.div` 4 | display: inline-flex; 5 | width: calc(50% - 10px); 6 | position: relative; 7 | align-items: center; 8 | padding: 16px; 9 | border-radius: 8px; 10 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 11 | margin: 3px; 12 | box-sizing: border-box; 13 | 14 | @media only screen and (max-width: 700px) { 15 | width: 100%; 16 | } 17 | `; 18 | 19 | export const UserContainer = styled.div` 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | `; 24 | 25 | export const UserImg = styled.img` 26 | width: 80px; 27 | height: 80px; 28 | border-radius: 8px; 29 | `; 30 | 31 | export const UserName = styled.h1` 32 | margin-left: 16px; 33 | font-size: 1.7rem; 34 | font-weight: 500; 35 | color: ${(props) => props.theme.secondaryText}; 36 | `; 37 | 38 | export const Button = styled.button` 39 | border: none; 40 | padding: 8px 12px; 41 | border-radius: 6px; 42 | color: ${(props) => props.theme.secondaryText}; 43 | font-weight: 500; 44 | font-size: 1.5rem; 45 | transition: 0.1s; 46 | cursor: pointer; 47 | `; 48 | 49 | export const FriendBtn = styled(Button)` 50 | margin-left: auto; 51 | background-color: ${(props) => props.theme.secondaryBackground}; 52 | 53 | &&:focus { 54 | background-color: ${(props) => props.theme.secondaryBackground}; 55 | color: ${(props) => props.theme.secondaryText}; 56 | outline: none; 57 | } 58 | 59 | &&:hover { 60 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 61 | color: ${(props) => props.theme.secondaryText}; 62 | } 63 | 64 | &&:active { 65 | transform: scale(0.96); 66 | } 67 | `; 68 | 69 | export const ActionsContainer = styled.div` 70 | display: flex; 71 | flex-direction: column; 72 | width: 100%; 73 | `; 74 | 75 | export const RemoveFriendBtn = styled(Button)` 76 | text-align: left; 77 | padding: 8px; 78 | border-radius: 6px; 79 | display: flex; 80 | width: 100%; 81 | background-color: #fff; 82 | cursor: pointer; 83 | 84 | &:hover { 85 | background-color: ${(props) => props.theme.tertiaryBackground}; 86 | color: ${(props) => props.theme.secondaryText}; 87 | } 88 | 89 | &::after, 90 | &:focus { 91 | outline: none; 92 | color: ${(props) => props.theme.secondaryText}; 93 | } 94 | 95 | &:active { 96 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 97 | color: ${(props) => props.theme.secondaryText}; 98 | } 99 | `; 100 | -------------------------------------------------------------------------------- /src/pages/PhotosPage/PhotosPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | import { useParams } from "react-router-dom"; 4 | import { useQuery } from "@apollo/react-hooks"; 5 | import { 6 | InfoContainer, 7 | FixedContainer, 8 | PhotosHeading, 9 | PhotosContainer, 10 | Photo, 11 | PhotosSkeleton, 12 | } from "./PhotosPage.styles"; 13 | import { LOAD_USER, GET_POSTS, GET_URL_POSTS } from "../../utils/queries"; 14 | 15 | const PhotosPage = () => { 16 | const { data: userData } = useQuery(LOAD_USER); 17 | 18 | const { username } = useParams(); 19 | 20 | const { data: postsData, loading: postsLoading } = useQuery(GET_POSTS); 21 | 22 | const { data: urlPostsData, loading: urlPostsLoading } = useQuery( 23 | GET_URL_POSTS, 24 | { 25 | variables: { 26 | username, 27 | }, 28 | } 29 | ); 30 | 31 | /* eslint-disable consistent-return */ 32 | const readOnly = () => { 33 | if (userData) { 34 | if (userData.loadUser.username !== username) { 35 | return true; 36 | // eslint-disable-next-line no-else-return 37 | } else { 38 | return false; 39 | } 40 | } 41 | }; 42 | 43 | return ( 44 | 45 | 46 | Photos 47 | 48 | {!readOnly() && 49 | postsData && 50 | postsData.getPosts.map( 51 | (post) => post.image && 52 | )} 53 | {!readOnly() && postsLoading && ( 54 | 55 | 60 | 61 | 62 | 63 | )} 64 | {readOnly() && 65 | urlPostsData && 66 | urlPostsData.getUrlPosts.map( 67 | (post) => post.image && 68 | )} 69 | {readOnly() && urlPostsLoading && ( 70 | 71 | 76 | 77 | 78 | 79 | )} 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default PhotosPage; 87 | -------------------------------------------------------------------------------- /src/components/Post/CreatePostDefault.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Popup from "reactjs-popup"; 3 | 4 | export const CreatePostContainer = styled.div` 5 | display: flex; 6 | font-family: "Roboto"; 7 | background-color: #fff; 8 | padding: 8px 12px; 9 | max-width: ${(props) => (props.newsfeed ? "680px" : "500px")}; 10 | box-shadow: ${(props) => props.theme.boxShadow2}; 11 | border-radius: 8px; 12 | @media only screen and (max-width: 575px) { 13 | border-radius: 0; 14 | } 15 | align-items: center; 16 | @media only screen and (max-width: 575px) { 17 | margin: 0 auto; 18 | } 19 | `; 20 | 21 | export const UserAvatar = styled.img` 22 | width: 40px; 23 | height: 40px; 24 | border-radius: 50%; 25 | margin-right: 8px; 26 | `; 27 | 28 | export const CreatePostButton = styled.button` 29 | border: none; 30 | color: ${(props) => props.theme.secondaryText}; 31 | font-weight: 600; 32 | font-size: 1.5rem; 33 | background-color: ${(props) => props.theme.secondaryBackground}; 34 | border-radius: 4px; 35 | width: 100%; 36 | cursor: pointer; 37 | line-height: 1; 38 | transition: 0.1s; 39 | height: 3.5rem; 40 | 41 | &:focus { 42 | background-color: ${(props) => props.theme.secondaryBackground}; 43 | color: ${(props) => props.theme.secondaryText}; 44 | outline: none; 45 | } 46 | &:active { 47 | transform: scale(0.96); 48 | } 49 | &:hover { 50 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 51 | color: ${(props) => props.theme.secondaryText}; 52 | } 53 | `; 54 | 55 | export const CloseContainer = styled.div` 56 | display: inline-flex; 57 | justify-content: center; 58 | align-items: center; 59 | border-radius: 50%; 60 | padding: 0; 61 | background-color: ${(props) => props.theme.secondaryBackground}; 62 | cursor: pointer; 63 | transition: 0.1s; 64 | padding: 5px; 65 | 66 | &:focus { 67 | background-color: ${(props) => props.theme.secondaryBackground}; 68 | color: ${(props) => props.theme.secondaryText}; 69 | } 70 | &:active { 71 | transform: scale(0.96); 72 | } 73 | &:hover { 74 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 75 | color: ${(props) => props.theme.secondaryText}; 76 | } 77 | `; 78 | 79 | export const StyledPopup = styled(Popup)` 80 | &-content { 81 | padding: 0px !important; 82 | border-radius: 6px; 83 | border: none !important; 84 | width: 488px !important; 85 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1), 86 | inset 0 0 0 1px rgba(255, 255, 255, 0.5); 87 | } 88 | &-overlay { 89 | background-color: rgba(244, 244, 244, 0.8) !important; 90 | } 91 | `; 92 | -------------------------------------------------------------------------------- /src/components/Message/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | import { useQuery } from "@apollo/react-hooks"; 4 | import Message from "./Message"; 5 | import { 6 | MessageListContainer, 7 | MessageListHeading, 8 | MessageRow, 9 | MessageListSkeleton, 10 | } from "./MessageList.styles"; 11 | import { GET_CONVERSATIONS } from "../../utils/queries"; 12 | 13 | const MessageList = () => { 14 | const { data, loading } = useQuery(GET_CONVERSATIONS); 15 | 16 | return ( 17 | 18 | Messenger 19 | {!loading ? ( 20 | 21 | {data && 22 | data.getConversations.map((conversation) => ( 23 | // eslint-disable-next-line no-underscore-dangle 24 | 25 | ))} 26 | 27 | ) : ( 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | )} 70 | 71 | ); 72 | }; 73 | 74 | export default MessageList; 75 | -------------------------------------------------------------------------------- /src/pages/SinglePostPage/SinglePostPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams, useHistory } from "react-router-dom"; 3 | import { useQuery } from "@apollo/react-hooks"; 4 | import ContentLoader from "react-content-loader"; 5 | import Navbar from "../../components/Navbar/Navbar"; 6 | import Post from "../../components/Post/Post"; 7 | import { LOAD_USER, GET_SINGLE_POST } from "../../utils/queries"; 8 | import { 9 | InfoContainer, 10 | PostsSection, 11 | PostContainer, 12 | NavbarSkeleton, 13 | PostSkeleton, 14 | } from "./SinglePostPage.styles"; 15 | 16 | const SinglePostPage = () => { 17 | const { data: userData } = useQuery(LOAD_USER); 18 | const [pageLoading, setPageLoading] = useState(true); 19 | 20 | const { postId } = useParams(); 21 | const history = useHistory(); 22 | 23 | const { data: postData, loading } = useQuery(GET_SINGLE_POST, { 24 | variables: { 25 | postId, 26 | }, 27 | // eslint-disable-next-line react/display-name 28 | onError: () => history.push("/"), 29 | }); 30 | 31 | useEffect(() => { 32 | setPageLoading(false); 33 | }, [pageLoading]); 34 | 35 | return ( 36 | <> 37 | {!pageLoading && userData ? ( 38 | 39 | ) : ( 40 | 41 | 42 | 43 | 44 | 45 | )} 46 | 47 | 48 | 49 | {postData && 50 | postData.getSinglePost && 51 | !pageLoading && 52 | !loading && 53 | userData ? ( 54 | 60 | ) : ( 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | )} 76 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default SinglePostPage; 84 | -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const LoginFormContainer = styled.div` 4 | width: 500px; 5 | font-family: "Roboto"; 6 | font-size: 1.5rem; 7 | @media only screen and (max-width: 767px) { 8 | padding: 0 20px; 9 | } 10 | `; 11 | 12 | export const StyledButton = styled.button` 13 | background-color: ${(props) => props.theme.primaryText}; 14 | color: #fff; 15 | position: relative; 16 | width: 50%; 17 | height: auto; 18 | padding: 13px 50px; 19 | border-radius: 4px; 20 | font-size: 1.6rem; 21 | border: none; 22 | cursor: pointer; 23 | 24 | &:focus { 25 | opacity: 0.9; 26 | outline: none; 27 | } 28 | &:focus, 29 | &:hover { 30 | background-color: ${(props) => props.theme.primaryText}; 31 | color: #fff; 32 | } 33 | 34 | &:active { 35 | color: #fff; 36 | border-color: none; 37 | transform: scale(0.96); 38 | } 39 | 40 | @media only screen and (max-width: 480px) { 41 | width: 100%; 42 | } 43 | `; 44 | 45 | export const LoginHeading = styled.p` 46 | font-size: 2.4rem; 47 | font-weight: 600; 48 | margin-bottom: 30px; 49 | color: ${(props) => props.theme.secondaryText}; 50 | `; 51 | 52 | export const Label = styled.p` 53 | display: inline-block; 54 | margin: 0; 55 | margin-bottom: 7px; 56 | font-size: 1.5rem; 57 | font-weight: 600; 58 | line-height: 1; 59 | color: ${(props) => props.theme.secondaryText}; 60 | `; 61 | 62 | export const Input = styled.input` 63 | background-color: ${(props) => props.theme.inputColor}; 64 | border: none; 65 | border-radius: 6px; 66 | height: 40px; 67 | padding: 0 8px; 68 | box-sizing: border-box; 69 | &:focus { 70 | outline: none; 71 | border: 2px solid ${(props) => props.theme.primaryText}; 72 | } 73 | `; 74 | 75 | export const InputContainer = styled.div` 76 | display: flex; 77 | flex-direction: column; 78 | margin-bottom: 20px; 79 | `; 80 | 81 | export const ErrorMessageContainer = styled.div` 82 | display: flex; 83 | align-items: center; 84 | margin-top: 4px; 85 | `; 86 | 87 | export const ErrorMessageHeading = styled.h1` 88 | color: ${(props) => props.theme.errorText}; 89 | font-size: 1.2rem; 90 | font-weight: 500; 91 | margin-left: 7px; 92 | `; 93 | 94 | export const RegisterContainer = styled.div` 95 | margin-top: 20px; 96 | color: ${(props) => props.theme.secondaryText}; 97 | font-weight: 400; 98 | font-size: 1.5rem; 99 | align-items: center; 100 | display: flex; 101 | 102 | @media only screen and (max-width: 480px) { 103 | justify-content: center; 104 | } 105 | `; 106 | export const RegisterLink = styled.h1` 107 | font-weight: 400; 108 | font-size: 1.5rem; 109 | margin-left: 4px; 110 | color: ${(props) => props.theme.primaryText}; 111 | `; 112 | -------------------------------------------------------------------------------- /src/components/Message/Message.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import React from "react"; 3 | import moment from "moment/moment"; 4 | import { useApolloClient, useQuery } from "@apollo/react-hooks"; 5 | import PropTypes from "prop-types"; 6 | import { 7 | MessageContainer, 8 | NotifierFullName, 9 | NotifierAvatar, 10 | Body, 11 | Footer, 12 | } from "./Message.styles"; 13 | import { LOAD_USER } from "../../utils/queries"; 14 | 15 | const Message = ({ 16 | conversation: { 17 | latestMessage: { creator, notifier, body, createdAt }, 18 | }, 19 | }) => { 20 | const client = useApolloClient(); 21 | 22 | const { data: authData } = useQuery(LOAD_USER); 23 | 24 | return ( 25 | { 27 | if (authData) { 28 | const userB = 29 | authData.loadUser.id === creator._id ? notifier : creator; 30 | 31 | client.writeData({ 32 | data: { 33 | chat: { 34 | visible: true, 35 | __typename: "Chat", 36 | user: { 37 | ...userB, 38 | // eslint-disable-next-line no-underscore-dangle 39 | id: userB._id, 40 | __typename: "User", 41 | }, 42 | }, 43 | }, 44 | }); 45 | } 46 | }} 47 | > 48 | {authData && authData.loadUser.id === notifier._id && ( 49 | <> 50 | 51 | 52 | 53 | {creator.firstName} {creator.lastName} 54 | 55 |
56 | {body} · {moment(Number(createdAt)).fromNow()} 57 |
58 | 59 | 60 | )} 61 | {authData && authData.loadUser.id === creator._id && ( 62 | <> 63 | 64 | 65 | 66 | {notifier.firstName} {notifier.lastName} 67 | 68 |
69 | {body} · {moment(Number(createdAt)).fromNow()} 70 |
71 | 72 | 73 | )} 74 |
75 | ); 76 | }; 77 | 78 | export default Message; 79 | 80 | Message.propTypes = { 81 | conversation: PropTypes.shape({ 82 | latestMessage: PropTypes.shape({ 83 | creator: PropTypes.shape({ 84 | firstName: PropTypes.string, 85 | lastName: PropTypes.string, 86 | avatarImage: PropTypes.string, 87 | }), 88 | notifier: PropTypes.shape({ 89 | firstName: PropTypes.string, 90 | lastName: PropTypes.string, 91 | avatarImage: PropTypes.string, 92 | }), 93 | createdAt: PropTypes.string, 94 | body: PropTypes.string, 95 | }), 96 | }), 97 | }; 98 | 99 | Message.defaultProps = { 100 | conversation: null, 101 | }; 102 | -------------------------------------------------------------------------------- /src/routes/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery, useSubscription } from "@apollo/react-hooks"; 3 | import PropTypes from "prop-types"; 4 | import { Route, Redirect } from "react-router-dom"; 5 | import { notificationAlert } from "../utils/alerts"; 6 | import { 7 | GET_NOTIFICATIONS, 8 | NEW_NOTIFICATION, 9 | DELETE_NOTIFICATION, 10 | LOAD_USER, 11 | NEW_MESSAGE, 12 | GET_THREAD, 13 | GET_SINGLE_CHAT, 14 | } from "../utils/queries"; 15 | 16 | const PrivateRoute = ({ component: Component, ...rest }) => { 17 | const token = localStorage.getItem("token"); 18 | 19 | useQuery(GET_NOTIFICATIONS); 20 | const { data: userData, loading } = useQuery(LOAD_USER); 21 | 22 | const { refetch: threadRefetch } = useQuery(GET_THREAD); 23 | 24 | const { refetch: singleChatRefetch } = useQuery(GET_SINGLE_CHAT); 25 | 26 | useSubscription(NEW_MESSAGE, { 27 | variables: { 28 | notifierId: userData && userData.loadUser.id, 29 | }, 30 | onSubscriptionData: async ({ subscriptionData }) => { 31 | const { data: newThreadData } = await threadRefetch({ 32 | urlUser: subscriptionData.data.newMessage.creator.id, 33 | }); 34 | 35 | await singleChatRefetch({ 36 | threadId: newThreadData.getThread.id, 37 | }); 38 | }, 39 | }); 40 | 41 | useSubscription(NEW_NOTIFICATION, { 42 | onSubscriptionData: ({ client, subscriptionData }) => { 43 | const data = client.readQuery({ 44 | query: GET_NOTIFICATIONS, 45 | }); 46 | if ( 47 | subscriptionData.data.newNotification.notifier.id === 48 | userData.loadUser.id 49 | ) { 50 | notificationAlert(subscriptionData.data.newNotification); 51 | const newData = { 52 | getNotifications: [ 53 | subscriptionData.data.newNotification, 54 | ...data.getNotifications, 55 | ], 56 | }; 57 | 58 | client.writeQuery({ 59 | query: GET_NOTIFICATIONS, 60 | data: newData, 61 | }); 62 | } 63 | }, 64 | }); 65 | 66 | useSubscription(DELETE_NOTIFICATION, { 67 | onSubscriptionData: ({ client, subscriptionData }) => { 68 | const data = client.readQuery({ 69 | query: GET_NOTIFICATIONS, 70 | }); 71 | 72 | const newNotificationList = data.getNotifications.filter( 73 | (item) => item.id !== subscriptionData.data.deleteNotification 74 | ); 75 | 76 | client.writeQuery({ 77 | query: GET_NOTIFICATIONS, 78 | data: { getNotifications: newNotificationList }, 79 | }); 80 | }, 81 | }); 82 | if (!loading && !userData) { 83 | localStorage.removeItem("token"); 84 | return ; 85 | } 86 | return ( 87 | 90 | token ? : 91 | } 92 | /> 93 | ); 94 | }; 95 | 96 | export default PrivateRoute; 97 | 98 | PrivateRoute.propTypes = { 99 | component: PropTypes.func, 100 | }; 101 | 102 | PrivateRoute.defaultProps = { 103 | component: null, 104 | }; 105 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutOverview.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AboutPageContainer = styled.div``; 4 | 5 | export const AboutInfoContainer = styled.div` 6 | display: flex; 7 | font-family: Roboto; 8 | justify-content: center; 9 | padding: 28px 0; 10 | `; 11 | 12 | export const AboutContainer = styled.div` 13 | display: flex; 14 | width: 876px; 15 | background-color: #fff; 16 | border-radius: 6px; 17 | @media only screen and (max-width: 575px) { 18 | border-radius: 0; 19 | } 20 | box-shadow: ${(props) => props.theme.boxShadow2}; 21 | `; 22 | 23 | export const AboutSidebar = styled.div` 24 | display: flex; 25 | flex-direction: column; 26 | width: 33%; 27 | border-right: 1px solid ${(props) => props.theme.secondaryBackground}; 28 | padding: 6px; 29 | `; 30 | 31 | export const AboutHeading = styled.p` 32 | font-size: 2rem; 33 | font-weight: bold; 34 | color: ${(props) => props.theme.secondaryText}; 35 | margin: 20px 10px; 36 | `; 37 | 38 | export const SidebarButton = styled.button` 39 | border: none; 40 | width: 100%; 41 | text-align: left; 42 | font-size: 1.5rem; 43 | padding: 10px; 44 | border-radius: 6px; 45 | cursor: pointer; 46 | font-weight: 600; 47 | margin-bottom: 8px; 48 | `; 49 | 50 | export const Overview = styled(SidebarButton)` 51 | color: ${(props) => props.theme.primaryText}; 52 | background-color: ${(props) => props.theme.primaryBackground}; 53 | 54 | &:focus { 55 | outline: none; 56 | } 57 | `; 58 | 59 | export const WorkAndEducation = styled(SidebarButton)` 60 | color: ${(props) => props.theme.tertiaryText}; 61 | background-color: #fff; 62 | 63 | &:hover { 64 | background-color: ${(props) => props.theme.tertiaryBackground}; 65 | outline: none; 66 | } 67 | 68 | &::after, 69 | &:focus { 70 | outline: none; 71 | } 72 | 73 | &:active { 74 | background-color: ${(props) => props.theme.secondaryBackground}; 75 | outline: none; 76 | } 77 | `; 78 | 79 | export const ContactAndBasicInfo = styled(SidebarButton)` 80 | color: ${(props) => props.theme.tertiaryText}; 81 | background-color: #fff; 82 | 83 | &:hover { 84 | background-color: ${(props) => props.theme.tertiaryBackground}; 85 | } 86 | 87 | &::after, 88 | &:focus { 89 | outline: none; 90 | } 91 | 92 | &:active { 93 | background-color: ${(props) => props.theme.secondaryBackground}; 94 | outline: none; 95 | } 96 | `; 97 | 98 | export const AboutBodyContainer = styled.div` 99 | padding: 16px; 100 | width: 67%; 101 | display: flex; 102 | flex-direction: column; 103 | justify-content: center; 104 | `; 105 | 106 | export const WorkplaceContainer = styled.div` 107 | display: flex; 108 | align-items: center; 109 | `; 110 | 111 | export const OverviewContainer = styled.div` 112 | margin-top: 16px; 113 | display: flex; 114 | align-items: center; 115 | `; 116 | 117 | export const OverviewText = styled.h1` 118 | color: ${(props) => props.theme.secondaryText}; 119 | font-size: 1.5rem; 120 | font-weight: 400; 121 | margin-left: 16px; 122 | `; 123 | 124 | export const AboutSkeleton = styled.div` 125 | display: flex; 126 | `; 127 | -------------------------------------------------------------------------------- /src/pages/NewsfeedPage/NewsfeedPage.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | width: 100%; 8 | /* justify-content: flex-end; */ 9 | padding-top: 8px; 10 | font-family: Roboto; 11 | `; 12 | 13 | export const PostContainer = styled.div` 14 | display: flex; 15 | padding: 0 32px; 16 | justify-content: center; 17 | 18 | @media only screen and (max-width: 992px) { 19 | flex-basis: 100%; 20 | padding: 0; 21 | width: 100%; 22 | } 23 | `; 24 | export const PostsSection = styled.div` 25 | width: 680px; 26 | `; 27 | 28 | export const ContactsSidebar = styled.div` 29 | display: none; 30 | @media only screen and (min-width: 1250px) { 31 | display: flex; 32 | margin-top: 50px; 33 | max-width: 320px; 34 | font-family: Roboto; 35 | } 36 | `; 37 | export const ContactsContainer = styled.div` 38 | position: fixed; 39 | width: 250px; 40 | top: 120px; 41 | right: 10px; 42 | `; 43 | 44 | export const ContactsHeader = styled.div` 45 | display: flex; 46 | margin-right: 18px; 47 | margin-bottom: 16px; 48 | `; 49 | 50 | export const ContactsHeading = styled.h1` 51 | font-size: 1.7rem; 52 | font-weight: medium; 53 | margin-right: auto; 54 | color: ${(props) => props.theme.tertiaryText}; 55 | `; 56 | 57 | export const ContactsBody = styled.div` 58 | display: flex; 59 | align-items: center; 60 | margin-right: 8px; 61 | padding: 8px; 62 | border-radius: 8px; 63 | cursor: pointer; 64 | 65 | &&:hover { 66 | background-color: ${(props) => props.theme.secondaryBackground}; 67 | } 68 | &&:active { 69 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 70 | } 71 | `; 72 | 73 | export const ContactAvatar = styled.img` 74 | width: 36px; 75 | height: 36px; 76 | border-radius: 100%; 77 | object-fit: cover; 78 | `; 79 | 80 | export const ContactFullName = styled.h1` 81 | font-size: 1.5rem; 82 | margin-left: 12px; 83 | color: ${(props) => props.theme.secondaryText}; 84 | font-weight: medium; 85 | `; 86 | 87 | export const NavbarSkeleton = styled.div` 88 | display: flex; 89 | 90 | svg { 91 | width: 100%; 92 | height: 58px; 93 | rect { 94 | width: 100%; 95 | height: 50px; 96 | } 97 | } 98 | `; 99 | 100 | export const PostSkeleton = styled.div` 101 | display: flex; 102 | margin-top: 20px; 103 | background: #fff; 104 | border-radius: 8px; 105 | @media only screen and (max-width: 575px) { 106 | border-radius: 0; 107 | } 108 | padding: 13px; 109 | svg { 110 | width: 100%; 111 | height: 100%; 112 | } 113 | `; 114 | 115 | export const ContactSkeleton = styled.div` 116 | padding: 8px; 117 | `; 118 | 119 | export const CreatePostSkeleton = styled.div` 120 | display: flex; 121 | background-color: #fff; 122 | padding: 13px; 123 | border-radius: 8px; 124 | @media only screen and (max-width: 575px) { 125 | border-radius: 0; 126 | } 127 | 128 | svg { 129 | width: 100%; 130 | height: 56px; 131 | rect { 132 | width: 100%; 133 | height: 56px; 134 | } 135 | } 136 | `; 137 | -------------------------------------------------------------------------------- /src/globalStyles/index.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import { normalize } from "styled-normalize"; 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const GlobalStyle = createGlobalStyle` 6 | ${normalize} 7 | 8 | h1, h2, h3, h4, h5, h6 { 9 | margin: 0; 10 | } 11 | html { 12 | overflow-y: scroll; 13 | } 14 | 15 | html, body { 16 | font-size: 62.5%; 17 | } 18 | body { 19 | background-color: #f0f2f5; 20 | } 21 | #root { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | } 29 | 30 | .activeProfileHeaderRoute { 31 | button { 32 | color: #1876f2; 33 | } 34 | &::after { 35 | width: 90%; 36 | margin: 0 auto; 37 | height: 3px; 38 | display: block; 39 | background: ${(props) => props.theme.primaryText}; 40 | content: ""; 41 | } 42 | } 43 | .deletePostPopup { 44 | &-content { 45 | width: 200px !important; 46 | display: flex; 47 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.1),inset 0 0 0 1px rgba(255, 255, 255, 0.5) !important; 48 | border-radius: 8px; 49 | transform: translateX(-45%); 50 | border: none !important; 51 | padding: 8px !important; 52 | } 53 | } 54 | 55 | .deleteCommentPopup { 56 | &-content { 57 | width: 200px !important; 58 | display: flex; 59 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.1),inset 0 0 0 1px rgba(255, 255, 255, 0.5) !important; 60 | border-radius: 8px; 61 | border: none !important; 62 | padding: 8px !important; 63 | } 64 | } 65 | 66 | .removeFriendPopup { 67 | &-content { 68 | width: 200px !important; 69 | display: flex; 70 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.1),inset 0 0 0 1px rgba(255, 255, 255, 0.5) !important; 71 | border-radius: 8px; 72 | right: 20px !important; 73 | left: auto !important; 74 | border: none !important; 75 | padding: 8px !important; 76 | } 77 | } 78 | 79 | .profileFriendPopup { 80 | &-content { 81 | width: 200px !important; 82 | display: flex; 83 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.1),inset 0 0 0 1px rgba(255, 255, 255, 0.5) !important; 84 | border-radius: 8px; 85 | border: none !important; 86 | padding: 8px !important; 87 | transform: translateX(-20%); 88 | } 89 | } 90 | 91 | .notificationPopup { 92 | &-content { 93 | width: 368px !important; 94 | display: flex; 95 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.1),inset 0 0 0 1px rgba(255, 255, 255, 0.5) !important; 96 | border-radius: 8px; 97 | top: 48px !important; 98 | right: 20px !important; 99 | left: auto !important; 100 | border: none !important; 101 | padding: 0 !important; 102 | 103 | @media only screen and (max-width: 400px) { 104 | width: 100% !important; 105 | right: 0px !important; 106 | border-radius: 0 !important; 107 | } 108 | } 109 | } 110 | 111 | .notificationToast { 112 | flex-direction: column; 113 | padding: 0; 114 | border-radius: 8px; 115 | box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2), 0 2px 12px rgba(0, 0, 0, 0.2); 116 | } 117 | `; 118 | -------------------------------------------------------------------------------- /src/routes/ProfileRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useQuery, useSubscription } from "@apollo/react-hooks"; 4 | import { Route, Redirect } from "react-router-dom"; 5 | import ProfileLayout from "./layouts/ProfileLayout"; 6 | import { notificationAlert } from "../utils/alerts"; 7 | import { 8 | GET_NOTIFICATIONS, 9 | NEW_NOTIFICATION, 10 | DELETE_NOTIFICATION, 11 | NEW_MESSAGE, 12 | LOAD_USER, 13 | GET_SINGLE_CHAT, 14 | GET_THREAD, 15 | } from "../utils/queries"; 16 | 17 | const ProfileRoute = ({ component: Component, ...rest }) => { 18 | const token = localStorage.getItem("token"); 19 | const { data: userData, loading } = useQuery(LOAD_USER); 20 | 21 | useQuery(GET_NOTIFICATIONS); 22 | 23 | const { refetch: threadRefetch } = useQuery(GET_THREAD); 24 | 25 | const { refetch: singleChatRefetch } = useQuery(GET_SINGLE_CHAT); 26 | 27 | useSubscription(NEW_MESSAGE, { 28 | variables: { 29 | notifierId: userData && userData.loadUser.id, 30 | }, 31 | onSubscriptionData: async ({ subscriptionData }) => { 32 | // refetch thread and single chat ( apollo cache gets updated automatically ) 33 | const { data: newThreadData } = await threadRefetch({ 34 | urlUser: subscriptionData.data.newMessage.creator.id, 35 | }); 36 | 37 | await singleChatRefetch({ 38 | threadId: newThreadData.getThread.id, 39 | }); 40 | }, 41 | }); 42 | useSubscription(NEW_NOTIFICATION, { 43 | onSubscriptionData: ({ client, subscriptionData }) => { 44 | const data = client.readQuery({ 45 | query: GET_NOTIFICATIONS, 46 | }); 47 | if ( 48 | subscriptionData.data.newNotification.notifier.id === 49 | userData.loadUser.id 50 | ) { 51 | notificationAlert(subscriptionData.data.newNotification); 52 | 53 | const newData = { 54 | getNotifications: [ 55 | subscriptionData.data.newNotification, 56 | ...data.getNotifications, 57 | ], 58 | }; 59 | 60 | client.writeQuery({ 61 | query: GET_NOTIFICATIONS, 62 | data: newData, 63 | }); 64 | } 65 | }, 66 | }); 67 | 68 | useSubscription(DELETE_NOTIFICATION, { 69 | onSubscriptionData: ({ client, subscriptionData }) => { 70 | const data = client.readQuery({ 71 | query: GET_NOTIFICATIONS, 72 | }); 73 | 74 | const newNotificationList = data.getNotifications.filter( 75 | (item) => item.id !== subscriptionData.data.deleteNotification 76 | ); 77 | 78 | client.writeQuery({ 79 | query: GET_NOTIFICATIONS, 80 | data: { getNotifications: newNotificationList }, 81 | }); 82 | }, 83 | }); 84 | 85 | if (!loading && !userData) { 86 | localStorage.removeItem("token"); 87 | return ; 88 | } 89 | 90 | return ( 91 | 94 | token ? ( 95 | 96 | 97 | 98 | ) : ( 99 | 100 | ) 101 | } 102 | /> 103 | ); 104 | }; 105 | 106 | export default ProfileRoute; 107 | 108 | ProfileRoute.propTypes = { 109 | component: PropTypes.func, 110 | }; 111 | 112 | ProfileRoute.defaultProps = { 113 | component: null, 114 | }; 115 | -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useHistory, Link } from "react-router-dom"; 2 | import React, { useState } from "react"; 3 | import { useForm } from "react-hook-form"; 4 | import { useMutation } from "@apollo/react-hooks"; 5 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 6 | import Loader from "react-loader-spinner"; 7 | import { LOGIN_USER } from "../../utils/queries"; 8 | 9 | import { 10 | LoginFormContainer, 11 | Label, 12 | Input, 13 | InputContainer, 14 | StyledButton, 15 | LoginHeading, 16 | ErrorMessageContainer, 17 | ErrorMessageHeading, 18 | RegisterContainer, 19 | RegisterLink, 20 | } from "./LoginForm.styles"; 21 | import { ReactComponent as ErrorIcon } from "../../assets/icons/alert-circle.svg"; 22 | 23 | const LoginForm = () => { 24 | const { register, handleSubmit, getValues, errors } = useForm(); 25 | const [graphQLError, setGraphQLError] = useState(undefined); 26 | 27 | const history = useHistory(); 28 | 29 | const [loginUser, { loading }] = useMutation(LOGIN_USER, { 30 | onCompleted: (result) => { 31 | const { token, username } = result.login; 32 | localStorage.setItem("token", token); 33 | history.push(`/${username}`); 34 | }, 35 | variables: { 36 | email: getValues("email"), 37 | password: getValues("password"), 38 | }, 39 | onError: (error) => setGraphQLError(error.graphQLErrors[0]), 40 | }); 41 | 42 | const onSubmit = () => { 43 | loginUser(); 44 | }; 45 | 46 | return ( 47 | 48 | Sign in to Fakebooker 49 |
50 | 51 | 52 | 58 | {errors && errors.email && ( 59 | 60 | 61 | {errors.email.message} 62 | 63 | )} 64 | 65 | 66 | 67 | 74 | {errors && errors.password && ( 75 | 76 | 77 | 78 | {errors.password.message} 79 | 80 | 81 | )} 82 | 83 | 84 | Sign in 85 | {loading && ( 86 | 97 | )} 98 | 99 | {graphQLError && ( 100 | 101 | 102 | {graphQLError.message} 103 | 104 | )} 105 |
106 | 107 | Not a member? 108 | 109 | Sign up now 110 | 111 | 112 |
113 | ); 114 | }; 115 | 116 | export default LoginForm; 117 | -------------------------------------------------------------------------------- /src/components/Message/SingleChat.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ChatContainer = styled.div` 4 | position: fixed; 5 | background-color: #fff; 6 | top: auto; 7 | right: 30px; 8 | bottom: 30px; 9 | box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1); 10 | border-radius: 8px; 11 | width: 330px; 12 | z-index: 30; 13 | height: 325px; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: space-between; 17 | @media only screen and (max-width: 400px) { 18 | right: 0; 19 | left: 0; 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | `; 24 | 25 | export const ChatHeader = styled.div` 26 | display: flex; 27 | align-items: center; 28 | font-family: "Roboto"; 29 | margin: 8px; 30 | `; 31 | 32 | export const CreatorAvatar = styled.img` 33 | width: 32px; 34 | height: 32px; 35 | border-radius: 50%; 36 | `; 37 | 38 | export const CreatorFullName = styled.h3` 39 | font-size: 1.5rem; 40 | font-weight: 600; 41 | margin-left: 8px; 42 | line-height: 1; 43 | `; 44 | 45 | export const ChatBodyContainer = styled.div` 46 | border-top: 2px solid rgba(0, 0, 0, 0.1); 47 | justify-self: flex-start; 48 | height: 100%; 49 | padding: 0 8px; 50 | font-family: "Roboto"; 51 | overflow-y: auto; 52 | `; 53 | export const ChatDataContainer = styled.div` 54 | max-height: 310px; 55 | display: flex; 56 | flex-direction: column; 57 | `; 58 | 59 | export const CreatorContainer = styled.div` 60 | display: inline-flex; 61 | align-items: flex-end; 62 | margin: 8px; 63 | margin-right: 20%; 64 | `; 65 | 66 | export const CreatorImg = styled.img` 67 | width: 28px; 68 | height: 28px; 69 | border-radius: 50%; 70 | `; 71 | 72 | export const Message = styled.div` 73 | font-size: 1.5rem; 74 | padding: 8px 12px; 75 | margin-left: 8px; 76 | font-family: "Roboto"; 77 | border-radius: 18px; 78 | `; 79 | export const CreatorMessage = styled(Message)` 80 | background-color: ${(props) => props.theme.secondaryBackground}; 81 | color: ${(props) => props.theme.secondaryText}; 82 | `; 83 | 84 | export const AuthUserContainer = styled.div` 85 | display: flex; 86 | justify-content: flex-end; 87 | margin: 8px; 88 | margin-left: 20%; 89 | `; 90 | 91 | export const AuthUserMessage = styled(Message)` 92 | background-color: #0084ff; 93 | color: #fff; 94 | `; 95 | 96 | export const InputContainer = styled.form` 97 | display: flex; 98 | font-family: "Roboto"; 99 | `; 100 | 101 | export const MessageInput = styled.input` 102 | background-color: ${(props) => props.theme.inputColor}; 103 | border: none; 104 | border-radius: 20px; 105 | width: 100%; 106 | height: 37px; 107 | padding-left: 8px; 108 | font-size: 1.5rem; 109 | box-sizing: border-box; 110 | 111 | ::placeholder { 112 | color: ${(props) => props.theme.placeholderColor}; 113 | font-size: 1.5rem; 114 | } 115 | &:focus { 116 | outline: none; 117 | border: 2px solid ${(props) => props.theme.primaryText}; 118 | } 119 | `; 120 | 121 | export const SubmitMessageBtn = styled.button` 122 | display: flex; 123 | justify-content: center; 124 | align-items: center; 125 | padding: 0; 126 | margin-left: 8px; 127 | border: none; 128 | background-color: #fff; 129 | 130 | svg { 131 | fill: #0084ff; 132 | } 133 | 134 | &:focus { 135 | outline: none; 136 | } 137 | &:disabled { 138 | svg { 139 | opacity: 0.5; 140 | } 141 | } 142 | `; 143 | 144 | export const CloseContainer = styled.div` 145 | display: flex; 146 | justify-content: center; 147 | align-items: center; 148 | margin-left: auto; 149 | border-radius: 100%; 150 | 151 | &:hover { 152 | background-color: ${(props) => props.theme.tertiaryBackground}; 153 | outline: none; 154 | } 155 | 156 | &::after, 157 | &:focus { 158 | outline: none; 159 | } 160 | 161 | &:active { 162 | background-color: ${(props) => props.theme.secondaryBackground}; 163 | outline: none; 164 | } 165 | `; 166 | 167 | export const ChatBodySkeleton = styled.div` 168 | svg { 169 | width: 100%; 170 | height: 100%; 171 | } 172 | `; 173 | 174 | export const ChatFooterContainer = styled.div` 175 | padding: 8px; 176 | `; 177 | -------------------------------------------------------------------------------- /src/pages/FriendsPage/FriendsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import ContentLoader from "react-content-loader"; 4 | import { useQuery } from "@apollo/react-hooks"; 5 | import { 6 | InfoContainer, 7 | FixedContainer, 8 | FriendsHeading, 9 | FriendsContainer, 10 | FriendSkeleton, 11 | } from "./FriendsPage.styles"; 12 | import { LOAD_USER, LOAD_FROM_URL_USER } from "../../utils/queries"; 13 | import Friend from "../../components/Friend/Friend"; 14 | 15 | const FriendsPage = () => { 16 | const { data: userData, loading: authLoading } = useQuery(LOAD_USER); 17 | 18 | const { username } = useParams(); 19 | 20 | /* eslint-disable consistent-return */ 21 | const readOnly = () => { 22 | if (userData) { 23 | if (userData.loadUser.username !== username) { 24 | return true; 25 | // eslint-disable-next-line no-else-return 26 | } else { 27 | return false; 28 | } 29 | } 30 | }; 31 | 32 | const { data: profileData, loading: profileLoading } = useQuery( 33 | LOAD_FROM_URL_USER, 34 | { 35 | variables: { 36 | username, 37 | }, 38 | skip: !readOnly(), 39 | } 40 | ); 41 | 42 | return ( 43 | 44 | 45 | Friends 46 | 47 | {!readOnly() && 48 | !authLoading && 49 | userData && 50 | userData.loadUser.friends.map((friend) => ( 51 | 52 | ))} 53 | {!readOnly() && authLoading && ( 54 | <> 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | )} 79 | {readOnly() && 80 | !profileLoading && 81 | profileData && 82 | profileData.loadFromUrlUser.friends.map((friend) => ( 83 | 84 | ))} 85 | {readOnly() && profileLoading && ( 86 | <> 87 | 88 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | )} 111 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | export default FriendsPage; 118 | -------------------------------------------------------------------------------- /src/components/Post/CreatePostActive.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CreatePostNewContainer = styled.form` 4 | box-shadow: ${(props) => props.theme.boxShadow1}; 5 | font-family: Roboto; 6 | border-radius: 8px; 7 | `; 8 | 9 | export const CreatePostHeader = styled.div` 10 | border-bottom: 1px solid ${(props) => props.theme.secondaryHoverBackground}; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | position: relative; 15 | height: 60px; 16 | `; 17 | 18 | export const CreatePostHeading = styled.h1` 19 | font-size: 2rem; 20 | font-weight: bold; 21 | margin-bottom: 0; 22 | color: ${(props) => props.theme.secondaryText}; 23 | `; 24 | 25 | export const CloseContainer = styled.div` 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | border-radius: 50%; 30 | padding: 0; 31 | background-color: ${(props) => props.theme.secondaryBackground}; 32 | cursor: pointer; 33 | transition: 0.1s; 34 | padding: 5px; 35 | 36 | &:focus { 37 | background-color: ${(props) => props.theme.secondaryBackground}; 38 | color: ${(props) => props.theme.secondaryText}; 39 | } 40 | &:active { 41 | transform: scale(0.96); 42 | } 43 | &:hover { 44 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 45 | color: ${(props) => props.theme.secondaryText}; 46 | } 47 | `; 48 | 49 | export const EndPositionContainer = styled.div` 50 | display: flex; 51 | justify-content: flex-end; 52 | `; 53 | 54 | export const CreatePostBody = styled.div``; 55 | 56 | export const User = styled.div` 57 | margin: 13px 16px; 58 | display: flex; 59 | align-items: center; 60 | margin-bottom: 0; 61 | `; 62 | 63 | export const UserAvatar = styled.img` 64 | width: 46px; 65 | height: 46px; 66 | border-radius: 100%; 67 | `; 68 | 69 | export const UserName = styled.h3` 70 | color: 1px solid ${(props) => props.theme.secondaryText}; 71 | font-size: 1.5rem; 72 | font-weight: bold; 73 | padding-left: 10px; 74 | `; 75 | 76 | export const CreatePostInputContainer = styled.div` 77 | display: flex; 78 | flex-direction: column; 79 | `; 80 | export const CreatePostInput = styled.textarea` 81 | display: flex; 82 | border: none; 83 | resize: none; 84 | margin: 16px; 85 | font-size: 1.5rem; 86 | color: ${(props) => props.theme.secondaryText}; 87 | 88 | &::placeholder { 89 | color: ${(props) => props.theme.placeholderColor}; 90 | font-size: 1.5rem; 91 | } 92 | &:focus { 93 | outline: none; 94 | } 95 | `; 96 | 97 | export const PublishBtnContainer = styled.div` 98 | width: 100%; 99 | display: flex; 100 | `; 101 | 102 | export const PublishBtn = styled.button` 103 | position: relative; 104 | width: 100%; 105 | border: none; 106 | border-radius: 6px; 107 | padding: 8px 12px; 108 | margin: 16px; 109 | font-weight: 600; 110 | font-size: 1.6rem; 111 | color: #fff; 112 | background-color: ${(props) => props.theme.primaryText}; 113 | transition: 0.1s; 114 | cursor: pointer; 115 | 116 | &:focus { 117 | color: #fff; 118 | background-color: ${(props) => props.theme.primaryText}; 119 | outline: none; 120 | } 121 | &:active { 122 | transform: scale(0.96); 123 | } 124 | &:disabled { 125 | opacity: 0.5; 126 | } 127 | `; 128 | 129 | export const AdditionalActionContainer = styled.div` 130 | display: flex; 131 | justify-content: center; 132 | align-items: center; 133 | width: 36px; 134 | height: 36px; 135 | border-radius: 100%; 136 | 137 | &:focus { 138 | background-color: ${(props) => props.theme.tertiaryBackground}; 139 | } 140 | 141 | &:hover { 142 | background-color: ${(props) => props.theme.tertiaryBackground}; 143 | } 144 | 145 | &:active { 146 | background-color: ${(props) => props.theme.secondaryBackground}; 147 | } 148 | `; 149 | export const PostImage = styled.div` 150 | background-image: url("${(props) => props.img}"); 151 | display: block; 152 | margin: 0 13px; 153 | background-size: cover; 154 | border-radius: 8px; 155 | min-height: 20rem; 156 | `; 157 | export const AdditionalActions = styled.div` 158 | display: flex; 159 | margin-left: auto; 160 | `; 161 | 162 | export const ImageSkeleton = styled.div` 163 | display: flex; 164 | height: 100%; 165 | padding: 0 13px; 166 | 167 | svg { 168 | width: 100%; 169 | height: 100%; 170 | rect { 171 | width: 100%; 172 | height: 100%; 173 | } 174 | } 175 | `; 176 | -------------------------------------------------------------------------------- /src/components/RegisterForm/RegisterForm.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RegisterFormContainer = styled.div` 4 | width: 500px; 5 | font-family: "Roboto"; 6 | 7 | @media only screen and (max-width: 767px) { 8 | padding: 0 20px; 9 | } 10 | `; 11 | 12 | export const StyledButton = styled.button` 13 | background-color: ${(props) => props.theme.primaryText}; 14 | position: relative; 15 | color: #fff; 16 | width: 50%; 17 | border: none; 18 | height: auto; 19 | padding: 13px 50px; 20 | font-size: 1.5rem; 21 | transition: 0.1s; 22 | cursor: pointer; 23 | border-radius: 4px; 24 | 25 | &:focus { 26 | opacity: 0.9; 27 | outline: none; 28 | } 29 | 30 | &:hover { 31 | background-color: ${(props) => props.theme.primaryText}; 32 | color: #fff; 33 | outline: none; 34 | } 35 | 36 | &&:active { 37 | color: #fff; 38 | transform: scale(0.96); 39 | } 40 | 41 | @media only screen and (max-width: 480px) { 42 | width: 100%; 43 | } 44 | `; 45 | 46 | export const RegisterHeading = styled.p` 47 | font-size: 2.4rem; 48 | font-weight: 600; 49 | margin-bottom: 30px; 50 | color: ${(props) => props.theme.secondaryText}; 51 | `; 52 | 53 | export const Label = styled.p` 54 | display: inline-block; 55 | margin: 0; 56 | margin-bottom: 7px; 57 | font-size: 1.5rem; 58 | font-weight: 600; 59 | line-height: 1; 60 | color: ${(props) => props.theme.secondaryText}; 61 | `; 62 | 63 | export const Input = styled.input` 64 | background-color: ${(props) => props.theme.inputColor}; 65 | border: none; 66 | border-radius: 6px; 67 | color: ${(props) => props.theme.secondaryText}; 68 | height: 40px; 69 | padding: 0 8px; 70 | font-size: 1.5rem; 71 | box-sizing: border-box; 72 | 73 | &:focus { 74 | outline: none; 75 | border: 2px solid ${(props) => props.theme.primaryText}; 76 | } 77 | `; 78 | 79 | export const BirthdayInput = styled(Input)` 80 | width: 100%; 81 | padding: 8px; 82 | margin-right: 16px; 83 | `; 84 | 85 | export const FullNameContainer = styled.div` 86 | display: flex; 87 | width: 100%; 88 | margin-bottom: 20px; 89 | @media only screen and (max-width: 767px) { 90 | flex-direction: column; 91 | } 92 | `; 93 | 94 | export const NameContainer = styled.div` 95 | display: flex; 96 | flex-direction: column; 97 | width: 50%; 98 | margin-right: 16px; 99 | @media only screen and (max-width: 767px) { 100 | width: 100%; 101 | } 102 | `; 103 | 104 | export const LastNameContainer = styled(NameContainer)` 105 | margin-right: 0; 106 | @media only screen and (max-width: 767px) { 107 | margin-top: 20px; 108 | } 109 | `; 110 | 111 | export const EmailContainer = styled.div` 112 | margin-bottom: 20px; 113 | display: flex; 114 | flex-direction: column; 115 | `; 116 | 117 | export const PasswordContainer = styled.div` 118 | margin-bottom: 20px; 119 | display: flex; 120 | flex-direction: column; 121 | `; 122 | 123 | export const BirthdayContainer = styled.div` 124 | margin-bottom: 20px; 125 | width: 45%; 126 | @media only screen and (max-width: 767px) { 127 | width: 100%; 128 | } 129 | `; 130 | 131 | export const GenderContainer = styled.div` 132 | margin-top: 7px; 133 | margin-bottom: 30px; 134 | display: flex; 135 | flex-direction: column; 136 | `; 137 | 138 | export const Gender = styled.input` 139 | margin-right: 8px; 140 | cursor: pointer; 141 | `; 142 | 143 | export const GenderLabel = styled.label` 144 | line-height: 1; 145 | margin: 0; 146 | font-size: 1.5rem; 147 | color: ${(props) => props.theme.secondaryText}; 148 | `; 149 | 150 | export const GenderInputContainer = styled.div` 151 | display: flex; 152 | justify-content: center; 153 | align-items: center; 154 | `; 155 | 156 | export const FemaleContainer = styled(GenderInputContainer)` 157 | margin-right: 16px; 158 | `; 159 | 160 | export const ErrorMessageContainer = styled.div` 161 | display: flex; 162 | align-items: center; 163 | margin-top: 4px; 164 | `; 165 | 166 | export const ErrorMessageHeading = styled.h1` 167 | color: ${(props) => props.theme.errorText}; 168 | font-size: 1.2rem; 169 | font-weight: 500; 170 | margin-left: 7px; 171 | `; 172 | 173 | export const LoginContainer = styled.div` 174 | margin-top: 20px; 175 | color: ${(props) => props.theme.secondaryText}; 176 | font-weight: 400; 177 | font-size: 1.5rem; 178 | align-items: center; 179 | display: flex; 180 | 181 | @media only screen and (max-width: 480px) { 182 | justify-content: center; 183 | } 184 | `; 185 | export const LoginLink = styled.h1` 186 | font-weight: 400; 187 | font-size: 1.5rem; 188 | margin-left: 4px; 189 | color: ${(props) => props.theme.primaryText}; 190 | `; 191 | -------------------------------------------------------------------------------- /src/components/Post/CreatePostActive.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | import PropTypes from "prop-types"; 4 | import { useMutation } from "@apollo/react-hooks"; 5 | import { useForm } from "react-hook-form"; 6 | import Image from "../Image/Image"; 7 | import { 8 | CreatePostNewContainer, 9 | ImageSkeleton, 10 | CreatePostHeader, 11 | CreatePostHeading, 12 | CloseContainer, 13 | CreatePostBody, 14 | User, 15 | UserAvatar, 16 | UserName, 17 | CreatePostInput, 18 | CreatePostInputContainer, 19 | PublishBtn, 20 | PublishBtnContainer, 21 | AdditionalActionContainer, 22 | PostImage, 23 | EndPositionContainer, 24 | AdditionalActions, 25 | } from "./CreatePostActive.styles"; 26 | import { ReactComponent as CloseBtn } from "../../assets/icons/close.svg"; 27 | import { ReactComponent as MarkdownIcon } from "../../assets/icons/logo-markdown.svg"; 28 | import { CREATE_POST, GET_POSTS, DELETE_IMAGE } from "../../utils/queries"; 29 | 30 | const CreatePostActive = ({ user, closeModal, onNewsfeed }) => { 31 | const { register, watch, handleSubmit } = useForm(); 32 | const [image, setImage] = useState(undefined); 33 | const [imageLoading, setImageLoading] = useState(undefined); 34 | 35 | const [deleteImage] = useMutation(DELETE_IMAGE, { 36 | variables: { 37 | publicId: image && image.public_id, 38 | }, 39 | }); 40 | 41 | const [createPost] = useMutation(CREATE_POST, { 42 | variables: { 43 | body: watch("body"), 44 | image: image && image.secure_url, 45 | }, 46 | update: async (proxy, result) => { 47 | if (!onNewsfeed) { 48 | const data = proxy.readQuery({ 49 | query: GET_POSTS, 50 | }); 51 | const newData = { 52 | getPosts: [result.data.createPost, ...data.getPosts], 53 | }; 54 | 55 | proxy.writeQuery({ 56 | query: GET_POSTS, 57 | data: newData, 58 | }); 59 | } 60 | }, 61 | }); 62 | 63 | const onSubmit = () => { 64 | createPost(); 65 | setImage(undefined); 66 | closeModal(); 67 | }; 68 | 69 | return ( 70 | 71 | 72 | Create Post 73 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {user.firstName} {user.lastName} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 102 | {imageLoading && imageLoading !== 1 && ( 103 | 104 | 109 | 110 | 111 | 112 | )} 113 | {image && ( 114 | 115 | 116 | { 119 | deleteImage(); 120 | setImage(undefined); 121 | }} 122 | style={{ marginTop: "13px", marginRight: "16px" }} 123 | > 124 | 125 | 126 | 127 | 128 | )} 129 | 130 | 131 | 132 | 133 | Post 134 | 135 | 136 | 137 | ); 138 | }; 139 | 140 | export default CreatePostActive; 141 | 142 | CreatePostActive.propTypes = { 143 | closeModal: PropTypes.func, 144 | user: PropTypes.shape({ 145 | firstName: PropTypes.string, 146 | lastName: PropTypes.string, 147 | avatarImage: PropTypes.string, 148 | }), 149 | onNewsfeed: PropTypes.bool, 150 | }; 151 | 152 | CreatePostActive.defaultProps = { 153 | user: null, 154 | closeModal: () => null, 155 | onNewsfeed: null, 156 | }; 157 | -------------------------------------------------------------------------------- /src/components/Comment/CreateComment.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { useForm } from "react-hook-form"; 5 | import { useMutation } from "@apollo/react-hooks"; 6 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 7 | import Loader from "react-loader-spinner"; 8 | import { CommentInput, CommentForm, UserAvatar } from "./CreateComment.styles"; 9 | import { 10 | CREATE_COMMENT, 11 | GET_POSTS, 12 | GET_URL_POSTS, 13 | GET_NEWSFEED, 14 | GET_SINGLE_POST, 15 | } from "../../utils/queries"; 16 | 17 | const CreateComment = ({ 18 | user, 19 | postId, 20 | urlProfile, 21 | onNewsfeed, 22 | onSinglePost, 23 | }) => { 24 | const { username } = useParams(); 25 | const { register, getValues, setValue, handleSubmit } = useForm(); 26 | 27 | // you need to be able to comment on this guy's post ( implement getUrlposts and getPosts ) 28 | const [createComment, { loading }] = useMutation(CREATE_COMMENT, { 29 | variables: { 30 | body: getValues("body"), 31 | postId, 32 | }, 33 | update: (proxy, result) => { 34 | if (!urlProfile && !onNewsfeed && !onSinglePost) { 35 | const data = proxy.readQuery({ 36 | query: GET_POSTS, 37 | }); 38 | 39 | const getPosts = data.getPosts.map((post) => { 40 | if (post.id === postId) { 41 | return { 42 | ...post, 43 | comments: [...post.comments, result.data.createComment], 44 | }; 45 | } 46 | return post; 47 | }); 48 | 49 | proxy.writeQuery({ 50 | query: GET_POSTS, 51 | data: { getPosts }, 52 | }); 53 | } 54 | if (urlProfile && !onNewsfeed && !onSinglePost) { 55 | const data = proxy.readQuery({ 56 | query: GET_URL_POSTS, 57 | variables: { 58 | username, 59 | }, 60 | }); 61 | 62 | const getUrlPosts = data.getUrlPosts.map((post) => { 63 | if (post.id === postId) { 64 | return { 65 | ...post, 66 | comments: [...post.comments, result.data.createComment], 67 | }; 68 | } 69 | return post; 70 | }); 71 | proxy.writeQuery({ 72 | query: GET_URL_POSTS, 73 | data: { getUrlPosts }, 74 | variables: { 75 | username, 76 | }, 77 | }); 78 | } 79 | if (!onSinglePost && !urlProfile && onNewsfeed) { 80 | const data = proxy.readQuery({ 81 | query: GET_NEWSFEED, 82 | }); 83 | 84 | const getNewsfeed = data.getNewsfeed.map((post) => { 85 | if (post.id === postId) { 86 | return { 87 | ...post, 88 | comments: [...post.comments, result.data.createComment], 89 | }; 90 | } 91 | return post; 92 | }); 93 | 94 | proxy.writeQuery({ 95 | query: GET_NEWSFEED, 96 | data: { getNewsfeed }, 97 | }); 98 | } 99 | if (onSinglePost && !urlProfile && !onNewsfeed) { 100 | const data = proxy.readQuery({ 101 | query: GET_SINGLE_POST, 102 | variables: { 103 | postId, 104 | }, 105 | }); 106 | 107 | const getSinglePost = { 108 | ...data.getSinglePost, 109 | comments: [...data.getSinglePost.comments, result.data.createComment], 110 | }; 111 | 112 | proxy.writeQuery({ 113 | query: GET_SINGLE_POST, 114 | data: { getSinglePost }, 115 | }); 116 | } 117 | }, 118 | }); 119 | 120 | const onSubmit = async () => { 121 | await createComment(); 122 | setValue("body", ""); 123 | }; 124 | 125 | return ( 126 | <> 127 | 128 | 129 | 137 | {loading && ( 138 | 149 | )} 150 | 151 | 152 | ); 153 | }; 154 | 155 | export default CreateComment; 156 | 157 | CreateComment.propTypes = { 158 | user: PropTypes.shape({ 159 | id: PropTypes.string, 160 | avatarImage: PropTypes.string, 161 | firstName: PropTypes.string, 162 | lastName: PropTypes.string, 163 | username: PropTypes.string, 164 | email: PropTypes.string, 165 | birthday: PropTypes.string, 166 | gender: PropTypes.string, 167 | coverImage: PropTypes.string, 168 | }), 169 | postId: PropTypes.string, 170 | urlProfile: PropTypes.bool, 171 | onNewsfeed: PropTypes.bool, 172 | onSinglePost: PropTypes.bool, 173 | }; 174 | 175 | CreateComment.defaultProps = { 176 | user: null, 177 | postId: null, 178 | urlProfile: null, 179 | onNewsfeed: null, 180 | onSinglePost: null, 181 | }; 182 | -------------------------------------------------------------------------------- /src/pages/ProfilePage/ProfilePage.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoContainer = styled.div` 4 | display: flex; 5 | height: 100%; 6 | padding: 28px 0; 7 | justify-content: center; 8 | `; 9 | 10 | export const PostsSection = styled.div` 11 | width: 500px; 12 | @media only screen and (max-width: 575px) { 13 | width: 100%; 14 | } 15 | `; 16 | export const CreatePostMobile = styled.div` 17 | display: none; 18 | @media only screen and (max-width: 1000px) { 19 | display: block; 20 | width: 500px; 21 | margin-bottom: 16px; 22 | } 23 | 24 | @media only screen and (max-width: 575px) { 25 | display: block; 26 | width: 100%; 27 | } 28 | `; 29 | 30 | export const CreatePostDesktop = styled.div` 31 | display: none; 32 | @media only screen and (min-width: 1000px) { 33 | display: block; 34 | width: 100%; 35 | margin-bottom: 16px; 36 | } 37 | `; 38 | 39 | export const FixedContainer = styled.div` 40 | height: 100%; 41 | display: flex; 42 | @media only screen and (max-width: 1000px) { 43 | width: 100%; 44 | /* justify-content: center; */ 45 | flex-direction: column; 46 | align-items: center; 47 | } 48 | `; 49 | 50 | export const CreatePostSkeleton = styled.div` 51 | display: flex; 52 | background-color: #fff; 53 | padding: 13px; 54 | border-radius: 8px; 55 | @media only screen and (max-width: 575px) { 56 | border-radius: 0; 57 | } 58 | 59 | svg { 60 | width: 100%; 61 | height: 56px; 62 | rect { 63 | width: 100%; 64 | height: 56px; 65 | } 66 | } 67 | `; 68 | 69 | export const PostSectionSkeleton = styled.div` 70 | display: flex; 71 | margin-top: 20px; 72 | 73 | svg { 74 | width: 100%; 75 | height: 56px; 76 | rect { 77 | width: 100%; 78 | height: 56px; 79 | } 80 | } 81 | `; 82 | 83 | export const AboutSkeleton = styled.div` 84 | background-color: #fff; 85 | display: flex; 86 | height: 100%; 87 | box-sizing: border-box; 88 | padding: 13px; 89 | border-radius: 8px; 90 | 91 | @media only screen and (min-width: 1000px) { 92 | margin-right: 16px; 93 | width: 370px; 94 | } 95 | 96 | @media only screen and (max-width: 1000px) { 97 | width: 100%; 98 | } 99 | 100 | svg { 101 | width: 100%; 102 | height: 100%; 103 | 104 | rect { 105 | width: 100%; 106 | } 107 | } 108 | `; 109 | 110 | export const PostSkeleton = styled.div` 111 | display: flex; 112 | margin-top: 20px; 113 | background: #fff; 114 | border-radius: 8px; 115 | @media only screen and (max-width: 575px) { 116 | border-radius: 0; 117 | } 118 | padding: 13px; 119 | `; 120 | 121 | export const Photos = styled.div` 122 | margin-top: 16px; 123 | font-family: Roboto; 124 | background-color: #fff; 125 | box-shadow: ${(props) => props.theme.boxShadow2}; 126 | border-radius: 6px; 127 | padding: 12px; 128 | 129 | @media only screen and (max-width: 575px) { 130 | margin-right: 0px; 131 | border-radius: 0; 132 | } 133 | 134 | @media only screen and (min-width: 1000px) { 135 | margin-right: 16px; 136 | } 137 | `; 138 | 139 | export const PhotosGrid = styled.div` 140 | display: grid; 141 | max-width: 340px; 142 | grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); 143 | grid-gap: 5px; 144 | @media only screen and (max-width: 1000px) { 145 | max-width: 500px; 146 | } 147 | @media only screen and (max-width: 575px) { 148 | max-width: 100%; 149 | } 150 | `; 151 | 152 | export const Photo = styled.img` 153 | width: 100%; 154 | height: 101px; 155 | border-radius: 8px; 156 | object-fit: cover; 157 | `; 158 | 159 | export const AboutInfoContainer = styled.div` 160 | display: flex; 161 | flex-direction: column; 162 | height: 100%; 163 | @media only screen and (max-width: 1000px) { 164 | width: 500px; 165 | } 166 | 167 | @media only screen and (max-width: 575px) { 168 | width: 100%; 169 | } 170 | `; 171 | 172 | export const PhotosHeading = styled.h1` 173 | font-size: 2rem; 174 | font-weight: bold; 175 | color: ${(props) => props.theme.secondaryText}; 176 | margin-bottom: 12px; 177 | `; 178 | 179 | export const Friends = styled.div` 180 | font-family: Roboto; 181 | margin-top: 16px; 182 | margin-bottom: 16px; 183 | background-color: #fff; 184 | box-shadow: ${(props) => props.theme.boxShadow2}; 185 | border-radius: 6px; 186 | padding: 12px; 187 | 188 | @media only screen and (max-width: 575px) { 189 | border-radius: 0; 190 | margin-right: 0; 191 | } 192 | 193 | @media only screen and (min-width: 1000px) { 194 | margin-right: 16px; 195 | } 196 | `; 197 | 198 | export const FriendsGrid = styled.div` 199 | display: grid; 200 | max-width: 340px; 201 | grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); 202 | grid-gap: 15px; 203 | @media only screen and (max-width: 1000px) { 204 | max-width: 500px; 205 | } 206 | @media only screen and (max-width: 575px) { 207 | max-width: 100%; 208 | } 209 | `; 210 | export const FriendsHeading = styled.h1` 211 | font-size: 2rem; 212 | font-weight: bold; 213 | color: ${(props) => props.theme.secondaryText}; 214 | margin-bottom: 12px; 215 | `; 216 | 217 | export const FriendContainer = styled.div` 218 | display: flex; 219 | flex-direction: column; 220 | `; 221 | 222 | export const FriendAvatar = styled.img` 223 | width: 100%; 224 | height: 101px; 225 | object-fit: cover; 226 | border-radius: 8px; 227 | `; 228 | 229 | export const FriendFullname = styled.h1` 230 | font-size: 1.5rem; 231 | margin-top: 8px; 232 | font-weight: 500; 233 | color: ${(props) => props.theme.secondaryText}; 234 | `; 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fakebooker 1.0.0 ![Fakebooker-Frontend](https://github.com/KristianWEB/fakebooker-frontend/workflows/Fakebooker-Frontend/badge.svg) ![Fakebooker-Backend](https://github.com/KristianWEB/fakebooker-backend/workflows/Fakebooker-Backend/badge.svg) 2 | 3 | Fakebooker is an extensive open-source project that is essentially a clone of the real Facebook. Now Fakebooker is not one of those little pet projects that just prove the concept, the Fakebooker UI is almost identical to the real Facebook Beta which makes it even more unique. The idea of this project is to try to implement Facebook's design patterns using the MERN stack. It is made for educational purposes only and nothing else! 4 | 5 | ## Screenshots 6 | 7 | **Newsfeed:** 8 | ![newsfeed](https://github.com/KristianWEB/fakebooker-frontend/blob/master/src/assets/screenshots/newsfeed.PNG) 9 | 10 | **ProfilePage:** 11 | ![profilepage](https://github.com/KristianWEB/fakebooker-frontend/blob/master/src/assets/screenshots/profilepage.PNG) 12 | 13 | ## Technologies 14 | 15 | ### Frontend 16 | 17 | - Javascript library for building User Interfaces: `React` 18 | - Client for handling GraphQL queries + caching + local state management: `ApolloClient` 19 | - CSS-in-JS library for handling Fakebooker's styles: `styled-components` 20 | - Performant, flexible and extensible forms with easy-to-use validation: `react-hook-form` 21 | - Notifications are built on top of: `react-toastify` 22 | - Popups are made using: `reactjs-popup` 23 | - Handling time properly: `momentjs` 24 | - Skeletons for loading components: `react-content-loader` 25 | - Loading spinners for smaller actions: `react-loader-spinner` 26 | 27 | ### Backend [link](https://github.com/KristianWEB/fakebooker-backend) 28 | 29 | - The NoSQL database for modern applications: `mongodb` 30 | - Elegant MongoDB object modeling for node.js: `mongoose` 31 | - GraphQLServer: `apollo-server` 32 | - Authentication built on top of tokens: `jwt` 33 | - Image Upload: `cloudinary` 34 | - Easily faking data: `faker` 35 | - Testing framework: `jest` 36 | - Integration testing package: `apollo-server-testing` 37 | 38 | **Most of the backend code is test covered except some resolvers and the subscription ones** 39 | 40 | ## Features ( 1.0.0 ) 41 | 42 | **I've tried my best to clone the real Facebook Beta even with the same colors provided in theme.js** 43 | 44 | ### Login / Register 45 | 46 | - You can `login` or `register` easily, and if something goes wrong there are cool validations to help you get on ( every input is validated on the app ) 47 | 48 | ### Post 49 | 50 | - You can create a post in which you can also use `markdown` and upload an image if you want to just like Facebook 51 | - You can like or comment on a post and if the post is not yours the person who created it will receive an instant realtime notification about your particular action ( `apollo subscriptions` ) 52 | - You can delete a post ( `if it's yours all comments and likes associated to it will get deleted` ) 53 | - You can unlike or delete your comment ( `the notifications related to your like or comment will get deleted` ) 54 | - You can delete anyone's comment as long as you are the post creator 55 | - On every post or comment if you hover on the post's / comment's creator you can go to his profile page 56 | 57 | ### ProfileHeader 58 | 59 | - You can make friends: send friend requests to other users and they can respond to your request ( `a friend notification is fired through subscriptions once again` ) 60 | - You can open up a chat with that particular user 61 | - You can change your avatarImage ( `cloudinary` ) 62 | - You can change your coverImage ( `cloudinary` ) 63 | - You can view your Timeline, About, Friends and Photos pages ( `refer to ProfilePage` ) 64 | 65 | ### Navbar 66 | 67 | - You can search for users with filter on. 68 | - You can chat with people ( `again through subscriptions` ) and the ProfilePage corresponds with that user's data 69 | - You can see all your conversations ( `chat button on Navbar` ) 70 | - You can see all your notifications ( `notification button on Navbar` ) 71 | - When you click on a notification it redirects you to the SinglePostPage if it's a post type or user if it's a friend request type 72 | 73 | ### Loading 74 | 75 | - Loading states and skeletons for every single component/page on the app 76 | 77 | ### SinglePostPage 78 | 79 | - Navbar 80 | - Consists of the single post that the notification is pointing to 81 | 82 | ### Newsfeed 83 | 84 | - The newsfeed contains all posts from the app, which again you can interact with them 85 | - Contacts: Your friends ( `if you click on a friend, a chat tab is going to be opened for a quick chat with him` ) 86 | - Navbar 87 | 88 | ### ProfilePage 89 | 90 | - Navbar 91 | - ProfileHeader 92 | 93 | #### You've read most of the features, go to [fakebooker.com](https://fakebooker.com) and then check in real time the features that I'm explaining 94 | 95 | #### Timeline Page 96 | 97 | - Consists of all your information: About, Photos, Friends and your posts 98 | 99 | #### About Page 100 | 101 | - Consists of all your personal information such as: 102 | - Workplace 103 | - School 104 | - Homeplace 105 | - Birthday 106 | - Gender 107 | 108 | #### Friends Page 109 | 110 | - Consists of all your friends ( `each friend has a button to remove him from your friends' list` ) 111 | 112 | #### Photos Page 113 | 114 | - Consists of all your posts that contain images 115 | 116 | ## Setup a local environment 117 | 118 | - Copy the `.env.example` file into `.env.local` and fill in the actual values ( `you need to have a cloudinary account` ) 119 | 120 | ```sh 121 | npm install && npm start 122 | ``` 123 | 124 | ## Contribute 125 | 126 | Contribution is accepted and I encourage you to do so as long as you follow my Github worklfow 127 | 128 | ## Issues / Problems 129 | 130 | Check out `Projects` tab in both frontend and backend repos and if you don't find that issue that you are having, better create one and I will make sure everything is alright! 131 | -------------------------------------------------------------------------------- /src/components/Post/Post.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PostContainer = styled.div` 4 | margin: 1.6rem 0; 5 | &&:first-child { 6 | margin-top: 0; 7 | } 8 | max-width: 680px; 9 | box-shadow: ${(props) => props.theme.boxShadow2}; 10 | border-radius: 8px; 11 | font-family: "Roboto"; 12 | @media only screen and (max-width: 575px) { 13 | border-radius: 0; 14 | } 15 | background-color: #fff; 16 | `; 17 | 18 | export const PopContainer = styled.div` 19 | display: flex; 20 | border-radius: 8px; 21 | width: 100%; 22 | z-index: 20; 23 | flex-direction: column; 24 | 25 | @media only screen and (max-width: 575px) { 26 | width: 200px; 27 | } 28 | `; 29 | 30 | export const PopButton = styled.button` 31 | text-align: left; 32 | font-size: 1.5rem; 33 | border: none; 34 | padding: 8px; 35 | border-radius: 6px; 36 | display: flex; 37 | width: 100%; 38 | background-color: #fff; 39 | color: ${(props) => props.theme.secondaryText}; 40 | transition: 0.1s; 41 | font-weight: 500; 42 | cursor: pointer; 43 | 44 | &:hover { 45 | background-color: ${(props) => props.theme.tertiaryBackground}; 46 | color: ${(props) => props.theme.secondaryText}; 47 | } 48 | 49 | &::after, 50 | &:focus { 51 | outline: none; 52 | color: ${(props) => props.theme.secondaryText}; 53 | } 54 | 55 | &:active { 56 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 57 | color: ${(props) => props.theme.secondaryText}; 58 | } 59 | `; 60 | 61 | export const CommentsContainer = styled.div` 62 | padding: 8px 16px; 63 | border-radius: 0 0 8px 8px; 64 | `; 65 | 66 | export const PostHeader = styled.div` 67 | display: flex; 68 | margin-bottom: 0; 69 | padding: 12px 16px; 70 | `; 71 | export const SettingsContainer = styled.div` 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | height: 100%; 76 | border-radius: 100%; 77 | padding: 5px; 78 | cursor: pointer; 79 | position: relative; 80 | 81 | &:focus { 82 | background-color: ${(props) => props.theme.tertiaryBackground}; 83 | } 84 | 85 | &:hover { 86 | background-color: ${(props) => props.theme.tertiaryBackground}; 87 | } 88 | &:active { 89 | background-color: ${(props) => props.theme.secondaryBackground}; 90 | } 91 | `; 92 | 93 | export const PostCard = styled.div``; 94 | 95 | export const ProfileWrapper = styled.div` 96 | width: 100%; 97 | display: flex; 98 | align-items: center; 99 | `; 100 | 101 | export const NameWrapper = styled.div` 102 | display: flex; 103 | flex-direction: column; 104 | margin-left: 0.7rem; 105 | `; 106 | export const ProfileName = styled.h3` 107 | font-size: 1.5rem; 108 | color: ${(props) => props.theme.secondaryText}; 109 | font-weight: 500; 110 | font-family: "Roboto"; 111 | 112 | &:hover { 113 | text-decoration: underline; 114 | } 115 | `; 116 | 117 | export const PostCreation = styled.p` 118 | font-size: 1.3rem; 119 | margin: 4px 0 0 0; 120 | color: ${(props) => props.theme.tertiaryText}; 121 | `; 122 | 123 | export const PostContent = styled.div` 124 | color: ${(props) => props.theme.secondaryText}; 125 | font-size: 1.5rem; 126 | p { 127 | margin: 0; 128 | padding: 0px 16px 16px 16px; 129 | } 130 | `; 131 | 132 | export const PostFooter = styled.div` 133 | display: flex; 134 | border-top: 1px solid ${(props) => props.theme.secondaryHoverBackground}; 135 | border-bottom: 1px solid ${(props) => props.theme.secondaryHoverBackground}; 136 | margin: 0 16px; 137 | @media only screen and (max-width: 575px) { 138 | margin: 0; 139 | } 140 | `; 141 | export const FooterButton = styled.button` 142 | display: flex; 143 | cursor: pointer; 144 | align-items: center; 145 | border-radius: 6px; 146 | margin: 3px 0; 147 | padding: 5px; 148 | justify-content: center; 149 | flex: 3; 150 | border: none; 151 | background-color: transparent; 152 | 153 | &:hover { 154 | background-color: ${(props) => props.theme.tertiaryBackground}; 155 | outline: none; 156 | } 157 | 158 | &::after, 159 | &:focus { 160 | outline: none; 161 | } 162 | &:active { 163 | background-color: ${(props) => props.theme.secondaryBackground}; 164 | } 165 | `; 166 | 167 | export const Count = styled.p` 168 | font-size: 1.5rem; 169 | margin: 0; 170 | margin-left: 5px; 171 | line-height: 1; 172 | `; 173 | 174 | export const LikesCount = styled(Count)` 175 | margin-top: 5px; 176 | color: ${(props) => props.theme.primaryText}; 177 | `; 178 | 179 | export const CommentsCount = styled(Count)` 180 | color: ${(props) => props.theme.secondaryTextColor}; 181 | `; 182 | 183 | // export const SharesWrapper = styled.div` 184 | // display: flex; 185 | // align-items: center; 186 | // cursor: pointer; 187 | // justify-content: center; 188 | // flex: 3; 189 | // margin: 3px 0; 190 | // margin-left: 0.5rem; 191 | // border-radius: 6px; 192 | // padding: 5px; 193 | 194 | // &:hover { 195 | // background-color: ${(props) => props.theme.tertiaryBackground}; 196 | // outline: none; 197 | // } 198 | 199 | // &::after, 200 | // &:focus { 201 | // outline: none; 202 | // } 203 | // &:active { 204 | // background-color: ${(props) => props.theme.secondaryBackground}; 205 | // } 206 | // `; 207 | // export const SharesCount = styled.p` 208 | // font-size: 1.5rem; 209 | // margin: 0; 210 | // margin-left: 6px; 211 | // color: ${(props) => props.theme.secondaryText}; 212 | // `; 213 | 214 | export const FooterHeading = styled.span` 215 | font-size: 1.5rem; 216 | margin-left: 5px; 217 | color: ${(props) => props.theme.tertiaryText}; 218 | line-height: 1; 219 | margin-top: 5px; 220 | `; 221 | 222 | export const PostImage = styled.img` 223 | width: 100%; 224 | height: 100%; 225 | `; 226 | 227 | export const ProfileAvatar = styled.img` 228 | width: 40px; 229 | height: 40px; 230 | border-radius: 100%; 231 | `; 232 | 233 | export const PostSkeleton = styled.div` 234 | display: flex; 235 | margin-top: 20px; 236 | background: #fff; 237 | border-radius: 8px; 238 | padding: 13px; 239 | `; 240 | -------------------------------------------------------------------------------- /src/components/Comment/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Popup from "reactjs-popup"; 3 | import { useParams, Link } from "react-router-dom"; 4 | import PropTypes from "prop-types"; 5 | import { useMutation, useQuery } from "@apollo/react-hooks"; 6 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 7 | import Loader from "react-loader-spinner"; 8 | import { 9 | CommentContainer, 10 | BodyContainer, 11 | Body, 12 | Username, 13 | PopButton, 14 | ActionsContainer, 15 | CommentAvatar, 16 | } from "./Comment.styles"; 17 | import { 18 | DELETE_COMMENT, 19 | GET_POSTS, 20 | GET_URL_POSTS, 21 | GET_SINGLE_POST, 22 | GET_NEWSFEED, 23 | LOAD_USER, 24 | } from "../../utils/queries"; 25 | import { ReactComponent as ThreeDotsSvg } from "../../assets/icons/ellipsis-horizontal.svg"; 26 | 27 | const Comment = ({ 28 | comment: { userId, body, id }, 29 | post: { 30 | id: postId, 31 | userId: { id: postCreatorId }, 32 | }, 33 | urlProfile, 34 | onNewsfeed, 35 | onSinglePost, 36 | }) => { 37 | const { username } = useParams(); 38 | 39 | const { data: userData } = useQuery(LOAD_USER); 40 | 41 | const [deleteComment, { loading }] = useMutation(DELETE_COMMENT, { 42 | variables: { 43 | commentId: id, 44 | postId, 45 | }, 46 | update: (proxy) => { 47 | if (!urlProfile && !onNewsfeed && !onSinglePost) { 48 | const data = proxy.readQuery({ 49 | query: GET_POSTS, 50 | }); 51 | 52 | const getPosts = data.getPosts.map((post) => { 53 | if (post.id === postId) { 54 | return { 55 | ...post, 56 | comments: post.comments.filter((c) => c.id !== id), 57 | }; 58 | } 59 | return post; 60 | }); 61 | 62 | proxy.writeQuery({ 63 | query: GET_POSTS, 64 | data: { getPosts }, 65 | }); 66 | } 67 | if (urlProfile && !onNewsfeed && !onSinglePost) { 68 | const data = proxy.readQuery({ 69 | query: GET_URL_POSTS, 70 | variables: { 71 | username, 72 | }, 73 | }); 74 | 75 | const getUrlPosts = data.getUrlPosts.map((post) => { 76 | if (post.id === postId) { 77 | return { 78 | ...post, 79 | comments: post.comments.filter((c) => c.id !== id), 80 | }; 81 | } 82 | return post; 83 | }); 84 | 85 | proxy.writeQuery({ 86 | query: GET_URL_POSTS, 87 | data: { getUrlPosts }, 88 | variables: { 89 | username, 90 | }, 91 | }); 92 | } 93 | if (onNewsfeed && !urlProfile && !onSinglePost) { 94 | const data = proxy.readQuery({ 95 | query: GET_NEWSFEED, 96 | }); 97 | 98 | const getNewsfeed = data.getNewsfeed.map((post) => { 99 | if (post.id === postId) { 100 | return { 101 | ...post, 102 | comments: post.comments.filter((c) => c.id !== id), 103 | }; 104 | } 105 | return post; 106 | }); 107 | 108 | proxy.writeQuery({ 109 | query: GET_NEWSFEED, 110 | data: { getNewsfeed }, 111 | }); 112 | } 113 | if (!onNewsfeed && !urlProfile && onSinglePost) { 114 | const data = proxy.readQuery({ 115 | query: GET_SINGLE_POST, 116 | variables: { 117 | postId, 118 | }, 119 | }); 120 | 121 | const newComments = data.getSinglePost.comments.filter( 122 | (c) => c.id !== id 123 | ); 124 | 125 | const getSinglePost = { 126 | ...data.getSinglePost, 127 | comments: newComments, 128 | }; 129 | 130 | proxy.writeQuery({ 131 | query: GET_SINGLE_POST, 132 | data: { getSinglePost }, 133 | }); 134 | } 135 | }, 136 | }); 137 | 138 | const SettingsPopup = () => ( 139 | 140 | 141 | Delete Comment 142 | {loading && ( 143 | 154 | )} 155 | 156 | 157 | ); 158 | return ( 159 | <> 160 | 161 | 162 | 163 | 164 | 165 | {userId.firstName} {userId.lastName} 166 | 167 | 168 | {body} 169 | 170 | {userData && 171 | (userData.loadUser.id === userId.id || 172 | postCreatorId === userData.loadUser.id) && ( 173 | 187 | } 188 | closeOnDocumentClick 189 | > 190 | 191 | 192 | )} 193 | 194 | 195 | ); 196 | }; 197 | 198 | export default Comment; 199 | 200 | Comment.propTypes = { 201 | comment: PropTypes.shape({ 202 | userId: PropTypes.shape({ 203 | avatarImage: PropTypes.string, 204 | firstName: PropTypes.string, 205 | lastName: PropTypes.string, 206 | }), 207 | body: PropTypes.string, 208 | id: PropTypes.string, 209 | }), 210 | post: PropTypes.shape({ 211 | postId: PropTypes.string, 212 | postCreatorId: PropTypes.string, 213 | }), 214 | urlProfile: PropTypes.bool, 215 | onNewsfeed: PropTypes.bool, 216 | onSinglePost: PropTypes.bool, 217 | }; 218 | 219 | Comment.defaultProps = { 220 | comment: null, 221 | post: null, 222 | urlProfile: null, 223 | onNewsfeed: null, 224 | onSinglePost: null, 225 | }; 226 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutWorkAndEducation.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AboutInfoContainer = styled.div` 4 | display: flex; 5 | font-family: Roboto; 6 | justify-content: center; 7 | padding: 28px 0; 8 | `; 9 | 10 | export const AboutContainer = styled.div` 11 | display: flex; 12 | width: 876px; 13 | background-color: #fff; 14 | border-radius: 6px; 15 | @media only screen and (max-width: 575px) { 16 | border-radius: 0; 17 | } 18 | box-shadow: ${(props) => props.theme.boxShadow2}; 19 | `; 20 | 21 | export const AboutSidebar = styled.div` 22 | display: flex; 23 | flex-direction: column; 24 | width: 33%; 25 | border-right: 1px solid ${(props) => props.theme.secondaryBackground}; 26 | padding: 6px; 27 | `; 28 | 29 | export const AboutHeading = styled.p` 30 | font-size: 2rem; 31 | font-weight: bold; 32 | color: ${(props) => props.theme.secondaryText}; 33 | margin: 20px 10px; 34 | `; 35 | 36 | export const SidebarButton = styled.button` 37 | border: none; 38 | width: 100%; 39 | text-align: left; 40 | font-size: 1.5rem; 41 | padding: 10px; 42 | border-radius: 6px; 43 | cursor: pointer; 44 | font-weight: 600; 45 | margin-bottom: 8px; 46 | `; 47 | 48 | export const Overview = styled(SidebarButton)` 49 | color: ${(props) => props.theme.tertiaryText}; 50 | background-color: #fff; 51 | 52 | &:hover { 53 | background-color: ${(props) => props.theme.tertiaryBackground}; 54 | outline: none; 55 | } 56 | 57 | &::after, 58 | &:focus { 59 | outline: none; 60 | } 61 | 62 | &:active { 63 | background-color: ${(props) => props.theme.secondaryBackground}; 64 | outline: none; 65 | } 66 | `; 67 | 68 | export const WorkAndEducation = styled(SidebarButton)` 69 | color: ${(props) => props.theme.primaryText}; 70 | background-color: ${(props) => props.theme.primaryBackground}; 71 | 72 | &:focus { 73 | outline: none; 74 | } 75 | `; 76 | 77 | export const ContactAndBasicInfo = styled(SidebarButton)` 78 | color: ${(props) => props.theme.tertiaryText}; 79 | background-color: #fff; 80 | 81 | &:hover { 82 | background-color: ${(props) => props.theme.tertiaryBackground}; 83 | outline: none; 84 | } 85 | 86 | &::after, 87 | &:focus { 88 | outline: none; 89 | } 90 | 91 | &:active { 92 | background-color: ${(props) => props.theme.secondaryBackground}; 93 | outline: none; 94 | } 95 | `; 96 | 97 | export const AboutBodyContainer = styled.div` 98 | padding: 16px; 99 | flex-direction: column; 100 | width: 67%; 101 | display: flex; 102 | justify-content: center; 103 | `; 104 | 105 | export const WorkplaceContainer = styled.div` 106 | width: 100%; 107 | `; 108 | export const ActionHeading = styled.h1` 109 | color: ${(props) => props.theme.secondaryText}; 110 | font-weight: bold; 111 | font-size: 1.7rem; 112 | line-height: 1; 113 | margin: 0; 114 | `; 115 | 116 | export const ActionButton = styled.button` 117 | border: none; 118 | cursor: pointer; 119 | background-color: #fff; 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | padding: 0; 124 | margin-left: -5px; 125 | margin-top: 10px; 126 | &:focus { 127 | outline: none; 128 | } 129 | `; 130 | export const SchoolContainer = styled.div` 131 | width: 100%; 132 | margin-top: 32px; 133 | `; 134 | 135 | export const ActionSpan = styled.span` 136 | color: ${(props) => props.theme.primaryText}; 137 | font-size: 1.5rem; 138 | font-weight: 500; 139 | margin-left: 12px; 140 | `; 141 | 142 | export const School = styled.div` 143 | display: flex; 144 | align-items: center; 145 | `; 146 | 147 | export const ActionBody = styled.h1` 148 | font-size: 1.5rem; 149 | font-weight: 400; 150 | `; 151 | 152 | export const WorkPlace = styled.div` 153 | display: flex; 154 | align-items: center; 155 | margin-top: 10px; 156 | color: ${(props) => props.theme.secondaryText}; 157 | `; 158 | 159 | export const SettingsContainer = styled.button` 160 | margin-left: auto; 161 | border: none; 162 | border-radius: 50%; 163 | display: flex; 164 | padding: 0; 165 | justify-content: center; 166 | align-items: center; 167 | cursor: pointer; 168 | transition: 0.01s; 169 | padding: 5px; 170 | background-color: ${(props) => props.theme.secondaryBackground}; 171 | 172 | &:focus { 173 | background-color: ${(props) => props.theme.secondaryBackground}; 174 | outline: none; 175 | } 176 | &:active { 177 | transform: scale(0.96); 178 | } 179 | &:hover { 180 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 181 | } 182 | `; 183 | 184 | export const WorkplaceActionContainer = styled.form` 185 | display: flex; 186 | position: relative; 187 | flex-direction: column; 188 | `; 189 | 190 | export const SchoolActionContainer = styled.form` 191 | display: flex; 192 | position: relative; 193 | flex-direction: column; 194 | `; 195 | 196 | export const ActionInput = styled.input` 197 | border: 1px solid #ced0d4; 198 | margin-top: 15px; 199 | padding: 16px; 200 | border-radius: 6px; 201 | font-size: 1.5rem; 202 | ::placeholder { 203 | font-size: 1.5rem; 204 | } 205 | &:hover { 206 | border: 1px solid #8a8d91; 207 | } 208 | 209 | &:focus { 210 | outline: none; 211 | border: 1px solid ${(props) => props.theme.primaryText}; 212 | } 213 | `; 214 | 215 | export const FooterButton = styled.button` 216 | font-weight: 500; 217 | border: none; 218 | font-size: 1.5rem; 219 | border-radius: 6px; 220 | padding: 8px 12px; 221 | transition: 0.1s; 222 | cursor: pointer; 223 | `; 224 | 225 | export const CancelButton = styled(FooterButton)` 226 | margin-right: 8px; 227 | background-color: ${(props) => props.theme.secondaryBackground}; 228 | color: ${(props) => props.theme.secondaryText}; 229 | 230 | &&:focus { 231 | border-color: none; 232 | background-color: ${(props) => props.theme.secondaryBackground}; 233 | color: ${(props) => props.theme.secondaryText}; 234 | outline: none; 235 | } 236 | 237 | &&:active { 238 | transform: scale(0.96); 239 | } 240 | 241 | &&:hover { 242 | border-color: none; 243 | color: ${(props) => props.theme.secondaryText}; 244 | background-color: ${(props) => props.theme.secondaryHoverBackground}; 245 | } 246 | `; 247 | 248 | export const SaveButton = styled(FooterButton)` 249 | background-color: ${(props) => props.theme.primaryText}; 250 | color: #fff; 251 | 252 | &&:focus, 253 | &&:hover { 254 | background-color: ${(props) => props.theme.primaryText}; 255 | color: #fff; 256 | outline: none; 257 | } 258 | 259 | &&:active { 260 | color: #fff; 261 | transform: scale(0.96); 262 | } 263 | `; 264 | 265 | export const Footer = styled.div` 266 | display: flex; 267 | margin-top: 12px; 268 | justify-content: flex-end; 269 | `; 270 | 271 | export const AboutSkeleton = styled.div` 272 | display: flex; 273 | `; 274 | -------------------------------------------------------------------------------- /src/components/Message/SingleChat.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | import PropTypes from "prop-types"; 4 | import { useForm } from "react-hook-form"; 5 | import { useMutation, useQuery, useApolloClient } from "@apollo/react-hooks"; 6 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 7 | import Loader from "react-loader-spinner"; 8 | import { 9 | ChatBodySkeleton, 10 | ChatContainer, 11 | ChatHeader, 12 | CreatorAvatar, 13 | CreatorFullName, 14 | ChatBodyContainer, 15 | ChatFooterContainer, 16 | CreatorContainer, 17 | AuthUserContainer, 18 | CreatorImg, 19 | CreatorMessage, 20 | AuthUserMessage, 21 | InputContainer, 22 | MessageInput, 23 | SubmitMessageBtn, 24 | CloseContainer, 25 | ChatDataContainer, 26 | } from "./SingleChat.styles"; 27 | import { ReactComponent as CloseIcon } from "../../assets/icons/close.svg"; 28 | import { ReactComponent as RightArrowBtn } from "../../assets/icons/play.svg"; 29 | import { 30 | CREATE_MESSAGE, 31 | LOAD_USER, 32 | CREATE_THREAD, 33 | GET_SINGLE_CHAT, 34 | GET_THREAD, 35 | } from "../../utils/queries"; 36 | 37 | const SingleChat = ({ creator }) => { 38 | const el = useRef(null); 39 | const { register, setValue, watch, handleSubmit } = useForm(); 40 | 41 | const client = useApolloClient(); 42 | 43 | const { data: authUser } = useQuery(LOAD_USER); 44 | 45 | useEffect(() => { 46 | el.current.scrollTop = el.current.scrollHeight; 47 | }); 48 | 49 | // get thread if it exists 50 | const { data: getThreadData, refetch: refetchThreadQuery } = useQuery( 51 | GET_THREAD, 52 | { 53 | variables: { 54 | urlUser: creator.id, 55 | }, 56 | } 57 | ); 58 | 59 | const [createThread, { data: threadData }] = useMutation(CREATE_THREAD, { 60 | variables: { 61 | urlUser: creator.id, 62 | }, 63 | }); 64 | 65 | const { 66 | data: conversationData, 67 | loading: conversationLoading, 68 | refetch: refetchSingleChat, 69 | } = useQuery(GET_SINGLE_CHAT, { 70 | variables: { 71 | threadId: 72 | getThreadData && getThreadData.getThread && getThreadData.getThread.id, 73 | }, 74 | }); 75 | 76 | const [createMessage, { loading: createMessageLoading }] = useMutation( 77 | CREATE_MESSAGE, 78 | { 79 | variables: { 80 | notifier: creator.id, 81 | body: watch("message"), 82 | threadId: threadData && threadData.createThread.id, 83 | }, 84 | update: async (proxy, result) => { 85 | // We are checking if there is no thread and if its true then refetch the thread and the single chat which update the apollo cache automatically so we dont have to add the message manually 86 | if (getThreadData && !getThreadData.getThread) { 87 | const { data: newData } = await refetchThreadQuery(); 88 | 89 | await refetchSingleChat({ 90 | threadId: newData.getThread.id, 91 | }); 92 | } else { 93 | // if there is a thread + single chat that means that we can add the message manually on the creator ( the notifier gets the message by a subscription => check ProfileRoute:31) 94 | const data = proxy.readQuery({ 95 | query: GET_SINGLE_CHAT, 96 | variables: { 97 | threadId: threadData.createThread.id, 98 | }, 99 | }); 100 | const newData = { 101 | getSingleChat: [...data.getSingleChat, result.data.createMessage], 102 | }; 103 | proxy.writeQuery({ 104 | query: GET_SINGLE_CHAT, 105 | data: newData, 106 | variables: { 107 | threadId: threadData.createThread.id, 108 | }, 109 | }); 110 | } 111 | }, 112 | } 113 | ); 114 | 115 | const onSubmit = async () => { 116 | await createThread(); 117 | createMessage(); 118 | setValue("message", ""); 119 | }; 120 | 121 | return ( 122 | 123 | 124 | 125 | 126 | {creator.firstName} {creator.lastName} 127 | 128 | 130 | client.writeData({ 131 | data: { 132 | chat: { 133 | visible: false, 134 | __typename: "Chat", 135 | user: null, 136 | }, 137 | }, 138 | }) 139 | } 140 | > 141 | 142 | 143 | 144 | 145 | {!conversationLoading ? ( 146 | 147 | {conversationData && 148 | conversationData.getSingleChat.map((m) => 149 | m.creator.id === authUser.loadUser.id ? ( 150 | 151 | {m.body} 152 | 153 | ) : ( 154 | 155 | 156 | {m.body} 157 | 158 | ) 159 | )} 160 | 161 | ) : ( 162 | 163 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | )} 177 | 178 | 179 | <> 180 | 181 |
187 | 188 | {createMessageLoading && ( 189 | 200 | )} 201 |
202 | 207 | 208 | 209 |
210 | 211 |
212 |
213 | ); 214 | }; 215 | 216 | export default SingleChat; 217 | 218 | SingleChat.propTypes = { 219 | creator: PropTypes.shape({ 220 | id: PropTypes.string, 221 | firstName: PropTypes.string, 222 | lastName: PropTypes.string, 223 | avatarImage: PropTypes.string, 224 | }), 225 | }; 226 | 227 | SingleChat.defaultProps = { 228 | creator: null, 229 | }; 230 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutOverview.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "@apollo/react-hooks"; 3 | import ContentLoader from "react-content-loader"; 4 | import { Link, useParams } from "react-router-dom"; 5 | import { 6 | AboutSkeleton, 7 | AboutInfoContainer, 8 | AboutContainer, 9 | AboutSidebar, 10 | AboutHeading, 11 | AboutBodyContainer, 12 | Overview, 13 | WorkAndEducation, 14 | ContactAndBasicInfo, 15 | OverviewContainer, 16 | OverviewText, 17 | } from "./AboutOverview.styles"; 18 | import { LOAD_USER, LOAD_FROM_URL_USER } from "../../utils/queries"; 19 | import { ReactComponent as WorkplaceIcon } from "../../assets/icons/briefcase.svg"; 20 | import { ReactComponent as SchoolIcon } from "../../assets/icons/school.svg"; 21 | import { ReactComponent as HomeIcon } from "../../assets/icons/home.svg"; 22 | 23 | const AboutPageOverview = () => { 24 | const { data: userData, loading: authLoading } = useQuery(LOAD_USER); 25 | const { username } = useParams(); 26 | 27 | const { data: profileData, loading: profileLoading } = useQuery( 28 | LOAD_FROM_URL_USER, 29 | { 30 | variables: { 31 | username, 32 | }, 33 | } 34 | ); 35 | 36 | // const { loadUser: user } = userData; 37 | // const { loadFromUrlUser: profileUser } = profileData; 38 | 39 | /* eslint-disable consistent-return */ 40 | const readOnly = () => { 41 | if (userData) { 42 | if (userData.loadUser.username !== username) { 43 | return true; 44 | // eslint-disable-next-line no-else-return 45 | } else { 46 | return false; 47 | } 48 | } 49 | }; 50 | 51 | return ( 52 | 53 | 54 | {!authLoading && !profileLoading ? ( 55 | <> 56 | 57 | About 58 | 59 | Overview 60 | 61 | 62 | Work and Education 63 | 64 | 65 | 66 | Contact and Basic Info 67 | 68 | 69 | 70 | {readOnly() ? ( 71 | 72 | 73 | 74 | 75 | {profileData.loadFromUrlUser.workPlace ? ( 76 | <> 77 | Works at 78 | 79 | {" "} 80 | {profileData.loadFromUrlUser.workPlace} 81 | 82 | 83 | ) : ( 84 | 85 | No workplace to show 86 | 87 | )} 88 | 89 | 90 | 91 | 92 | 93 | {profileData.loadFromUrlUser.school ? ( 94 | <> 95 | Studies at{" "} 96 | 97 | {profileData.loadFromUrlUser.school} 98 | 99 | 100 | ) : ( 101 | 102 | No school to show 103 | 104 | )} 105 | 106 | 107 | 108 | 109 | 110 | {profileData.loadFromUrlUser.homePlace ? ( 111 | <> 112 | Lives in{" "} 113 | 114 | {profileData.loadFromUrlUser.homePlace} 115 | 116 | 117 | ) : ( 118 | 119 | No homeplace to show 120 | 121 | )} 122 | 123 | 124 | 125 | ) : ( 126 | 127 | 128 | 129 | 130 | {userData.loadUser.workPlace ? ( 131 | <> 132 | Works at 133 | 134 | {" "} 135 | {userData.loadUser.workPlace} 136 | 137 | 138 | ) : ( 139 | 140 | No workplace to show 141 | 142 | )} 143 | 144 | 145 | 146 | 147 | 148 | {userData.loadUser.school ? ( 149 | <> 150 | Studies at{" "} 151 | 152 | {userData.loadUser.school} 153 | 154 | 155 | ) : ( 156 | 157 | No school to show 158 | 159 | )} 160 | 161 | 162 | 163 | 164 | 165 | {userData.loadUser.homePlace ? ( 166 | <> 167 | Lives in{" "} 168 | 169 | {userData.loadUser.homePlace} 170 | 171 | 172 | ) : ( 173 | 174 | No homeplace to show 175 | 176 | )} 177 | 178 | 179 | 180 | )} 181 | 182 | ) : ( 183 | 184 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | )} 196 | 197 | 198 | ); 199 | }; 200 | 201 | export default AboutPageOverview; 202 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useQuery } from "@apollo/react-hooks"; 4 | import Popup from "reactjs-popup"; 5 | import { Link, useParams } from "react-router-dom"; 6 | import NotificationList from "../Notification/NotificationList"; 7 | import MessageList from "../Message/MessageList"; 8 | import { 9 | NavContainer, 10 | LogoContainer, 11 | GetUserContainer, 12 | SearchBar, 13 | SearchBarMobile, 14 | MobileLogoContainer, 15 | DesktopLogoContainer, 16 | User, 17 | Username, 18 | UserFullName, 19 | GetUserAvatar, 20 | ProfileContainer, 21 | NewsFeedContainer, 22 | MessageContainer, 23 | NotificationContainer, 24 | SettingsContainer, 25 | ArrowContainer, 26 | SearchContainer, 27 | UserAvatar, 28 | UsersContainer, 29 | UserContainer, 30 | FlexContainer, 31 | SearchInputContainer, 32 | IconContainer, 33 | } from "./Navbar.styles"; 34 | import { ReactComponent as Logo } from "../../assets/icons/logo.svg"; 35 | import { ReactComponent as HomeIcon } from "../../assets/icons/home.svg"; 36 | import { ReactComponent as ChatIcon } from "../../assets/icons/chatbox.svg"; 37 | import { ReactComponent as BellIcon } from "../../assets/icons/notifications.svg"; 38 | import { ReactComponent as SettingsIcon } from "../../assets/icons/caret-down-outline.svg"; 39 | import { ReactComponent as SearchIcon } from "../../assets/icons/search-outline.svg"; 40 | import { ReactComponent as CloseIcon } from "../../assets/icons/close.svg"; 41 | import { ReactComponent as ArrowIcon } from "../../assets/icons/arrow-back-outline.svg"; 42 | import { GET_USERS } from "../../utils/queries"; 43 | 44 | const Navbar = ({ onProfile, user }) => { 45 | const [display, setDisplay] = useState(""); 46 | const [search, setSearch] = useState(""); 47 | const { username } = useParams(); 48 | const ref = useRef(null); 49 | const { data } = useQuery(GET_USERS); 50 | 51 | const handleClickOutside = (event) => { 52 | const { current: wrap } = ref; 53 | if (wrap && !wrap.contains(event.target)) { 54 | setDisplay(false); 55 | } 56 | }; 57 | 58 | useEffect(() => { 59 | window.addEventListener("mousedown", handleClickOutside); 60 | return () => { 61 | window.removeEventListener("mousedown", handleClickOutside); 62 | }; 63 | }); 64 | 65 | return ( 66 | 67 | 68 | 69 | {!display ? ( 70 | 76 | 77 | 78 | 79 | 80 | ) : ( 81 | setDisplay(!display)}> 82 | 83 | 84 | )} 85 | 86 | 87 | 88 | 89 | 90 | 91 | { 100 | return !display && setDisplay(!display); 101 | }} 102 | onChange={(e) => setSearch(e.target.value)} 103 | /> 104 | {!display && ( 105 | 106 | 116 | 117 | )} 118 | 119 | setDisplay(!display)}> 120 | {!display ? ( 121 | 122 | ) : ( 123 | 124 | )} 125 | 126 | 127 | 128 | {display && ( 129 | 130 | setSearch(e.target.value)} 136 | /> 137 | 138 | {data && 139 | data.getUsers 140 | .filter(({ firstName, lastName }) => { 141 | const fullName = `${firstName.toLowerCase()} ${lastName.toLowerCase()}`; 142 | 143 | return fullName.indexOf(search.toLowerCase()) > -1; 144 | }) 145 | .map((getUser) => ( 146 | setDisplay(!display)} 150 | > 151 | 152 | 153 | 154 | {getUser.firstName} {getUser.lastName} 155 | 156 | 157 | 158 | ))} 159 | 160 | 161 | )} 162 | {!onProfile && ( 163 | 164 | 165 | 166 | )} 167 | 168 | 169 | {onProfile && user.username === username ? ( 170 | 171 | 172 | {user.firstName} 173 | 174 | ) : ( 175 | 176 | 177 | {user.firstName} 178 | 179 | )} 180 | 181 | 186 | 187 | 188 | } 189 | closeOnDocumentClick 190 | arrow={false} 191 | on="click" 192 | > 193 | 194 | 195 | 201 | 202 | 203 | } 204 | closeOnDocumentClick 205 | arrow={false} 206 | > 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | ); 215 | }; 216 | 217 | export default Navbar; 218 | 219 | Navbar.propTypes = { 220 | onProfile: PropTypes.bool, 221 | user: PropTypes.shape({ 222 | firstName: PropTypes.string, 223 | lastName: PropTypes.string, 224 | avatarImage: PropTypes.string, 225 | username: PropTypes.string, 226 | }), 227 | }; 228 | 229 | Navbar.defaultProps = { 230 | onProfile: null, 231 | user: null, 232 | }; 233 | -------------------------------------------------------------------------------- /src/components/RegisterForm/RegisterForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useMutation } from "@apollo/react-hooks"; 3 | import { useForm } from "react-hook-form"; 4 | import { useHistory, Link } from "react-router-dom"; 5 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 6 | import Loader from "react-loader-spinner"; 7 | import { REGISTER_USER } from "../../utils/queries"; 8 | import { 9 | RegisterFormContainer, 10 | StyledButton, 11 | RegisterHeading, 12 | Label, 13 | Input, 14 | BirthdayInput, 15 | NameContainer, 16 | FullNameContainer, 17 | LastNameContainer, 18 | EmailContainer, 19 | PasswordContainer, 20 | BirthdayContainer, 21 | GenderContainer, 22 | GenderLabel, 23 | GenderInputContainer, 24 | FemaleContainer, 25 | ErrorMessageContainer, 26 | ErrorMessageHeading, 27 | LoginContainer, 28 | LoginLink, 29 | Gender, 30 | } from "./RegisterForm.styles"; 31 | import { ReactComponent as ErrorIcon } from "../../assets/icons/alert-circle.svg"; 32 | 33 | const RegisterForm = () => { 34 | const { register, handleSubmit, getValues, errors } = useForm(); 35 | const [graphQLError, setGraphQLError] = useState(undefined); 36 | 37 | const history = useHistory(); 38 | 39 | const [registerUser, { loading }] = useMutation(REGISTER_USER, { 40 | onCompleted: (result) => { 41 | const { token, username } = result.register; 42 | localStorage.setItem("token", token); 43 | history.push(`/${username}`); 44 | }, 45 | variables: { 46 | firstName: getValues("firstName"), 47 | lastName: getValues("lastName"), 48 | email: getValues("email"), 49 | password: getValues("password"), 50 | gender: getValues("gender"), 51 | birthday: getValues("birthday"), 52 | }, 53 | onError: (error) => setGraphQLError(error.graphQLErrors[0]), 54 | }); 55 | 56 | const onSubmitRegister = () => { 57 | registerUser(); 58 | }; 59 | 60 | return ( 61 | 62 | Sign Up to Fakebooker 63 |
67 | 68 | 69 | 70 | 80 | {errors.firstName && ( 81 | 82 | 83 | 84 | {errors.firstName.message} 85 | 86 | 87 | )} 88 | 89 | 90 | 91 | 101 | {errors.lastName && ( 102 | 103 | 104 | 105 | {errors.lastName.message} 106 | 107 | 108 | )} 109 | 110 | 111 | 112 | 113 | 123 | {errors.email && ( 124 | 125 | 126 | {errors.email.message} 127 | 128 | )} 129 | {graphQLError && ( 130 | 131 | 132 | {graphQLError.message} 133 | 134 | )} 135 | 136 | 137 | 138 | 145 | {errors.password && ( 146 | 147 | 148 | 149 | {errors.password.message} 150 | 151 | 152 | )} 153 | 154 | 155 | 156 | 164 | {errors.birthday && ( 165 | 166 | 167 | 168 | {errors.birthday.message} 169 | 170 | 171 | )} 172 | 173 | 174 | 175 |
176 | 177 | 185 | Female 186 | 187 | 188 | 196 | Male 197 | 198 |
199 | {errors.gender && ( 200 | 201 | 202 | {errors.gender.message} 203 | 204 | )} 205 |
206 | 207 | Create Account 208 | {loading && ( 209 | 220 | )} 221 | 222 |
223 | 224 | Already a member? 225 | 226 | Sign In 227 | 228 | 229 |
230 | ); 231 | }; 232 | 233 | export default RegisterForm; 234 | -------------------------------------------------------------------------------- /src/components/Post/Post.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useHistory, Link } from "react-router-dom"; 3 | import moment from "moment/moment"; 4 | import PropTypes from "prop-types"; 5 | import { useMutation } from "@apollo/react-hooks"; 6 | import ReactMarkdown from "react-markdown"; 7 | import Popup from "reactjs-popup"; 8 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 9 | import Loader from "react-loader-spinner"; 10 | import { 11 | PostContainer, 12 | SettingsContainer, 13 | PostHeader, 14 | ProfileAvatar, 15 | ProfileWrapper, 16 | NameWrapper, 17 | ProfileName, 18 | PostCreation, 19 | PostContent, 20 | PostFooter, 21 | PopContainer, 22 | PopButton, 23 | LikesCount, 24 | CommentsCount, 25 | FooterHeading, 26 | CommentsContainer, 27 | PostImage, 28 | FooterButton, 29 | } from "./Post.styles"; 30 | import { ReactComponent as CommentsSVG } from "../../assets/icons/chatbox.svg"; 31 | import { ReactComponent as LikesSVG } from "../../assets/icons/thumbs-up.svg"; 32 | import { ReactComponent as ThreeDotsSvg } from "../../assets/icons/ellipsis-horizontal.svg"; 33 | import Comment from "../Comment/Comment"; 34 | import CreateComment from "../Comment/CreateComment"; 35 | import { 36 | DELETE_POST, 37 | LIKE_POST, 38 | GET_SINGLE_POST, 39 | GET_POSTS, 40 | GET_NEWSFEED, 41 | } from "../../utils/queries"; 42 | 43 | const Post = ({ post, user, readOnly, onNewsfeed, onSinglePost }) => { 44 | const [liked, setLiked] = useState(false); 45 | useEffect(() => { 46 | if (user && post.likes.find((like) => like.userId === user.id)) { 47 | setLiked(true); 48 | } else setLiked(false); 49 | }, [user, post]); 50 | 51 | const history = useHistory(); 52 | const [deletePost, { loading: deletePostLoading }] = useMutation( 53 | DELETE_POST, 54 | { 55 | variables: { 56 | postId: post.id, 57 | }, 58 | onCompleted: () => { 59 | if (history.location.pathname === `/post/${post.id}`) { 60 | history.push("/"); 61 | } 62 | }, 63 | onError: () => { 64 | if (history.location.pathname === `/post/${post.id}`) { 65 | history.push("/"); 66 | } 67 | }, 68 | update: (proxy) => { 69 | if (!onNewsfeed && !onSinglePost) { 70 | const data = proxy.readQuery({ 71 | query: GET_POSTS, 72 | }); 73 | 74 | const newPostList = data.getPosts.filter((p) => p.id !== post.id); 75 | 76 | const newData = { getPosts: [...newPostList] }; 77 | 78 | proxy.writeQuery({ 79 | query: GET_POSTS, 80 | data: newData, 81 | }); 82 | } 83 | if (onNewsfeed) { 84 | const data = proxy.readQuery({ 85 | query: GET_NEWSFEED, 86 | }); 87 | 88 | const newPostList = data.getNewsfeed.filter((p) => p.id !== post.id); 89 | 90 | const newData = { getNewsfeed: [...newPostList] }; 91 | 92 | proxy.writeQuery({ 93 | query: GET_NEWSFEED, 94 | data: newData, 95 | }); 96 | } 97 | if (onSinglePost) { 98 | const newData = { getSinglePost: null }; 99 | 100 | proxy.writeQuery({ 101 | query: GET_SINGLE_POST, 102 | data: newData, 103 | variables: { 104 | postId: post.id, 105 | }, 106 | }); 107 | } 108 | }, 109 | } 110 | ); 111 | 112 | const [likePost, { loading }] = useMutation(LIKE_POST, { 113 | variables: { 114 | postId: post.id, 115 | }, 116 | }); 117 | 118 | const SettingsPopup = () => ( 119 | 120 | 121 | Delete Post 122 | {deletePostLoading && ( 123 | 134 | )} 135 | 136 | 137 | ); 138 | 139 | return ( 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | {post.userId.firstName} {post.userId.lastName} 148 | 149 | 150 | 151 | {moment(Number(post.createdAt)).fromNow()} 152 | 153 | 154 | 155 | {!readOnly && post.userId.id === user.id && ( 156 | 162 | 170 | 171 | } 172 | closeOnDocumentClick 173 | > 174 | 175 | 176 | )} 177 | 178 | 179 |
185 | 186 |
187 |
188 | {post.image && } 189 | 190 | {liked && !loading ? ( 191 | 192 | 193 | {post.likes.length} 194 | 195 | ) : ( 196 | 197 | 198 | {loading ? ( 199 | 208 | ) : ( 209 | Like 210 | )} 211 | 212 | )} 213 | 214 | 215 | 216 | {post.comments.length === 0 ? ( 217 | Comment 218 | ) : ( 219 | post.comments.length 220 | )} 221 | 222 | 223 | 224 | 225 | {post.comments.map((comment) => ( 226 | 234 | ))} 235 | 242 | 243 |
244 | ); 245 | }; 246 | 247 | export default Post; 248 | 249 | Post.propTypes = { 250 | post: PropTypes.shape({ 251 | id: PropTypes.string, 252 | userId: PropTypes.shape({ 253 | id: PropTypes.string, 254 | avatarImage: PropTypes.string, 255 | firstName: PropTypes.string, 256 | lastName: PropTypes.string, 257 | }), 258 | createdAt: PropTypes.string, 259 | body: PropTypes.string, 260 | image: PropTypes.string, 261 | likes: PropTypes.array, 262 | comments: PropTypes.array, 263 | }), 264 | user: PropTypes.shape({ 265 | id: PropTypes.string, 266 | avatarImage: PropTypes.string, 267 | firstName: PropTypes.string, 268 | lastName: PropTypes.string, 269 | email: PropTypes.string, 270 | birthday: PropTypes.string, 271 | gender: PropTypes.string, 272 | coverImage: PropTypes.string, 273 | }), 274 | readOnly: PropTypes.bool, 275 | onNewsfeed: PropTypes.bool, 276 | }; 277 | 278 | Post.defaultProps = { 279 | post: null, 280 | user: null, 281 | readOnly: null, 282 | onNewsfeed: null, 283 | onSinglePost: null, 284 | }; 285 | -------------------------------------------------------------------------------- /src/pages/NewsfeedPage/NewsfeedPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useQuery, useApolloClient } from "@apollo/react-hooks"; 3 | import ContentLoader from "react-content-loader"; 4 | import Navbar from "../../components/Navbar/Navbar"; 5 | import Post from "../../components/Post/Post"; 6 | import CreatePostDefault from "../../components/Post/CreatePostDefault"; 7 | import { LOAD_USER, GET_NEWSFEED, NEW_POST } from "../../utils/queries"; 8 | import { 9 | InfoContainer, 10 | PostsSection, 11 | PostContainer, 12 | ContactsSidebar, 13 | ContactsContainer, 14 | CreatePostSkeleton, 15 | ContactsHeader, 16 | ContactsHeading, 17 | ContactsBody, 18 | ContactAvatar, 19 | ContactFullName, 20 | NavbarSkeleton, 21 | PostSkeleton, 22 | ContactSkeleton, 23 | } from "./NewsfeedPage.styles"; 24 | 25 | const NewsfeedPage = () => { 26 | const { data: userData } = useQuery(LOAD_USER); 27 | const client = useApolloClient(); 28 | const [pageLoading, setPageLoading] = useState(true); 29 | const { data: newsfeedData, subscribeToMore, loading } = useQuery( 30 | GET_NEWSFEED, 31 | { 32 | fetchPolicy: "network-only", 33 | } 34 | ); 35 | 36 | useEffect(() => { 37 | setPageLoading(false); 38 | subscribeToMore({ 39 | document: NEW_POST, 40 | updateQuery: (prev, { subscriptionData }) => { 41 | if (!subscriptionData.data) return prev; 42 | const post = subscriptionData.data.newPost; 43 | return { getNewsfeed: [post, ...prev.getNewsfeed] }; 44 | }, 45 | }); 46 | }, [subscribeToMore]); 47 | 48 | return ( 49 | <> 50 | {!pageLoading && userData ? ( 51 | 52 | ) : ( 53 | 54 | 55 | 56 | 57 | 58 | )} 59 | 60 | 61 | 62 | {!pageLoading && !loading && userData ? ( 63 | 64 | ) : ( 65 | 66 | 67 | 68 | 69 | 70 | )} 71 | {newsfeedData && !pageLoading && !loading && userData ? ( 72 | newsfeedData.getNewsfeed.map((post) => ( 73 | 79 | )) 80 | ) : ( 81 | <> 82 | 83 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | )} 140 | 141 | 142 | 143 | 144 | 145 | 146 | Contacts 147 | 148 | {userData && !pageLoading ? ( 149 | userData.loadUser.friends.map((friend) => ( 150 | 153 | client.writeData({ 154 | data: { 155 | chat: { 156 | visible: true, 157 | __typename: "Chat", 158 | user: { 159 | ...friend, 160 | __typename: "User", 161 | }, 162 | }, 163 | }, 164 | }) 165 | } 166 | > 167 | 168 | 169 | {friend.firstName} {friend.lastName} 170 | 171 | 172 | )) 173 | ) : ( 174 | <> 175 | 176 | 184 | 185 | 186 | 187 | 188 | 189 | 197 | 198 | 199 | 200 | 201 | 202 | 210 | 211 | 212 | 213 | 214 | 215 | )} 216 | 217 | 218 | 219 | ); 220 | }; 221 | 222 | export default NewsfeedPage; 223 | --------------------------------------------------------------------------------