├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── Apollo
│ ├── Client.js
│ └── LocalState.js
├── Components
│ ├── App.js
│ ├── Avatar.js
│ ├── Button.js
│ ├── FatText.js
│ ├── FollowButton
│ │ ├── FollowButtonContainer.js
│ │ ├── FollowButtonPresenter.js
│ │ ├── FollowButtonQueries.js
│ │ └── index.js
│ ├── Footer.js
│ ├── Header.js
│ ├── Icons.js
│ ├── Input.js
│ ├── Loader.js
│ ├── Post
│ │ ├── PostContainer.js
│ │ ├── PostPresenter.js
│ │ ├── PostQueries.js
│ │ └── index.js
│ ├── Routes.js
│ ├── SquarePost.js
│ └── UserCard.js
├── Hooks
│ └── useInput.js
├── Routes
│ ├── Auth
│ │ ├── AuthContainer.js
│ │ ├── AuthPresenter.js
│ │ ├── AuthQueries.js
│ │ └── index.js
│ ├── EditProfile.js
│ ├── Explore.js
│ ├── Feed.js
│ ├── Post.js
│ ├── Profile
│ │ ├── ProfileContainer.js
│ │ ├── ProfilePresenter.js
│ │ └── index.js
│ └── Search
│ │ ├── SearchContainer.js
│ │ ├── SearchPresenter.js
│ │ ├── SearchQueries.js
│ │ └── index.js
├── SharedQueries.js
├── Styles
│ ├── GlobalStyles.js
│ └── Theme.js
└── index.js
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instaclone Frontend
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prismagram-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-boost": "^0.3.1",
7 | "graphql": "^14.1.1",
8 | "prop-types": "^15.7.2",
9 | "react": "^16.8.4",
10 | "react-apollo-hooks": "^0.4.3",
11 | "react-autosize-textarea": "^6.0.0",
12 | "react-dom": "^16.8.4",
13 | "react-helmet": "^5.2.0",
14 | "react-router-dom": "^4.3.1",
15 | "react-scripts": "2.1.8",
16 | "react-toastify": "^4.5.2",
17 | "rl-react-helmet": "^5.2.0",
18 | "styled-components": "^4.1.3",
19 | "styled-reset": "^2.0.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": [
31 | ">0.2%",
32 | "not dead",
33 | "not ie <= 11",
34 | "not op_mini all"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nomadcoders/prismagram-frontend/5b90b0200be1949aeb89213e43f25143457c3532/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/Apollo/Client.js:
--------------------------------------------------------------------------------
1 | import ApolloClient from "apollo-boost";
2 | import { defaults, resolvers } from "./LocalState";
3 |
4 | export default new ApolloClient({
5 | uri:
6 | process.env.NODE_ENV === "development"
7 | ? "http://localhost:4000"
8 | : "https://prismagram-backend.herokuapp.com",
9 | clientState: {
10 | defaults,
11 | resolvers
12 | },
13 | headers: {
14 | Authorization: `Bearer ${localStorage.getItem("token")}`
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/Apollo/LocalState.js:
--------------------------------------------------------------------------------
1 | export const defaults = {
2 | isLoggedIn: Boolean(localStorage.getItem("token")) || false
3 | };
4 |
5 | export const resolvers = {
6 | Mutation: {
7 | logUserIn: (_, { token }, { cache }) => {
8 | localStorage.setItem("token", token);
9 | cache.writeData({
10 | data: {
11 | isLoggedIn: true
12 | }
13 | });
14 | return null;
15 | },
16 | logUserOut: (_, __, { cache }) => {
17 | localStorage.removeItem("token");
18 | window.location = "/";
19 | return null;
20 | }
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/Components/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { gql } from "apollo-boost";
3 | import styled, { ThemeProvider } from "styled-components";
4 | import { HashRouter as Router } from "react-router-dom";
5 | import { useQuery } from "react-apollo-hooks";
6 | import { ToastContainer, toast } from "react-toastify";
7 | import "react-toastify/dist/ReactToastify.css";
8 | import GlobalStyles from "../Styles/GlobalStyles";
9 | import Theme from "../Styles/Theme";
10 | import Routes from "./Routes";
11 | import Footer from "./Footer";
12 | import Header from "./Header";
13 |
14 | const QUERY = gql`
15 | {
16 | isLoggedIn @client
17 | }
18 | `;
19 |
20 | const Wrapper = styled.div`
21 | margin: 0 auto;
22 | max-width: ${props => props.theme.maxWidth};
23 | width: 100%;
24 | `;
25 |
26 | export default () => {
27 | const {
28 | data: { isLoggedIn }
29 | } = useQuery(QUERY);
30 |
31 | return (
32 |
33 | <>
34 |
35 |
36 | <>
37 | {isLoggedIn && }
38 |
39 |
40 |
41 |
42 | >
43 |
44 |
45 | >
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/Components/Avatar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 |
5 | const getSize = size => {
6 | let number;
7 | if (size === "sm") {
8 | number = 30;
9 | } else if (size === "md") {
10 | number = 50;
11 | } else if (size === "lg") {
12 | number = 150;
13 | }
14 | return `
15 | width:${number}px;
16 | height:${number}px;
17 | `;
18 | };
19 |
20 | const Container = styled.div`
21 | ${props => getSize(props.size)}
22 | background-image:url(${props => props.url});
23 | background-size:cover;
24 | border-radius:50%;
25 | `;
26 |
27 | const Avatar = ({ size = "sm", url, className }) => (
28 |
29 | );
30 |
31 | Avatar.propTypes = {
32 | size: PropTypes.oneOf(["sm", "md", "lg"]),
33 | url: PropTypes.string.isRequired
34 | };
35 |
36 | export default Avatar;
37 |
--------------------------------------------------------------------------------
/src/Components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 |
5 | const Container = styled.button`
6 | width: 100%;
7 | border: 0;
8 | border-radius: ${props => props.theme.borderRadius};
9 | color: white;
10 | font-weight: 600;
11 | background-color: ${props => props.theme.blueColor};
12 | text-align: center;
13 | padding: 7px 0px;
14 | font-size: 14px;
15 | cursor: pointer;
16 | `;
17 |
18 | const Button = ({ text, onClick }) => (
19 | {text}
20 | );
21 |
22 | Button.propTypes = {
23 | text: PropTypes.string.isRequired
24 | };
25 |
26 | export default Button;
27 |
--------------------------------------------------------------------------------
/src/Components/FatText.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | const Text = styled.span`
6 | font-weight: 600;
7 | `;
8 |
9 | const FatText = ({ text, className }) => (
10 | {text}
11 | );
12 |
13 | FatText.propTypes = {
14 | text: PropTypes.string.isRequired
15 | };
16 |
17 | export default FatText;
18 |
--------------------------------------------------------------------------------
/src/Components/FollowButton/FollowButtonContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import { useMutation } from "react-apollo-hooks";
4 | import { FOLLOW, UNFOLLOW } from "./FollowButtonQueries";
5 | import FollowButtonPresenter from "./FollowButtonPresenter";
6 |
7 | const FollowButtonContainer = ({ isFollowing, id }) => {
8 | const [isFollowingS, setIsFollowing] = useState(isFollowing);
9 | const followMutation = useMutation(FOLLOW, { variables: { id } });
10 | const unfollowMutation = useMutation(UNFOLLOW, { variables: { id } });
11 |
12 | const onClick = () => {
13 | if (isFollowingS === true) {
14 | setIsFollowing(false);
15 | unfollowMutation();
16 | } else {
17 | setIsFollowing(true);
18 | followMutation();
19 | }
20 | };
21 | return ;
22 | };
23 |
24 | FollowButtonContainer.propTypes = {
25 | isFollowing: PropTypes.bool.isRequired,
26 | id: PropTypes.string.isRequired
27 | };
28 |
29 | export default FollowButtonContainer;
30 |
--------------------------------------------------------------------------------
/src/Components/FollowButton/FollowButtonPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Button from "../Button";
3 |
4 | export default ({ isFollowing, onClick }) => (
5 |
6 | );
7 |
--------------------------------------------------------------------------------
/src/Components/FollowButton/FollowButtonQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const FOLLOW = gql`
4 | mutation follow($id: String!) {
5 | follow(id: $id)
6 | }
7 | `;
8 |
9 | export const UNFOLLOW = gql`
10 | mutation unfollow($id: String!) {
11 | unfollow(id: $id)
12 | }
13 | `;
14 |
--------------------------------------------------------------------------------
/src/Components/FollowButton/index.js:
--------------------------------------------------------------------------------
1 | import FollowButtonContainer from "./FollowButtonContainer";
2 | export default FollowButtonContainer;
3 |
--------------------------------------------------------------------------------
/src/Components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Footer = styled.footer`
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | text-transform: uppercase;
9 | font-weight: 600;
10 | font-size: 12px;
11 | margin: 50px 0px;
12 | `;
13 |
14 | const List = styled.ul`
15 | display: flex;
16 | `;
17 |
18 | const ListItem = styled.li`
19 | &:not(:last-child) {
20 | margin-right: 16px;
21 | }
22 | `;
23 |
24 | const Link = styled.a`
25 | color: ${props => props.theme.darkBlueColor};
26 | `;
27 |
28 | const Copyright = styled.span`
29 | color: ${props => props.theme.darkGreyColor};
30 | `;
31 |
32 | export default () => (
33 |
71 | );
72 |
--------------------------------------------------------------------------------
/src/Components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link, withRouter } from "react-router-dom";
4 | import Input from "./Input";
5 | import useInput from "../Hooks/useInput";
6 | import { Compass, HeartEmpty, User, Logo } from "./Icons";
7 | import { useQuery } from "react-apollo-hooks";
8 | import { ME } from "../SharedQueries";
9 |
10 | const Header = styled.header`
11 | width: 100%;
12 | border: 0;
13 | position: fixed;
14 | top: 0;
15 | left: 0;
16 | background-color: white;
17 | border-bottom: ${props => props.theme.boxBorder};
18 | border-radius: 0px;
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | padding: 25px 0px;
23 | z-index: 2;
24 | `;
25 |
26 | const HeaderWrapper = styled.div`
27 | width: 100%;
28 | max-width: ${props => props.theme.maxWidth};
29 | display: flex;
30 | justify-content: center;
31 | `;
32 |
33 | const HeaderColumn = styled.div`
34 | width: 33%;
35 | text-align: center;
36 | &:first-child {
37 | margin-right: auto;
38 | text-align: left;
39 | }
40 | &:last-child {
41 | margin-left: auto;
42 | text-align: right;
43 | }
44 | `;
45 |
46 | const SearchInput = styled(Input)`
47 | background-color: ${props => props.theme.bgColor};
48 | padding: 5px;
49 | font-size: 14px;
50 | border-radius: 3px;
51 | height: auto;
52 | text-align: center;
53 | width: 70%;
54 | &::placeholder {
55 | opacity: 0.8;
56 | font-weight: 200;
57 | }
58 | `;
59 |
60 | const HeaderLink = styled(Link)`
61 | &:not(:last-child) {
62 | margin-right: 30px;
63 | }
64 | `;
65 |
66 | export default withRouter(({ history }) => {
67 | const search = useInput("");
68 | const { data } = useQuery(ME);
69 | const onSearchSubmit = e => {
70 | e.preventDefault();
71 | history.push(`/search?term=${search.value}`);
72 | };
73 | return (
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {!data.me ? (
98 |
99 |
100 |
101 | ) : (
102 |
103 |
104 |
105 | )}
106 |
107 |
108 |
109 | );
110 | });
111 |
--------------------------------------------------------------------------------
/src/Components/Icons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const Compass = () => (
4 |
12 | );
13 |
14 | export const HeartEmpty = () => (
15 |
23 | );
24 |
25 | export const HeartFull = () => (
26 |
35 | );
36 |
37 | export const User = () => (
38 |
46 | );
47 |
48 | export const Comment = () => (
49 |
57 | );
58 |
59 | export const CommentFull = () => (
60 |
68 | );
69 |
70 | export const Logo = ({ size = 24 }) => (
71 |
79 | );
80 |
--------------------------------------------------------------------------------
/src/Components/Input.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | const Container = styled.input`
6 | border: 0;
7 | border: ${props => props.theme.boxBorder};
8 | border-radius: ${props => props.theme.borderRadius};
9 | background-color: ${props => props.theme.bgColor};
10 | height: 35px;
11 | font-size: 12px;
12 | padding: 0px 15px;
13 | `;
14 |
15 | const Input = ({
16 | placeholder,
17 | required = true,
18 | value,
19 | onChange,
20 | type = "text",
21 | className
22 | }) => (
23 |
31 | );
32 |
33 | Input.propTypes = {
34 | placeholder: PropTypes.string.isRequired,
35 | required: PropTypes.bool,
36 | value: PropTypes.string.isRequired,
37 | onChange: PropTypes.func.isRequired,
38 | type: PropTypes.string
39 | };
40 |
41 | export default Input;
42 |
--------------------------------------------------------------------------------
/src/Components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { keyframes } from "styled-components";
3 | import { Logo } from "./Icons";
4 |
5 | const Animation = keyframes`
6 | 0%{
7 | opacity:0
8 | }
9 | 50%{
10 | opacity:1
11 | }
12 | 100%{
13 | opacity:0;
14 | }
15 | `;
16 |
17 | const Loader = styled.div`
18 | animation: ${Animation} 1s linear infinite;
19 | width: 100%;
20 | text-align: center;
21 | `;
22 |
23 | export default () => (
24 |
25 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/Components/Post/PostContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import useInput from "../../Hooks/useInput";
4 | import PostPresenter from "./PostPresenter";
5 | import { useMutation } from "react-apollo-hooks";
6 | import { TOGGLE_LIKE, ADD_COMMENT } from "./PostQueries";
7 | import { toast } from "react-toastify";
8 |
9 | const PostContainer = ({
10 | id,
11 | user,
12 | files,
13 | likeCount,
14 | isLiked,
15 | comments,
16 | createdAt,
17 | caption,
18 | location
19 | }) => {
20 | const [isLikedS, setIsLiked] = useState(isLiked);
21 | const [likeCountS, setLikeCount] = useState(likeCount);
22 | const [currentItem, setCurrentItem] = useState(0);
23 | const [selfComments, setSelfComments] = useState([]);
24 | const comment = useInput("");
25 | const toggleLikeMutation = useMutation(TOGGLE_LIKE, {
26 | variables: { postId: id }
27 | });
28 | const addCommentMutation = useMutation(ADD_COMMENT, {
29 | variables: { postId: id, text: comment.value }
30 | });
31 | const slide = () => {
32 | const totalFiles = files.length;
33 | if (currentItem === totalFiles - 1) {
34 | setTimeout(() => setCurrentItem(0), 3000);
35 | } else {
36 | setTimeout(() => setCurrentItem(currentItem + 1), 3000);
37 | }
38 | };
39 | useEffect(() => {
40 | slide();
41 | }, [currentItem]);
42 |
43 | const toggleLike = () => {
44 | toggleLikeMutation();
45 | if (isLikedS === true) {
46 | setIsLiked(false);
47 | setLikeCount(likeCountS - 1);
48 | } else {
49 | setIsLiked(true);
50 | setLikeCount(likeCountS + 1);
51 | }
52 | };
53 |
54 | const onKeyPress = async event => {
55 | const { which } = event;
56 | if (which === 13) {
57 | event.preventDefault();
58 | try {
59 | const {
60 | data: { addComment }
61 | } = await addCommentMutation();
62 | setSelfComments([...selfComments, addComment]);
63 | comment.setValue("");
64 | } catch {
65 | toast.error("Cant send comment");
66 | }
67 | }
68 | };
69 |
70 | return (
71 |
88 | );
89 | };
90 |
91 | PostContainer.propTypes = {
92 | id: PropTypes.string.isRequired,
93 | user: PropTypes.shape({
94 | id: PropTypes.string.isRequired,
95 | avatar: PropTypes.string,
96 | username: PropTypes.string.isRequired
97 | }).isRequired,
98 | files: PropTypes.arrayOf(
99 | PropTypes.shape({
100 | id: PropTypes.string.isRequired,
101 | url: PropTypes.string.isRequired
102 | })
103 | ).isRequired,
104 | likeCount: PropTypes.number.isRequired,
105 | isLiked: PropTypes.bool.isRequired,
106 | comments: PropTypes.arrayOf(
107 | PropTypes.shape({
108 | id: PropTypes.string.isRequired,
109 | text: PropTypes.string.isRequired,
110 | user: PropTypes.shape({
111 | id: PropTypes.string.isRequired,
112 | username: PropTypes.string.isRequired
113 | }).isRequired
114 | })
115 | ).isRequired,
116 | caption: PropTypes.string.isRequired,
117 | location: PropTypes.string,
118 | createdAt: PropTypes.string.isRequired
119 | };
120 |
121 | export default PostContainer;
122 |
--------------------------------------------------------------------------------
/src/Components/Post/PostPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import TextareaAutosize from "react-autosize-textarea";
5 | import FatText from "../FatText";
6 | import Avatar from "../Avatar";
7 | import { HeartFull, HeartEmpty, Comment as CommentIcon } from "../Icons";
8 |
9 | const Post = styled.div`
10 | ${props => props.theme.whiteBox};
11 | width: 100%;
12 | max-width: 600px;
13 | user-select: none;
14 | margin-bottom: 25px;
15 | a {
16 | color: inherit;
17 | }
18 | `;
19 |
20 | const Header = styled.header`
21 | padding: 15px;
22 | display: flex;
23 | align-items: center;
24 | `;
25 |
26 | const UserColumn = styled.div`
27 | margin-left: 10px;
28 | `;
29 |
30 | const Location = styled.span`
31 | display: block;
32 | margin-top: 5px;
33 | font-size: 12px;
34 | `;
35 |
36 | const Files = styled.div`
37 | position: relative;
38 | padding-bottom: 100%;
39 | display: flex;
40 | flex-direction: column;
41 | align-items: stretch;
42 | flex-shrink: 0;
43 | `;
44 |
45 | const File = styled.div`
46 | max-width: 100%;
47 | width: 100%;
48 | height: 600px;
49 | position: absolute;
50 | top: 0;
51 | background-image: url(${props => props.src});
52 | background-size: cover;
53 | background-position: center;
54 | opacity: ${props => (props.showing ? 1 : 0)};
55 | transition: opacity 0.5s linear;
56 | `;
57 |
58 | const Button = styled.span`
59 | cursor: pointer;
60 | `;
61 |
62 | const Meta = styled.div`
63 | padding: 15px;
64 | `;
65 |
66 | const Buttons = styled.div`
67 | ${Button} {
68 | &:first-child {
69 | margin-right: 10px;
70 | }
71 | }
72 | margin-bottom: 10px;
73 | `;
74 |
75 | const Timestamp = styled.span`
76 | font-weight: 400;
77 | text-transform: uppercase;
78 | opacity: 0.5;
79 | display: block;
80 | font-size: 12px;
81 | margin: 10px 0px;
82 | padding-bottom: 10px;
83 | border-bottom: ${props => props.theme.lightGreyColor} 1px solid;
84 | `;
85 |
86 | const Textarea = styled(TextareaAutosize)`
87 | border: none;
88 | width: 100%;
89 | resize: none;
90 | font-size: 14px;
91 | &:focus {
92 | outline: none;
93 | }
94 | `;
95 |
96 | const Comments = styled.ul`
97 | margin-top: 10px;
98 | `;
99 |
100 | const Comment = styled.li`
101 | margin-bottom: 7px;
102 | span {
103 | margin-right: 5px;
104 | }
105 | `;
106 |
107 | const Caption = styled.div`
108 | margin: 10px 0px;
109 | `;
110 |
111 | export default ({
112 | user: { username, avatar },
113 | location,
114 | files,
115 | isLiked,
116 | likeCount,
117 | createdAt,
118 | newComment,
119 | currentItem,
120 | toggleLike,
121 | onKeyPress,
122 | comments,
123 | selfComments,
124 | caption
125 | }) => (
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | {location}
134 |
135 |
136 |
137 | {files &&
138 | files.map((file, index) => (
139 |
140 | ))}
141 |
142 |
143 |
144 |
147 |
150 |
151 |
152 |
153 | {caption}
154 |
155 | {comments && (
156 |
157 | {comments.map(comment => (
158 |
159 |
160 | {comment.text}
161 |
162 | ))}
163 | {selfComments.map(comment => (
164 |
165 |
166 | {comment.text}
167 |
168 | ))}
169 |
170 | )}
171 | {createdAt}
172 |
178 |
179 |
180 | );
181 |
--------------------------------------------------------------------------------
/src/Components/Post/PostQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const TOGGLE_LIKE = gql`
4 | mutation toggelLike($postId: String!) {
5 | toggleLike(postId: $postId)
6 | }
7 | `;
8 |
9 | export const ADD_COMMENT = gql`
10 | mutation addComment($postId: String!, $text: String!) {
11 | addComment(postId: $postId, text: $text) {
12 | id
13 | text
14 | user {
15 | username
16 | }
17 | }
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/src/Components/Post/index.js:
--------------------------------------------------------------------------------
1 | import PostContainer from "./PostContainer";
2 | export default PostContainer;
3 |
--------------------------------------------------------------------------------
/src/Components/Routes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Route, Switch, Redirect } from "react-router-dom";
4 | import Auth from "../Routes/Auth";
5 | import Feed from "../Routes/Feed";
6 | import Explore from "../Routes/Explore";
7 | import Search from "../Routes/Search";
8 | import Profile from "../Routes/Profile";
9 |
10 | const LoggedInRoutes = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | const LoggedOutRoutes = () => (
21 |
22 |
23 |
24 |
25 | );
26 |
27 | const AppRouter = ({ isLoggedIn }) =>
28 | isLoggedIn ? : ;
29 |
30 | AppRouter.propTypes = {
31 | isLoggedIn: PropTypes.bool.isRequired
32 | };
33 |
34 | export default AppRouter;
35 |
--------------------------------------------------------------------------------
/src/Components/SquarePost.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import { HeartFull, CommentFull } from "./Icons";
5 |
6 | const Overlay = styled.div`
7 | background-color: rgba(0, 0, 0, 0.6);
8 | width: 100%;
9 | height: 100%;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | opacity: 0;
14 | transition: opacity 0.3s linear;
15 | svg {
16 | fill: white;
17 | }
18 | `;
19 |
20 | const Container = styled.div`
21 | background-image: url(${props => props.bg});
22 | background-size: cover;
23 | cursor: pointer;
24 | &:hover {
25 | ${Overlay} {
26 | opacity: 1;
27 | }
28 | }
29 | `;
30 |
31 | const Number = styled.div`
32 | color: white;
33 | display: flex;
34 | align-items: center;
35 | &:first-child {
36 | margin-right: 30px;
37 | }
38 | `;
39 |
40 | const NumberText = styled.span`
41 | margin-left: 10px;
42 | font-size: 16px;
43 | `;
44 |
45 | const SquarePost = ({ likeCount, commentCount, file }) => (
46 |
47 |
48 |
49 |
50 | {likeCount}
51 |
52 |
53 |
54 | {commentCount}
55 |
56 |
57 |
58 | );
59 |
60 | SquarePost.propTypes = {
61 | likeCount: PropTypes.number.isRequired,
62 | commentCount: PropTypes.number.isRequired,
63 | file: PropTypes.object.isRequired
64 | };
65 |
66 | export default SquarePost;
67 |
--------------------------------------------------------------------------------
/src/Components/UserCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import Avatar from "./Avatar";
5 | import FatText from "./FatText";
6 | import { Link } from "react-router-dom";
7 | import FollowButton from "./FollowButton";
8 |
9 | const Card = styled.div`
10 | ${props => props.theme.whiteBox}
11 | display:flex;
12 | flex-direction: column;
13 | align-items: center;
14 | padding: 20px;
15 | `;
16 |
17 | const EAvatar = styled(Avatar)`
18 | margin-bottom: 15px;
19 | `;
20 |
21 | const ELink = styled(Link)`
22 | color: inherit;
23 | margin-bottom: 10px;
24 | `;
25 |
26 | const UserCard = ({ id, username, isFollowing, url, isSelf }) => (
27 |
28 |
29 |
30 |
31 |
32 | {!isSelf && }
33 |
34 | );
35 |
36 | UserCard.propTypes = {
37 | id: PropTypes.string.isRequired,
38 | username: PropTypes.string.isRequired,
39 | isFollowing: PropTypes.bool.isRequired,
40 | url: PropTypes.string.isRequired,
41 | isSelf: PropTypes.bool.isRequired
42 | };
43 |
44 | export default UserCard;
45 |
--------------------------------------------------------------------------------
/src/Hooks/useInput.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default defaultValue => {
4 | const [value, setValue] = useState(defaultValue);
5 |
6 | const onChange = e => {
7 | const {
8 | target: { value }
9 | } = e;
10 | setValue(value);
11 | };
12 |
13 | return { value, onChange, setValue };
14 | };
15 |
--------------------------------------------------------------------------------
/src/Routes/Auth/AuthContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import AuthPresenter from "./AuthPresenter";
3 | import useInput from "../../Hooks/useInput";
4 | import { useMutation } from "react-apollo-hooks";
5 | import {
6 | LOG_IN,
7 | CREATE_ACCOUNT,
8 | CONFIRM_SECRET,
9 | LOCAL_LOG_IN
10 | } from "./AuthQueries";
11 | import { toast } from "react-toastify";
12 |
13 | export default () => {
14 | const [action, setAction] = useState("logIn");
15 | const username = useInput("");
16 | const firstName = useInput("");
17 | const lastName = useInput("");
18 | const secret = useInput("");
19 | const email = useInput("");
20 | const requestSecretMutation = useMutation(LOG_IN, {
21 | variables: { email: email.value }
22 | });
23 | const createAccountMutation = useMutation(CREATE_ACCOUNT, {
24 | variables: {
25 | email: email.value,
26 | username: username.value,
27 | firstName: firstName.value,
28 | lastName: lastName.value
29 | }
30 | });
31 | const confirmSecretMutation = useMutation(CONFIRM_SECRET, {
32 | variables: {
33 | email: email.value,
34 | secret: secret.value
35 | }
36 | });
37 | const localLogInMutation = useMutation(LOCAL_LOG_IN);
38 |
39 | const onSubmit = async e => {
40 | e.preventDefault();
41 | if (action === "logIn") {
42 | if (email.value !== "") {
43 | try {
44 | const {
45 | data: { requestSecret }
46 | } = await requestSecretMutation();
47 | if (!requestSecret) {
48 | toast.error("You dont have an account yet, create one");
49 | setTimeout(() => setAction("signUp"), 3000);
50 | } else {
51 | toast.success("Check your inbox for your login secret");
52 | setAction("confirm");
53 | }
54 | } catch {
55 | toast.error("Can't request secret, try again");
56 | }
57 | } else {
58 | toast.error("Email is required");
59 | }
60 | } else if (action === "signUp") {
61 | if (
62 | email.value !== "" &&
63 | username.value !== "" &&
64 | firstName.value !== "" &&
65 | lastName.value !== ""
66 | ) {
67 | try {
68 | const {
69 | data: { createAccount }
70 | } = await createAccountMutation();
71 | if (!createAccount) {
72 | toast.error("Can't create account");
73 | } else {
74 | toast.success("Account created! Log In now");
75 | setTimeout(() => setAction("logIn"), 3000);
76 | }
77 | } catch (e) {
78 | toast.error(e.message);
79 | }
80 | } else {
81 | toast.error("All field are required");
82 | }
83 | } else if (action === "confirm") {
84 | if (secret.value !== "") {
85 | try {
86 | const {
87 | data: { confirmSecret: token }
88 | } = await confirmSecretMutation();
89 | if (token !== "" && token !== undefined) {
90 | localLogInMutation({ variables: { token } });
91 | } else {
92 | throw Error();
93 | }
94 | } catch {
95 | toast.error("Cant confirm secret,check again");
96 | }
97 | }
98 | }
99 | };
100 |
101 | return (
102 |
112 | );
113 | };
114 |
--------------------------------------------------------------------------------
/src/Routes/Auth/AuthPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet";
3 | import styled from "styled-components";
4 | import Input from "../../Components/Input";
5 | import Button from "../../Components/Button";
6 |
7 | const Wrapper = styled.div`
8 | min-height: 80vh;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | flex-direction: column;
13 | `;
14 |
15 | const Box = styled.div`
16 | ${props => props.theme.whiteBox}
17 | border-radius:0px;
18 | width: 100%;
19 | max-width: 350px;
20 | `;
21 |
22 | const StateChanger = styled(Box)`
23 | text-align: center;
24 | padding: 20px 0px;
25 | `;
26 |
27 | const Link = styled.span`
28 | color: ${props => props.theme.blueColor};
29 | cursor: pointer;
30 | `;
31 |
32 | const Form = styled(Box)`
33 | padding: 40px;
34 | padding-bottom: 30px;
35 | margin-bottom: 15px;
36 | form {
37 | width: 100%;
38 | input {
39 | width: 100%;
40 | &:not(:last-child) {
41 | margin-bottom: 7px;
42 | }
43 | }
44 | button {
45 | margin-top: 10px;
46 | }
47 | }
48 | `;
49 |
50 | export default ({
51 | action,
52 | username,
53 | firstName,
54 | lastName,
55 | email,
56 | setAction,
57 | onSubmit,
58 | secret
59 | }) => (
60 |
61 |
71 | >
72 | )}
73 | {action === "signUp" && (
74 | <>
75 |
76 | Sign Up | Prismagram
77 |
78 |
85 | >
86 | )}
87 | {action === "confirm" && (
88 | <>
89 |
90 | Confirm Secret | Prismagram
91 |
92 |
96 | >
97 | )}
98 |
99 |
100 | {action !== "confirm" && (
101 |
102 | {action === "logIn" ? (
103 | <>
104 | Don't have an account?{" "}
105 | setAction("signUp")}>Sign up
106 | >
107 | ) : (
108 | <>
109 | Have an account?{" "}
110 | setAction("logIn")}>Log in
111 | >
112 | )}
113 |
114 | )}
115 |
116 | );
117 |
--------------------------------------------------------------------------------
/src/Routes/Auth/AuthQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const LOG_IN = gql`
4 | mutation requestSecret($email: String!) {
5 | requestSecret(email: $email)
6 | }
7 | `;
8 |
9 | export const CREATE_ACCOUNT = gql`
10 | mutation createAccount(
11 | $username: String!
12 | $email: String!
13 | $firstName: String
14 | $lastName: String
15 | ) {
16 | createAccount(
17 | username: $username
18 | email: $email
19 | firstName: $firstName
20 | lastName: $lastName
21 | )
22 | }
23 | `;
24 |
25 | export const CONFIRM_SECRET = gql`
26 | mutation confirmSecret($secret: String!, $email: String!) {
27 | confirmSecret(secret: $secret, email: $email)
28 | }
29 | `;
30 |
31 | export const LOCAL_LOG_IN = gql`
32 | mutation logUserIn($token: String!) {
33 | logUserIn(token: $token) @client
34 | }
35 | `;
36 |
--------------------------------------------------------------------------------
/src/Routes/Auth/index.js:
--------------------------------------------------------------------------------
1 | import AuthContainer from "./AuthContainer";
2 | export default AuthContainer;
3 |
--------------------------------------------------------------------------------
/src/Routes/EditProfile.js:
--------------------------------------------------------------------------------
1 | export default () => "Edit profile";
2 |
--------------------------------------------------------------------------------
/src/Routes/Explore.js:
--------------------------------------------------------------------------------
1 | export default () => "Explore";
2 |
--------------------------------------------------------------------------------
/src/Routes/Feed.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "rl-react-helmet";
3 | import styled from "styled-components";
4 | import { gql } from "apollo-boost";
5 | import { useQuery } from "react-apollo-hooks";
6 | import Loader from "../Components/Loader";
7 | import Post from "../Components/Post";
8 |
9 | const FEED_QUERY = gql`
10 | {
11 | seeFeed {
12 | id
13 | location
14 | caption
15 | user {
16 | id
17 | avatar
18 | username
19 | }
20 | files {
21 | id
22 | url
23 | }
24 | likeCount
25 | isLiked
26 | comments {
27 | id
28 | text
29 | user {
30 | id
31 | username
32 | }
33 | }
34 | createdAt
35 | }
36 | }
37 | `;
38 |
39 | const Wrapper = styled.div`
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | min-height: 80vh;
44 | `;
45 |
46 | export default () => {
47 | const { data, loading } = useQuery(FEED_QUERY);
48 | return (
49 |
50 |
51 | Feed | Prismagram
52 |
53 | {loading && }
54 | {!loading &&
55 | data &&
56 | data.seeFeed &&
57 | data.seeFeed.map(post => (
58 |
70 | ))}
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/Routes/Post.js:
--------------------------------------------------------------------------------
1 | export default () => "Auth";
2 | export default () => "Post";
3 |
--------------------------------------------------------------------------------
/src/Routes/Profile/ProfileContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { gql } from "apollo-boost";
3 | import withRouter from "react-router-dom/withRouter";
4 | import { useQuery, useMutation } from "react-apollo-hooks";
5 | import ProfilePresenter from "./ProfilePresenter";
6 |
7 | const GET_USER = gql`
8 | query seeUser($username: String!) {
9 | seeUser(username: $username) {
10 | id
11 | avatar
12 | username
13 | fullName
14 | isFollowing
15 | isSelf
16 | bio
17 | followingCount
18 | followersCount
19 | postsCount
20 | posts {
21 | id
22 | files {
23 | url
24 | }
25 | likeCount
26 | commentCount
27 | }
28 | }
29 | }
30 | `;
31 |
32 | export const LOG_OUT = gql`
33 | mutation logUserOut {
34 | logUserOut @client
35 | }
36 | `;
37 |
38 | export default withRouter(({ match: { params: { username } } }) => {
39 | const { data, loading } = useQuery(GET_USER, { variables: { username } });
40 | const logOut = useMutation(LOG_OUT);
41 | return ;
42 | });
43 |
--------------------------------------------------------------------------------
/src/Routes/Profile/ProfilePresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Helmet } from "rl-react-helmet";
4 | import Loader from "../../Components/Loader";
5 | import Avatar from "../../Components/Avatar";
6 | import FatText from "../../Components/FatText";
7 | import FollowButton from "../../Components/FollowButton";
8 | import SquarePost from "../../Components/SquarePost";
9 | import Button from "../../Components/Button";
10 |
11 | const Wrapper = styled.div`
12 | min-height: 100vh;
13 | `;
14 |
15 | const Header = styled.header`
16 | display: flex;
17 | align-items: center;
18 | justify-content: space-around;
19 | width: 80%;
20 | margin: 0 auto;
21 | margin-bottom: 40px;
22 | `;
23 |
24 | const HeaderColumn = styled.div``;
25 |
26 | const UsernameRow = styled.div`
27 | display: flex;
28 | align-items: center;
29 | `;
30 |
31 | const Username = styled.span`
32 | font-size: 26px;
33 | display: block;
34 | `;
35 |
36 | const Counts = styled.ul`
37 | display: flex;
38 | margin: 15px 0px;
39 | `;
40 |
41 | const Count = styled.li`
42 | font-size: 16px;
43 | &:not(:last-child) {
44 | margin-right: 10px;
45 | }
46 | `;
47 |
48 | const FullName = styled(FatText)`
49 | font-size: 16px;
50 | `;
51 |
52 | const Bio = styled.p`
53 | margin: 10px 0px;
54 | `;
55 |
56 | const Posts = styled.div`
57 | display: grid;
58 | grid-template-columns: repeat(4, 200px);
59 | grid-template-rows: 200px;
60 | grid-auto-rows: 200px;
61 | `;
62 |
63 | export default ({ loading, data, logOut }) => {
64 | if (loading === true) {
65 | return (
66 |
67 |
68 |
69 | );
70 | } else if (!loading && data && data.seeUser) {
71 | const {
72 | seeUser: {
73 | id,
74 | avatar,
75 | username,
76 | fullName,
77 | isFollowing,
78 | isSelf,
79 | bio,
80 | followingCount,
81 | followersCount,
82 | postsCount,
83 | posts
84 | }
85 | } = data;
86 | return (
87 |
88 |
89 | {username} | Prismagram
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {username}{" "}
98 | {isSelf ? (
99 |
100 | ) : (
101 |
102 | )}
103 |
104 |
105 |
106 | posts
107 |
108 |
109 | followers
110 |
111 |
112 | following
113 |
114 |
115 |
116 | {bio}
117 |
118 |
119 |
120 | {posts &&
121 | posts.map(post => (
122 |
128 | ))}
129 |
130 |
131 | );
132 | }
133 | return null;
134 | };
135 |
--------------------------------------------------------------------------------
/src/Routes/Profile/index.js:
--------------------------------------------------------------------------------
1 | import ProfileContainer from "./ProfileContainer";
2 | export default ProfileContainer;
3 |
--------------------------------------------------------------------------------
/src/Routes/Search/SearchContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "react-router-dom";
3 | import SearchPresenter from "./SearchPresenter";
4 | import { useQuery } from "react-apollo-hooks";
5 | import { SEARCH } from "./SearchQueries";
6 |
7 | export default withRouter(({ location: { search } }) => {
8 | const term = search.split("=")[1];
9 | const { data, loading } = useQuery(SEARCH, {
10 | skip: term === undefined,
11 | variables: {
12 | term
13 | }
14 | });
15 |
16 | return ;
17 | });
18 |
--------------------------------------------------------------------------------
/src/Routes/Search/SearchPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import FatText from "../../Components/FatText";
5 | import Loader from "../../Components/Loader";
6 | import UserCard from "../../Components/UserCard";
7 | import SquarePost from "../../Components/SquarePost";
8 |
9 | const Wrapper = styled.div`
10 | height: 50vh;
11 | `;
12 |
13 | const Section = styled.div`
14 | margin-bottom: 50px;
15 | display: grid;
16 | grid-gap: 25px;
17 | grid-template-columns: repeat(4, 160px);
18 | grid-template-rows: 160px;
19 | grid-auto-rows: 160px;
20 | `;
21 |
22 | const PostSection = styled(Section)`
23 | grid-template-columns: repeat(4, 200px);
24 | grid-template-rows: 200px;
25 | grid-auto-rows: 200px;
26 | `;
27 |
28 | const SearchPresenter = ({ searchTerm, loading, data }) => {
29 | if (searchTerm === undefined) {
30 | return (
31 |
32 |
33 |
34 | );
35 | } else if (loading === true) {
36 | return (
37 |
38 |
39 |
40 | );
41 | } else if (data && data.searchUser && data.searchPost) {
42 | return (
43 |
44 |
45 | {data.searchUser.length === 0 ? (
46 |
47 | ) : (
48 | data.searchUser.map(user => (
49 |
57 | ))
58 | )}
59 |
60 |
61 | {data.searchPost.length === 0 ? (
62 |
63 | ) : (
64 | data.searchPost.map(post => (
65 |
71 | ))
72 | )}
73 |
74 |
75 | );
76 | }
77 | };
78 |
79 | SearchPresenter.propTypes = {
80 | searchTerm: PropTypes.string,
81 | loading: PropTypes.bool
82 | };
83 |
84 | export default SearchPresenter;
85 |
--------------------------------------------------------------------------------
/src/Routes/Search/SearchQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const SEARCH = gql`
4 | query search($term: String!) {
5 | searchPost(term: $term) {
6 | id
7 | files {
8 | url
9 | }
10 | likeCount
11 | commentCount
12 | }
13 | searchUser(term: $term) {
14 | id
15 | avatar
16 | username
17 | isFollowing
18 | isSelf
19 | }
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/Routes/Search/index.js:
--------------------------------------------------------------------------------
1 | import SearchContainer from "./SearchContainer";
2 | export default SearchContainer;
3 |
--------------------------------------------------------------------------------
/src/SharedQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const ME = gql`
4 | {
5 | me {
6 | username
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/src/Styles/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import reset from "styled-reset";
3 |
4 | export default createGlobalStyle`
5 | ${reset};
6 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700');
7 | * {
8 | box-sizing:border-box;
9 | }
10 | body {
11 | background-color:${props => props.theme.bgColor};
12 | color:${props => props.theme.blackColor};
13 | font-size:14px;
14 | font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
15 | padding-top: 140px;
16 | }
17 | a {
18 | color:${props => props.theme.blueColor};
19 | text-decoration:none;
20 | }
21 | input:focus{
22 | outline:none;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/src/Styles/Theme.js:
--------------------------------------------------------------------------------
1 | const BOX_BORDER = "1px solid #e6e6e6";
2 | const BORDER_RADIUS = "4px";
3 |
4 | export default {
5 | maxWidth: "935px",
6 | bgColor: "#FAFAFA",
7 | blackColor: "#262626",
8 | darkGreyColor: "#999",
9 | lightGreyColor: "#c7c7c7",
10 | redColor: "#ED4956",
11 | blueColor: "#3897f0",
12 | darkBlueColor: "#003569",
13 | boxBorder: "1px solid #e6e6e6",
14 | borderRadius: "4px",
15 | whiteBox: `border:${BOX_BORDER};
16 | border-radius:${BORDER_RADIUS};
17 | background-color:white;
18 | `
19 | };
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./Components/App";
4 | import Client from "./Apollo/Client";
5 | import { ApolloProvider } from "react-apollo-hooks";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------