├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/icons/school.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
58 |
59 | >
60 | )}
61 | {authData && authData.loadUser.id === creator._id && (
62 | <>
63 |
64 |
65 |
66 | {notifier.firstName} {notifier.lastName}
67 |
68 |
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 |
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  
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 | 
9 |
10 | **ProfilePage:**
11 | 
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 |
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 |
--------------------------------------------------------------------------------