38 | {map(getPublicationsFolloweds, (publication, index) => (
39 |
40 |
41 |
42 |
46 | {publication.idUser.name}
47 |
48 |
49 |
openPublication(publication)}
53 | />
54 |
57 |
58 |
59 |
60 |
61 | ))}
62 |
63 | {showModal && (
64 |
69 | )}
70 | >
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/Home/Feed/Feed.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .feed {
4 | padding-top: 50px;
5 | height: calc(100vh - 48px);
6 | overflow-y: scroll;
7 | &::-webkit-scrollbar {
8 | display: none;
9 | }
10 |
11 | &__box {
12 | margin-bottom: 50px;
13 | border: 1px solid $border-grey;
14 |
15 | &-user {
16 | background-color: $background-light;
17 | padding: 15px !important;
18 | color: $font-dark;
19 |
20 | img {
21 | margin-right: 15px !important;
22 | }
23 |
24 | span {
25 | font-weight: bold;
26 | }
27 | }
28 |
29 | &-photo {
30 | height: 600px;
31 | background-position: center;
32 | background-repeat: no-repeat;
33 | background-size: cover;
34 |
35 | &:hover {
36 | cursor: pointer;
37 | opacity: 0.95;
38 | }
39 | }
40 |
41 | &-actions {
42 | padding: 5px 5px;
43 |
44 | i {
45 | margin-right: 10px !important;
46 | }
47 | }
48 |
49 | &-form {
50 | > form {
51 | position: relative !important;
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Home/Feed/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Feed";
2 |
--------------------------------------------------------------------------------
/src/components/Home/UsersNotFolloweds/UsersNotFolloweds.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Image } from "semantic-ui-react";
3 | import { Link } from "react-router-dom";
4 | import { map } from "lodash";
5 | import { useQuery } from "@apollo/client";
6 | import { GET_NOT_FOLLOWEDS } from "../../../gql/follow";
7 | import ImageNotFound from "../../../assets/png/avatar.png";
8 | import "./UsersNotFolloweds.scss";
9 |
10 | export default function UsersNotFolloweds() {
11 | const { data, loading } = useQuery(GET_NOT_FOLLOWEDS);
12 |
13 | if (loading) return null;
14 | const { getNotFolloweds } = data;
15 |
16 | return (
17 |
18 |
Usuarios que no sigues
19 | {map(getNotFolloweds, (user, index) => (
20 |
25 |
26 | {user.name}
27 |
28 | ))}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Home/UsersNotFolloweds/UsersNotFolloweds.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .users-not-followeds {
4 | display: flex;
5 | flex-direction: column;
6 | margin-top: 35px;
7 |
8 | &__user {
9 | margin-top: 20px;
10 | color: $font-dark;
11 |
12 | img {
13 | margin-right: 15px !important;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Home/UsersNotFolloweds/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UsersNotFolloweds";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalBasic/ModalBasic.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal } from "semantic-ui-react";
3 | import "./ModalBasic.scss";
4 |
5 | export default function ModalBasic(props) {
6 | const { show, setShow, title, children } = props;
7 |
8 | const onClose = () => {
9 | setShow(false);
10 | };
11 |
12 | return (
13 |
14 | {title && {title}}
15 | {children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalBasic/ModalBasic.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .modal-basic {
4 | border-radius: 15px !important;
5 | overflow: hidden;
6 |
7 | .header {
8 | text-align: center;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalBasic/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ModalBasic";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Actions/Actions.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Icon } from "semantic-ui-react";
3 | import { useMutation, useQuery } from "@apollo/client";
4 | import {
5 | ADD_LIKE,
6 | IS_LIKE,
7 | DELETE_LIKE,
8 | COUNT_LIKE,
9 | } from "../../../../gql/like";
10 | import "./Actions.scss";
11 |
12 | export default function Actions(props) {
13 | const { publication } = props;
14 | const [loadingAction, setLoadingAction] = useState(false);
15 | const [addLike] = useMutation(ADD_LIKE);
16 | const [deleteLike] = useMutation(DELETE_LIKE);
17 | const { data, loading, refetch } = useQuery(IS_LIKE, {
18 | variables: { idPublication: publication.id },
19 | });
20 | const {
21 | data: dataCount,
22 | loading: loadingCount,
23 | refetch: refetchCount,
24 | } = useQuery(COUNT_LIKE, {
25 | variables: { idPublication: publication.id },
26 | });
27 |
28 | const onAddLike = async () => {
29 | setLoadingAction(true);
30 | try {
31 | await addLike({
32 | variables: {
33 | idPublication: publication.id,
34 | },
35 | });
36 | refetch();
37 | refetchCount();
38 | } catch (error) {
39 | console.log(error);
40 | }
41 | setLoadingAction(false);
42 | };
43 |
44 | const onDeleteLike = async () => {
45 | setLoadingAction(true);
46 | try {
47 | await deleteLike({
48 | variables: {
49 | idPublication: publication.id,
50 | },
51 | });
52 | refetch();
53 | refetchCount();
54 | } catch (error) {
55 | console.log(error);
56 | }
57 | setLoadingAction(false);
58 | };
59 |
60 | const onAction = () => {
61 | if (!loadingAction) {
62 | if (isLike) {
63 | onDeleteLike();
64 | } else {
65 | onAddLike();
66 | }
67 | }
68 | };
69 |
70 | if (loading || loadingCount) return null;
71 | const { isLike } = data;
72 | const { countLikes } = dataCount;
73 |
74 | return (
75 |
76 |
81 | {countLikes} {countLikes === 1 ? "Like" : "Likes"}
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Actions/Actions.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../scss/index.scss";
2 |
3 | .actions {
4 | height: 35px;
5 | display: flex;
6 | align-items: center;
7 | padding: 0 10px;
8 |
9 | i {
10 | font-size: 16px !important;
11 |
12 | &:hover {
13 | cursor: pointer;
14 | }
15 | }
16 |
17 | .like.active {
18 | color: $danger;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Actions/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Actions";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/CommentForm/CommentForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, Button } from "semantic-ui-react";
3 | import { useFormik } from "formik";
4 | import * as Yup from "yup";
5 | import { useMutation } from "@apollo/client";
6 | import { ADD_COMMENT } from "../../../../gql/comment";
7 | import "./CommentForm.scss";
8 |
9 | export default function CommentForm(props) {
10 | const { publication } = props;
11 | const [addComment] = useMutation(ADD_COMMENT);
12 |
13 | const formik = useFormik({
14 | initialValues: {
15 | comment: "",
16 | },
17 | validationSchema: Yup.object({
18 | comment: Yup.string().required(),
19 | }),
20 | onSubmit: async (formData) => {
21 | try {
22 | await addComment({
23 | variables: {
24 | input: {
25 | idPublication: publication.id,
26 | comment: formData.comment,
27 | },
28 | },
29 | });
30 | formik.handleReset();
31 | } catch (error) {
32 | console.log(error);
33 | }
34 | },
35 | });
36 |
37 | return (
38 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/CommentForm/CommentForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../scss/index.scss";
2 |
3 | .comment-form {
4 | position: absolute !important;
5 | bottom: 0;
6 | display: flex;
7 | width: 100%;
8 |
9 | .field {
10 | margin: 0 !important;
11 | width: 100%;
12 |
13 | .ui.input {
14 | height: 60px;
15 | input {
16 | border: 0;
17 | border-top: 1px solid $border-grey;
18 | border-radius: 0;
19 | padding-right: 80px;
20 | }
21 | }
22 | }
23 |
24 | .ui.button {
25 | background-color: transparent;
26 | margin: 0;
27 | position: absolute;
28 | right: 0;
29 | height: 100%;
30 |
31 | &:hover {
32 | background-color: transparent;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/CommentForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CommentForm";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Comments/Comments.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Image } from "semantic-ui-react";
3 | import { map } from "lodash";
4 | import { Link } from "react-router-dom";
5 | import { useQuery } from "@apollo/client";
6 | import { GET_COMMENTS } from "../../../../gql/comment";
7 | import ImageNoFound from "../../../../assets/png/avatar.png";
8 | import "./Comments.scss";
9 |
10 | export default function Comments(props) {
11 | const { publication } = props;
12 | const { data, loading, startPolling, stopPolling } = useQuery(GET_COMMENTS, {
13 | variables: {
14 | idPublication: publication.id,
15 | },
16 | });
17 |
18 | useEffect(() => {
19 | startPolling(1000);
20 | return () => {
21 | stopPolling();
22 | };
23 | }, [startPolling, stopPolling]);
24 |
25 | if (loading) return null;
26 | const { getComments } = data;
27 |
28 | return (
29 |
30 | {map(getComments, (comment, index) => (
31 |
36 |
37 |
38 |
{comment.idUser.username}
39 |
{comment.comment}
40 |
41 |
42 | ))}
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Comments/Comments.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../scss/index.scss";
2 |
3 | .comments {
4 | height: calc(100% - 95px);
5 | overflow-y: scroll;
6 |
7 | .comment {
8 | display: flex;
9 | align-items: center;
10 | padding: 10px;
11 | border-bottom: 1px solid $border-grey;
12 | color: $font-dark;
13 |
14 | img {
15 | margin-right: 15px !important;
16 | }
17 |
18 | p {
19 | margin: 0;
20 |
21 | &:first-of-type {
22 | font-weight: bold;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/Comments/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Comments";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/ModalPublication.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal, Grid } from "semantic-ui-react";
3 | import Comments from "./Comments";
4 | import Actions from "./Actions";
5 | import CommentForm from "./CommentForm";
6 | import "./ModalPublication.scss";
7 |
8 | export default function ModalPublication(props) {
9 | const { show, setShow, publication } = props;
10 |
11 | const onClose = () => setShow(false);
12 |
13 | return (
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/ModalPublication.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .modal-publication {
4 | height: 520px !important;
5 |
6 | .ui.grid {
7 | margin: 0;
8 | height: 100%;
9 | }
10 |
11 | &__left {
12 | background-position: center;
13 | background-size: cover;
14 | background-repeat: no-repeat;
15 | }
16 |
17 | &__right {
18 | padding: 0 !important;
19 | height: 100%;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalPublication/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ModalPublication";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalUpload/ModalUpload.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { Modal, Icon, Button, Dimmer, Loader } from "semantic-ui-react";
3 | import { toast } from "react-toastify";
4 | import { useDropzone } from "react-dropzone";
5 | import { useMutation } from "@apollo/client";
6 | import { PUBLISH } from "../../../gql/publication";
7 | import "./ModalUpload.scss";
8 |
9 | export default function ModalUpload(props) {
10 | const { show, setShow } = props;
11 | const [fileUpload, setFileUpload] = useState(null);
12 | const [isLoading, setIsLoading] = useState(false);
13 | const [publish] = useMutation(PUBLISH);
14 |
15 | const onDrop = useCallback((acceptedFile) => {
16 | const file = acceptedFile[0];
17 | setFileUpload({
18 | type: "image",
19 | file,
20 | preview: URL.createObjectURL(file),
21 | });
22 | });
23 |
24 | const { getRootProps, getInputProps } = useDropzone({
25 | accept: "image/jpeg, image/png",
26 | noKeyboard: true,
27 | multiple: false,
28 | onDrop,
29 | });
30 |
31 | const onClose = () => {
32 | setIsLoading(false);
33 | setFileUpload(null);
34 | setShow(false);
35 | };
36 |
37 | const onPublish = async () => {
38 | try {
39 | setIsLoading(true);
40 | const result = await publish({
41 | variables: {
42 | file: fileUpload.file,
43 | },
44 | });
45 | const { data } = result;
46 |
47 | if (!data.publish.status) {
48 | toast.warning("Error en la publicación");
49 | isLoading(false);
50 | } else {
51 | onClose();
52 | }
53 | } catch (error) {
54 | console.log(error);
55 | }
56 | };
57 |
58 | return (
59 |
60 |
65 | {!fileUpload && (
66 | <>
67 |
68 |
Arrastra tu foto que quieras publicar
69 | >
70 | )}
71 |
72 |
73 |
74 | {fileUpload?.type === "image" && (
75 |
79 | )}
80 |
81 | {fileUpload && (
82 |
85 | )}
86 |
87 | {isLoading && (
88 |
89 |
90 | Publicando...
91 |
92 | )}
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalUpload/ModalUpload.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .modal-upload {
4 | border-radius: 15px !important;
5 | overflow: hidden;
6 | padding: 8px;
7 | width: 650px;
8 | height: 650px;
9 |
10 | .dropzone {
11 | height: 100%;
12 | width: 100%;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | flex-direction: column;
17 | border: 2px dashed $border-dark;
18 | border-radius: 15px !important;
19 | position: relative;
20 | z-index: 1;
21 |
22 | i {
23 | font-size: 50px;
24 | color: $font-grey;
25 | }
26 | p {
27 | margin-block-start: 20px;
28 | }
29 | }
30 |
31 | .image {
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | width: 100%;
36 | height: 100%;
37 | background-repeat: no-repeat;
38 | background-position: center;
39 | background-size: cover;
40 | }
41 |
42 | .btn-upload {
43 | position: absolute;
44 | bottom: 0;
45 | left: 0;
46 | width: 100%;
47 | height: 45px;
48 | border: 0 !important;
49 | border-radius: 0 !important;
50 | margin: 0 !important;
51 | z-index: 2;
52 | }
53 |
54 | .publishing {
55 | p {
56 | margin-top: 80px;
57 | font-size: 18px;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalUpload/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ModalUpload";
2 |
--------------------------------------------------------------------------------
/src/components/Publications/PreviewPublication/PreviewPublication.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Image } from "semantic-ui-react";
3 | import ModalPublication from "../../Modal/ModalPublication";
4 | import "./PreviewPublication.scss";
5 |
6 | export default function PreviewPublication(props) {
7 | const { publication } = props;
8 | const [showModal, setShowModal] = useState(false);
9 |
10 | return (
11 | <>
12 |
setShowModal(true)}>
13 |
14 |
15 |
16 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Publications/PreviewPublication/PreviewPublication.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .preview-publication {
4 | height: 100%;
5 |
6 | &:hover {
7 | cursor: pointer;
8 | opacity: 0.9;
9 | }
10 |
11 | &__image {
12 | width: 100%;
13 | height: 100%;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Publications/PreviewPublication/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PreviewPublication";
2 |
--------------------------------------------------------------------------------
/src/components/Publications/Publications.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Grid } from "semantic-ui-react";
3 | import { map } from "lodash";
4 | import PreviewPublication from "./PreviewPublication";
5 | import "./Publications.scss";
6 |
7 | export default function Publications(props) {
8 | const { getPublications } = props;
9 |
10 | return (
11 |
12 |
Publicaciones
13 |
14 | {map(getPublications, (publication, index) => (
15 |
16 |
17 |
18 | ))}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Publications/Publications.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .publications {
4 | margin-top: 50px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Publications/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Publications";
2 |
--------------------------------------------------------------------------------
/src/components/User/AvatarForm/AvatarForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { Button } from "semantic-ui-react";
3 | import { useDropzone } from "react-dropzone";
4 | import { toast } from "react-toastify";
5 | import { useMutation } from "@apollo/client";
6 | import { UPDATE_AVATAR, GET_USER, DELETE_AVATAR } from "../../../gql/user";
7 | import "./AvatarForm.scss";
8 | import AuthContext from "../../../context/AuthContext";
9 |
10 | export default function AvatarForm(props) {
11 | const { setShowModal, auth } = props;
12 | const [loading, setLoading] = useState(false);
13 |
14 | const [updateAvatar] = useMutation(UPDATE_AVATAR, {
15 | update(cache, { data: { updateAvatar } }) {
16 | const { getUser } = cache.readQuery({
17 | query: GET_USER,
18 | variables: { username: auth.username },
19 | });
20 |
21 | cache.writeQuery({
22 | query: GET_USER,
23 | variables: { username: auth.username },
24 | data: {
25 | getUser: { ...getUser, avatar: updateAvatar.urlAvatar },
26 | },
27 | });
28 | },
29 | });
30 |
31 | const [deleteAvatar] = useMutation(DELETE_AVATAR, {
32 | update(cache) {
33 | const { getUser } = cache.readQuery({
34 | query: GET_USER,
35 | variables: { username: auth.username },
36 | });
37 |
38 | cache.writeQuery({
39 | query: GET_USER,
40 | variables: { username: auth.username },
41 | data: {
42 | getUser: { ...getUser, avatar: "" },
43 | },
44 | });
45 | },
46 | });
47 |
48 | const onDrop = useCallback(async (acceptedFile) => {
49 | const file = acceptedFile[0];
50 |
51 | try {
52 | setLoading(true);
53 | const result = await updateAvatar({ variables: { file } });
54 | const { data } = result;
55 |
56 | if (!data.updateAvatar.status) {
57 | toast.warning("Error al actualizar el avatar");
58 | setLoading(false);
59 | } else {
60 | setLoading(false);
61 | setShowModal(false);
62 | }
63 | } catch (error) {
64 | console.log(error);
65 | }
66 | }, []);
67 |
68 | const { getRootProps, getInputProps } = useDropzone({
69 | accept: "image/jpeg, image/png",
70 | noKeyboard: true,
71 | multiple: false,
72 | onDrop,
73 | });
74 |
75 | const onDeleteAvatar = async () => {
76 | try {
77 | const result = await deleteAvatar();
78 | const { data } = result;
79 |
80 | if (!data.deleteAvatar) {
81 | toast.warning("Error al borrar el avatar");
82 | } else {
83 | setShowModal(false);
84 | }
85 | } catch (error) {
86 | console.log(error);
87 | }
88 | };
89 |
90 | return (
91 |
92 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/User/AvatarForm/AvatarForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .avatar-form {
4 | display: flex;
5 | flex-direction: column;
6 |
7 | .ui.button {
8 | background-color: $background-light;
9 | border-bottom: 1px solid $border-grey;
10 | padding: 18px 0;
11 | margin: 0;
12 | border-radius: 0;
13 |
14 | &:nth-child(1) {
15 | color: $action;
16 | }
17 | &:nth-child(2) {
18 | color: $danger;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/User/AvatarForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./AvatarForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/DescriptionForm/DescriptionForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, TextArea, Button } from "semantic-ui-react";
3 | import { toast } from "react-toastify";
4 | import { useFormik } from "formik";
5 | import * as Yup from "yup";
6 | import { useMutation } from "@apollo/client";
7 | import { UPDATE_USER } from "../../../gql/user";
8 | import "./DescriptionForm.scss";
9 |
10 | export default function DescriptionForm(props) {
11 | const { setShowModal, currentDescription, refetch } = props;
12 | const [updateUser] = useMutation(UPDATE_USER);
13 |
14 | const formik = useFormik({
15 | initialValues: {
16 | description: currentDescription || "",
17 | },
18 | validationSchema: Yup.object({
19 | description: Yup.string().required(),
20 | }),
21 | onSubmit: async (formData) => {
22 | try {
23 | await updateUser({
24 | variables: {
25 | input: formData,
26 | },
27 | });
28 | refetch();
29 | setShowModal(false);
30 | } catch (error) {
31 | toast.error("Error al actualziar tu biografía");
32 | }
33 | },
34 | });
35 |
36 | return (
37 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/User/DescriptionForm/DescriptionForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .description-form {
4 | margin: 20px;
5 |
6 | textarea {
7 | margin-bottom: 20px !important;
8 |
9 | &.error {
10 | border: 1px solid $danger !important;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/User/DescriptionForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./DescriptionForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/EmailForm/EmailForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, Button } from "semantic-ui-react";
3 | import { toast } from "react-toastify";
4 | import { useFormik } from "formik";
5 | import * as Yup from "yup";
6 | import { useMutation } from "@apollo/client";
7 | import { UPDATE_USER } from "../../../gql/user";
8 | import "./EmailForm.scss";
9 |
10 | export default function EmailForm(props) {
11 | const { setShowModal, currentEmail, refetch } = props;
12 | const [updateUser] = useMutation(UPDATE_USER);
13 |
14 | const formik = useFormik({
15 | initialValues: {
16 | email: currentEmail || "",
17 | },
18 | validationSchema: Yup.object({
19 | email: Yup.string().email().required(),
20 | }),
21 | onSubmit: async (formData) => {
22 | try {
23 | await updateUser({
24 | variables: {
25 | input: formData,
26 | },
27 | });
28 | refetch();
29 | setShowModal(false);
30 | } catch (error) {
31 | toast.error("Error al actualiar el email");
32 | }
33 | },
34 | });
35 |
36 | return (
37 |
45 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/User/EmailForm/EmailForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .email-form {
4 | margin: 20px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/User/EmailForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./EmailForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/ListUsers/ListUsers.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Image } from "semantic-ui-react";
3 | import { size, map } from "lodash";
4 | import { useHistory } from "react-router-dom";
5 | import ImageNoFound from "../../../assets/png/avatar.png";
6 | import "./ListUsers.scss";
7 |
8 | export default function ListUsers(props) {
9 | const { users, setShowModal } = props;
10 | const history = useHistory();
11 |
12 | const goToUser = (username) => {
13 | setShowModal(false);
14 | history.push(`/${username}`);
15 | };
16 |
17 | return (
18 |
19 | {size(users) === 0 ? (
20 |
No se han encontrado usuarios
21 | ) : (
22 | map(users, (user, index) => (
23 |
goToUser(user.username)}
27 | >
28 |
29 |
30 |
{user.name}
31 |
{user.username}
32 |
33 |
34 | ))
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/User/ListUsers/ListUsers.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .list-users {
4 | &__not-users {
5 | text-align: center;
6 | font-size: 17px;
7 | padding: 20px 0;
8 | }
9 |
10 | &__user {
11 | display: flex;
12 | align-items: center;
13 | margin: 10px 15px;
14 | &:hover {
15 | cursor: pointer;
16 | }
17 |
18 | img {
19 | margin-right: 15px !important;
20 | }
21 |
22 | p {
23 | margin: 0;
24 | font-size: 13px;
25 |
26 | &:first-of-type {
27 | color: #000;
28 | font-weight: bold;
29 | }
30 | &:last-of-type {
31 | color: $font-grey;
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/User/ListUsers/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListUsers";
2 |
--------------------------------------------------------------------------------
/src/components/User/PasswordForm/PasswordForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, Button } from "semantic-ui-react";
3 | import { useFormik } from "formik";
4 | import * as Yup from "yup";
5 | import { toast } from "react-toastify";
6 | import { useMutation } from "@apollo/client";
7 | import { UPDATE_USER } from "../../../gql/user";
8 | import "./PasswordForm.scss";
9 |
10 | export default function PasswordForm(props) {
11 | const { logout } = props;
12 | const [updateUser] = useMutation(UPDATE_USER);
13 |
14 | const formik = useFormik({
15 | initialValues: initialValues(),
16 | validationSchema: Yup.object({
17 | currentPassword: Yup.string().required(),
18 | newPassword: Yup.string()
19 | .required()
20 | .oneOf([Yup.ref("repeatNewPassoword")]),
21 | repeatNewPassoword: Yup.string()
22 | .required()
23 | .oneOf([Yup.ref("newPassword")]),
24 | }),
25 | onSubmit: async (formValues) => {
26 | try {
27 | const result = await updateUser({
28 | variables: {
29 | input: {
30 | currentPassword: formValues.currentPassword,
31 | newPassword: formValues.newPassword,
32 | },
33 | },
34 | });
35 |
36 | if (!result.data.updateUser) {
37 | toast.error("Error al cambiar la contraseña");
38 | } else {
39 | logout();
40 | }
41 | } catch (error) {
42 | toast.error("Error al cambiar la contraseña");
43 | }
44 | },
45 | });
46 |
47 | return (
48 |
57 |
65 |
73 |
76 |
77 | );
78 | }
79 |
80 | function initialValues() {
81 | return {
82 | currentPassword: "",
83 | newPassword: "",
84 | repeatNewPassoword: "",
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/User/PasswordForm/PasswordForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .password-form {
4 | margin: 20px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/User/PasswordForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PasswordForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/Profile/Followers/Followers.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { size } from "lodash";
3 | import { useQuery } from "@apollo/client";
4 | import { GET_FOLLOWERS, GET_FOLLOWEDS } from "../../../../gql/follow";
5 | import ModalBasic from "../../../Modal/ModalBasic";
6 | import ListUsers from "../../ListUsers";
7 | import "./Followers.scss";
8 |
9 | export default function Followers(props) {
10 | const { username, totalPublications } = props;
11 | const [showModal, setShowModal] = useState(false);
12 | const [titleModal, setTitleModal] = useState("");
13 | const [childrenModal, setChildrenModal] = useState(null);
14 |
15 | const {
16 | data: dataFollowers,
17 | loading: loadingFollowers,
18 | startPolling: startPollingFollowers,
19 | stopPolling: stopPollingFollowers,
20 | } = useQuery(GET_FOLLOWERS, {
21 | variables: { username },
22 | });
23 |
24 | const {
25 | data: dataFolloweds,
26 | loading: loadingFolloweds,
27 | startPolling: startPollingFolloweds,
28 | stopPolling: stopPollingFolloweds,
29 | } = useQuery(GET_FOLLOWEDS, {
30 | variables: { username },
31 | });
32 |
33 | useEffect(() => {
34 | startPollingFollowers(1000);
35 | return () => {
36 | stopPollingFollowers();
37 | };
38 | }, [startPollingFollowers, stopPollingFollowers]);
39 |
40 | useEffect(() => {
41 | startPollingFolloweds(1000);
42 | return () => {
43 | stopPollingFolloweds();
44 | };
45 | }, [startPollingFolloweds, stopPollingFolloweds]);
46 |
47 | const openFollowers = () => {
48 | setTitleModal("Seguidores");
49 | setChildrenModal(
50 |
51 | );
52 | setShowModal(true);
53 | };
54 |
55 | const openFolloweds = () => {
56 | setTitleModal("Usuarios seguidos");
57 | setChildrenModal(
58 |
59 | );
60 | setShowModal(true);
61 | };
62 |
63 | if (loadingFollowers || loadingFolloweds) return null;
64 | const { getFollowers } = dataFollowers;
65 | const { getFolloweds } = dataFolloweds;
66 |
67 | return (
68 | <>
69 |
70 |
71 | {totalPublications} publicaciones
72 |
73 |
74 | {size(getFollowers)} seguidores
75 |
76 |
77 | {size(getFolloweds)} seguidos
78 |
79 |
80 |
81 | {childrenModal}
82 |
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/User/Profile/Followers/Followers.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../scss/index.scss";
2 |
3 | .followers {
4 | display: flex;
5 |
6 | p {
7 | margin-right: 50px;
8 | font-size: 16px;
9 |
10 | &.link:hover {
11 | cursor: pointer;
12 | }
13 |
14 | span {
15 | font-weight: bold;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/User/Profile/Followers/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Followers";
2 |
--------------------------------------------------------------------------------
/src/components/User/Profile/HeaderProfile/HeaderProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "semantic-ui-react";
3 | import { useQuery, useMutation } from "@apollo/client";
4 | import { IS_FOLLOW, FOLLOW, UN_FOLLOW } from "../../../../gql/follow";
5 | import "./HeaderProfile.scss";
6 |
7 | export default function HeaderProfile(props) {
8 | const { getUser, auth, handlerModal } = props;
9 | const [follow] = useMutation(FOLLOW);
10 | const [unFollow] = useMutation(UN_FOLLOW);
11 | const { data, loading, refetch } = useQuery(IS_FOLLOW, {
12 | variables: { username: getUser.username },
13 | });
14 |
15 | const buttonFollow = () => {
16 | if (data.isFollow) {
17 | return (
18 |
21 | );
22 | } else {
23 | return (
24 |
27 | );
28 | }
29 | };
30 |
31 | const onFollow = async () => {
32 | try {
33 | await follow({
34 | variables: {
35 | username: getUser.username,
36 | },
37 | });
38 | refetch();
39 | } catch (error) {
40 | console.log(error);
41 | }
42 | };
43 |
44 | const onUnFollow = async () => {
45 | try {
46 | await unFollow({
47 | variables: {
48 | username: getUser.username,
49 | },
50 | });
51 | refetch();
52 | } catch (error) {
53 | console.log(error);
54 | }
55 | };
56 |
57 | return (
58 |
59 |
{getUser.username}
60 | {getUser.username === auth.username ? (
61 |
62 | ) : (
63 | !loading && buttonFollow()
64 | )}
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/User/Profile/HeaderProfile/HeaderProfile.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../scss/index.scss";
2 |
3 | .header-profile {
4 | display: flex;
5 | align-items: center;
6 | margin-bottom: 20px;
7 |
8 | h2 {
9 | margin: 0 30px 0 0;
10 | }
11 |
12 | button {
13 | padding: 5px 20px !important;
14 | background-color: transparent !important;
15 | border: 1px solid $border-grey !important;
16 | &:hover,
17 | &:focus {
18 | background-color: transparent !important;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/User/Profile/HeaderProfile/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HeaderProfile";
2 |
--------------------------------------------------------------------------------
/src/components/User/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Grid, Image } from "semantic-ui-react";
3 | import { useQuery } from "@apollo/client";
4 | import { GET_USER } from "../../../gql/user";
5 | import userAuth from "../../../hooks/useAuth";
6 | import UserNotFound from "../../UserNotFound";
7 | import ModalBasic from "../../Modal/ModalBasic";
8 | import AvatarForm from "../AvatarForm";
9 | import HeaderProfile from "./HeaderProfile";
10 | import SettignsForm from "../SettignsForm";
11 | import Followers from "./Followers";
12 | import ImageNoFound from "../../../assets/png/avatar.png";
13 | import "./Profile.scss";
14 |
15 | export default function Profile(props) {
16 | const { username, totalPublications } = props;
17 | const [showModal, setShowModal] = useState(false);
18 | const [titleModal, setTitleModal] = useState("");
19 | const [childrenModal, setChildrenModal] = useState(null);
20 | const { auth } = userAuth();
21 | const { data, loading, error, refetch } = useQuery(GET_USER, {
22 | variables: { username },
23 | });
24 |
25 | if (loading) return null;
26 | if (error) return
;
27 | const { getUser } = data;
28 |
29 | const handlerModal = (type) => {
30 | switch (type) {
31 | case "avatar":
32 | setTitleModal("Cambiar foto de perfil");
33 | setChildrenModal(
34 |
35 | );
36 | setShowModal(true);
37 | break;
38 | case "settigns":
39 | setTitleModal("");
40 | setChildrenModal(
41 |
48 | );
49 | setShowModal(true);
50 | break;
51 | default:
52 | break;
53 | }
54 | };
55 |
56 | return (
57 | <>
58 |
59 |
60 | username === auth.username && handlerModal("avatar")}
64 | />
65 |
66 |
67 |
72 |
76 |
77 |
{getUser.name}
78 | {getUser.siteWeb && (
79 |
85 | {getUser.siteWeb}
86 |
87 | )}
88 | {getUser.description && (
89 |
{getUser.description}
90 | )}
91 |
92 |
93 |
94 |
95 | {childrenModal}
96 |
97 | >
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/User/Profile/Profile.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .profile {
4 | margin-top: 20px !important;
5 |
6 | &__left {
7 | display: flex !important;
8 | align-items: center;
9 | justify-content: center;
10 |
11 | .ui.image.avatar {
12 | width: 170px;
13 | height: 170px;
14 |
15 | &:hover {
16 | cursor: pointer;
17 | }
18 | }
19 | }
20 |
21 | &__right {
22 | .other {
23 | .name {
24 | font-size: 16px;
25 | margin-bottom: 3px;
26 | font-weight: bold;
27 | }
28 | .description {
29 | font-size: 16px;
30 | margin-top: 10px;
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/User/Profile/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Profile";
2 |
--------------------------------------------------------------------------------
/src/components/User/SettignsForm/SettignsForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "semantic-ui-react";
3 | import { useHistory } from "react-router-dom";
4 | import { useApolloClient } from "@apollo/client";
5 | import useAuth from "../../../hooks/useAuth";
6 | import PasswordForm from "../PasswordForm";
7 | import EmailForm from "../EmailForm";
8 | import DescriptionForm from "../DescriptionForm";
9 | import SiteWebForm from "../SiteWebForm";
10 | import "./SettignsForm.scss";
11 |
12 | export default function SettignsForm(props) {
13 | const {
14 | setShowModal,
15 | setTitleModal,
16 | setChildrenModal,
17 | getUser,
18 | refetch,
19 | } = props;
20 | const history = useHistory();
21 | const client = useApolloClient();
22 | const { logout } = useAuth();
23 |
24 | const onChangePassoword = () => {
25 | setTitleModal("Cambiar tu contraseña");
26 | setChildrenModal(
);
27 | };
28 |
29 | const onChangeEmail = () => {
30 | setTitleModal("Cambiar email");
31 | setChildrenModal(
32 |
37 | );
38 | };
39 |
40 | const onChangeDescription = () => {
41 | setTitleModal("Actualizar tu biografía");
42 | setChildrenModal(
43 |
48 | );
49 | };
50 |
51 | const onChangeSiteWeb = () => {
52 | setTitleModal("Actualizar sitio web");
53 | setChildrenModal(
54 |
59 | );
60 | };
61 |
62 | const onLogout = () => {
63 | client.clearStore();
64 | logout();
65 | history.push("/");
66 | };
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/User/SettignsForm/SettignsForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .settigns-form {
4 | display: flex;
5 | flex-direction: column;
6 |
7 | .ui.button {
8 | background-color: $background-light;
9 | border-bottom: 1px solid $border-grey;
10 | padding: 18px 0;
11 | margin: 0;
12 | border-radius: 0;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/User/SettignsForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SettignsForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/SiteWebForm/SiteWebForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, Button } from "semantic-ui-react";
3 | import { toast } from "react-toastify";
4 | import { useFormik } from "formik";
5 | import * as Yup from "yup";
6 | import { useMutation } from "@apollo/client";
7 | import { UPDATE_USER } from "../../../gql/user";
8 | import "./SiteWebForm.scss";
9 |
10 | export default function SiteWebForm(props) {
11 | const { setShowModal, currentSiteWeb, refetch } = props;
12 | const [updateUser] = useMutation(UPDATE_USER);
13 |
14 | const formik = useFormik({
15 | initialValues: {
16 | siteWeb: currentSiteWeb || "",
17 | },
18 | validationSchema: Yup.object({
19 | siteWeb: Yup.string().required(),
20 | }),
21 | onSubmit: async (formData) => {
22 | try {
23 | await updateUser({
24 | variables: {
25 | input: formData,
26 | },
27 | });
28 | refetch();
29 | setShowModal(false);
30 | } catch (error) {
31 | toast.error("Error al actualizar tu sitio web");
32 | }
33 | },
34 | });
35 |
36 | return (
37 |
45 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/User/SiteWebForm/SiteWebForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .site-web-form {
4 | margin: 20px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/User/SiteWebForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SiteWebForm";
2 |
--------------------------------------------------------------------------------
/src/components/UserNotFound/UserNotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import "./UserNotFound.scss";
4 |
5 | export default function UserNotFound() {
6 | return (
7 |
8 |
Usuario no encontrado
9 |
10 | Es posible que el enlace que has seguido sea incorrecto o que el usuario
11 | se haya eliminado
12 |
13 |
Volver a la home
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/UserNotFound/UserNotFound.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .user-not-found {
4 | padding: 30px 0;
5 |
6 | p {
7 | text-align: center;
8 |
9 | &:nth-child(1) {
10 | font-size: 25px;
11 | font-weight: bold;
12 | }
13 |
14 | &:nth-child(2) {
15 | font-size: 16px;
16 | }
17 | }
18 |
19 | a {
20 | display: block;
21 | text-align: center;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/UserNotFound/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UserNotFound";
2 |
--------------------------------------------------------------------------------
/src/config/apollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
2 | import { createUploadLink } from "apollo-upload-client";
3 | import { setContext } from "apollo-link-context";
4 | import { getToken } from "../utils/token";
5 |
6 | const httpLik = createUploadLink({
7 | uri: "https://agus-instaclone-server.herokuapp.com/",
8 | });
9 |
10 | const authLink = setContext((_, { headers }) => {
11 | const token = getToken();
12 |
13 | return {
14 | headers: {
15 | ...headers,
16 | Authorization: token ? `Bearer ${token}` : "",
17 | },
18 | };
19 | });
20 |
21 | const client = new ApolloClient({
22 | connectToDevTools: true,
23 | cache: new InMemoryCache(),
24 | link: authLink.concat(httpLik),
25 | });
26 |
27 | export default client;
28 |
--------------------------------------------------------------------------------
/src/context/AuthContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const AuthContext = createContext({
4 | user: undefined,
5 | });
6 |
7 | export default AuthContext;
8 |
--------------------------------------------------------------------------------
/src/gql/comment.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const ADD_COMMENT = gql`
4 | mutation addComment($input: CommentInput) {
5 | addComment(input: $input) {
6 | idPublication
7 | comment
8 | }
9 | }
10 | `;
11 |
12 | export const GET_COMMENTS = gql`
13 | query getComments($idPublication: ID!) {
14 | getComments(idPublication: $idPublication) {
15 | comment
16 | idUser {
17 | username
18 | avatar
19 | }
20 | }
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/src/gql/follow.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const IS_FOLLOW = gql`
4 | query isFollow($username: String!) {
5 | isFollow(username: $username)
6 | }
7 | `;
8 |
9 | export const FOLLOW = gql`
10 | mutation follow($username: String!) {
11 | follow(username: $username)
12 | }
13 | `;
14 |
15 | export const UN_FOLLOW = gql`
16 | mutation unFollow($username: String!) {
17 | unFollow(username: $username)
18 | }
19 | `;
20 |
21 | export const GET_FOLLOWERS = gql`
22 | query getFollowers($username: String!) {
23 | getFollowers(username: $username) {
24 | username
25 | name
26 | avatar
27 | }
28 | }
29 | `;
30 |
31 | export const GET_FOLLOWEDS = gql`
32 | query getFolloweds($username: String!) {
33 | getFolloweds(username: $username) {
34 | username
35 | name
36 | avatar
37 | }
38 | }
39 | `;
40 |
41 | export const GET_NOT_FOLLOWEDS = gql`
42 | query getNotFolloweds {
43 | getNotFolloweds {
44 | username
45 | name
46 | avatar
47 | }
48 | }
49 | `;
50 |
--------------------------------------------------------------------------------
/src/gql/like.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const ADD_LIKE = gql`
4 | mutation addLike($idPublication: ID!) {
5 | addLike(idPublication: $idPublication)
6 | }
7 | `;
8 |
9 | export const IS_LIKE = gql`
10 | query isLike($idPublication: ID!) {
11 | isLike(idPublication: $idPublication)
12 | }
13 | `;
14 |
15 | export const DELETE_LIKE = gql`
16 | mutation deleteLike($idPublication: ID!) {
17 | deleteLike(idPublication: $idPublication)
18 | }
19 | `;
20 |
21 | export const COUNT_LIKE = gql`
22 | query countLikes($idPublication: ID!) {
23 | countLikes(idPublication: $idPublication)
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/gql/publication.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const PUBLISH = gql`
4 | mutation publish($file: Upload) {
5 | publish(file: $file) {
6 | status
7 | urlFile
8 | }
9 | }
10 | `;
11 |
12 | export const GET_PUBLICATIONS = gql`
13 | query getPublications($username: String!) {
14 | getPublications(username: $username) {
15 | id
16 | idUser
17 | file
18 | typeFile
19 | }
20 | }
21 | `;
22 |
23 | export const GET_PUBLICATIONS_FOLLOWEDS = gql`
24 | query getPublicationsFolloweds {
25 | getPublicationsFolloweds {
26 | id
27 | idUser {
28 | username
29 | name
30 | email
31 | avatar
32 | }
33 | file
34 | typeFile
35 | createAt
36 | }
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/gql/user.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const REGISTER = gql`
4 | mutation register($input: UserInput) {
5 | register(input: $input) {
6 | id
7 | name
8 | username
9 | email
10 | createAt
11 | }
12 | }
13 | `;
14 |
15 | export const LOGIN = gql`
16 | mutation login($input: LoginInput) {
17 | login(input: $input) {
18 | token
19 | }
20 | }
21 | `;
22 |
23 | export const GET_USER = gql`
24 | query getUser($id: ID, $username: String) {
25 | getUser(id: $id, username: $username) {
26 | id
27 | name
28 | username
29 | email
30 | siteWeb
31 | description
32 | avatar
33 | }
34 | }
35 | `;
36 |
37 | export const UPDATE_AVATAR = gql`
38 | mutation updateAvatar($file: Upload) {
39 | updateAvatar(file: $file) {
40 | status
41 | urlAvatar
42 | }
43 | }
44 | `;
45 |
46 | export const DELETE_AVATAR = gql`
47 | mutation deleteAvatar {
48 | deleteAvatar
49 | }
50 | `;
51 |
52 | export const UPDATE_USER = gql`
53 | mutation updateUser($input: UserUpdateInput) {
54 | updateUser(input: $input)
55 | }
56 | `;
57 |
58 | export const SEARCH = gql`
59 | query search($search: String) {
60 | search(search: $search) {
61 | name
62 | username
63 | avatar
64 | }
65 | }
66 | `;
67 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import AuthContext from "../context/AuthContext";
3 |
4 | export default () => useContext(AuthContext);
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import * as serviceWorker from "./serviceWorker";
5 | import "semantic-ui-css/semantic.min.css";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import "./index.scss";
8 |
9 | ReactDOM.render(
, document.getElementById("root"));
10 |
11 | // If you want your app to work offline and load faster, you can change
12 | // unregister() to register() below. Note this comes with some pitfalls.
13 | // Learn more about service workers: https://bit.ly/CRA-PWA
14 | serviceWorker.unregister();
15 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import "./scss/index.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | background-color: $background-grey-light;
11 | }
12 |
13 | code {
14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
15 | monospace;
16 | }
17 |
18 | .ui.form {
19 | .btn-submit {
20 | width: 100%;
21 | background-color: $action;
22 | color: $font-light;
23 | }
24 | .submit-error {
25 | color: $danger;
26 | text-align: center;
27 | margin: 20px 0 0 0;
28 | }
29 | }
30 |
31 | .ui.button.btn-action {
32 | background-color: $action !important;
33 | color: $font-light !important;
34 | border-color: $action !important;
35 | }
36 |
37 | .ui.button.btn-danger {
38 | background-color: $danger !important;
39 | color: $font-light !important;
40 | border-color: $danger !important;
41 | }
42 |
--------------------------------------------------------------------------------
/src/layouts/LayoutBasic.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container } from "semantic-ui-react";
3 | import Header from "../components/Header";
4 |
5 | export default function LayoutBasic(props) {
6 | const { children } = props;
7 |
8 | return (
9 | <>
10 |
11 |
{children}
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/Auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Container, Image } from "semantic-ui-react";
3 | import RegisterForm from "../../components/Auth/RegisterForm";
4 | import LoginForm from "../../components/Auth/LoginForm";
5 | import instaclone from "../../assets/png/instaclone.png";
6 | import "./Auth.scss";
7 |
8 | export default function Auth() {
9 | const [showLogin, setShowLogin] = useState(true);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {showLogin ? (
17 |
18 | ) : (
19 |
20 | )}
21 |
22 |
23 |
24 |
25 | {showLogin ? (
26 | <>
27 | ¿No tienes cuenta?
28 | setShowLogin(!showLogin)}>Regístrate
29 | >
30 | ) : (
31 | <>
32 | ¡Entra con tu cuenta!
33 | setShowLogin(!showLogin)}>
34 | Iniciar sesión
35 |
36 | >
37 | )}
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/Auth/Auth.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .auth {
4 | display: flex !important;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | height: 100vh;
9 | background-color: $background-grey-light;
10 |
11 | img {
12 | width: 250px;
13 | }
14 |
15 | .container-form {
16 | width: 400px;
17 | background-color: $background-light;
18 | padding: 20px 50px;
19 | border: 1px solid $border-grey;
20 | margin-top: 50px;
21 | border-radius: 5px;
22 | }
23 |
24 | .change-form {
25 | margin-top: 10px;
26 | width: 400px;
27 | background-color: $background-light;
28 | padding: 20px 50px;
29 | border: 1px solid $border-grey;
30 | text-align: center;
31 | border-radius: 5px;
32 |
33 | span {
34 | color: $action;
35 | font-weight: bold;
36 | margin-left: 5px;
37 | &:hover {
38 | cursor: pointer;
39 | text-decoration: underline;
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/Auth/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Auth";
2 |
--------------------------------------------------------------------------------
/src/pages/Error404/Error404.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Error404.scss";
3 |
4 | export default function Error404() {
5 | return (
6 |
7 |
Error404...
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/Error404/Error404.scss:
--------------------------------------------------------------------------------
1 | .error404 {
2 | }
3 |
--------------------------------------------------------------------------------
/src/pages/Error404/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Error404";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Grid } from "semantic-ui-react";
3 | import Feed from "../../components/Home/Feed";
4 | import UsersNotFolloweds from "../../components/Home/UsersNotFolloweds";
5 | import "./Home.scss";
6 |
7 | export default function Home() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | &__left {
3 | padding-top: 0 !important;
4 | padding-top: 0 !important;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Home";
2 |
--------------------------------------------------------------------------------
/src/pages/User.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { size } from "lodash";
4 | import { useQuery } from "@apollo/client";
5 | import { GET_PUBLICATIONS } from "../gql/publication";
6 | import Profile from "../components/User/Profile";
7 | import Publications from "../components/Publications";
8 |
9 | export default function User() {
10 | const { username } = useParams();
11 | const { data, loading, startPolling, stopPolling } = useQuery(
12 | GET_PUBLICATIONS,
13 | {
14 | variables: { username },
15 | }
16 | );
17 |
18 | useEffect(() => {
19 | startPolling(1000);
20 | return () => {
21 | startPolling();
22 | };
23 | }, [startPolling, stopPolling]);
24 |
25 | if (loading) return null;
26 | const { getPublications } = data;
27 |
28 | return (
29 | <>
30 |
31 |
32 | >
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/routes/Navigation.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
3 | import routes from "./routes";
4 | import { map } from "lodash";
5 |
6 | export default function Navigation() {
7 | return (
8 |
9 |
10 | {map(routes, (route, index) => (
11 | (
16 |
17 |
18 |
19 | )}
20 | />
21 | ))}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/routes/routes.js:
--------------------------------------------------------------------------------
1 | // Layouts
2 | import LayoutBasic from "../layouts/LayoutBasic";
3 |
4 | // Pages
5 | import Home from "../pages/Home";
6 | import User from "../pages/User";
7 | import Error404 from "../pages/Error404";
8 |
9 | const routes = [
10 | {
11 | path: "/",
12 | layout: LayoutBasic,
13 | component: Home,
14 | exact: true,
15 | },
16 | {
17 | path: "/:username",
18 | layout: LayoutBasic,
19 | component: User,
20 | exact: true,
21 | },
22 | {
23 | layout: LayoutBasic,
24 | component: Error404,
25 | },
26 | ];
27 |
28 | export default routes;
29 |
--------------------------------------------------------------------------------
/src/scss/colors.scss:
--------------------------------------------------------------------------------
1 | $font-light: #fff;
2 | $font-grey: #a2a2a2;
3 | $font-dark: #000;
4 |
5 | $background-light: #fff;
6 | $background-grey-light: #fafafa;
7 |
8 | $border-grey: #dbdbdb;
9 | $border-dark: #000;
10 |
11 | $action: #0095f6;
12 | $danger: #ed4956;
13 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import "./colors.scss";
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const TOKEN = "token";
2 |
--------------------------------------------------------------------------------
/src/utils/token.js:
--------------------------------------------------------------------------------
1 | import jwtDecode from "jwt-decode";
2 | import { TOKEN } from "./constants";
3 |
4 | export function setToken(token) {
5 | localStorage.setItem(TOKEN, token);
6 | }
7 |
8 | export function getToken() {
9 | return localStorage.getItem(TOKEN);
10 | }
11 |
12 | export function decodeToken(token) {
13 | return jwtDecode(token);
14 | }
15 |
16 | export function removeToken() {
17 | localStorage.removeItem(TOKEN);
18 | }
19 |
--------------------------------------------------------------------------------