├── .gitignore
├── README.md
├── package.json
├── public
├── _redirects
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── App.test.js
├── api
│ ├── auth.js
│ ├── follow.js
│ ├── tweet.js
│ └── user.js
├── assets
│ ├── png
│ │ ├── avatar-no-found.png
│ │ ├── error-404.png
│ │ ├── logo-white.png
│ │ └── logo.png
│ └── svg
│ │ ├── camera.svg
│ │ ├── close.svg
│ │ ├── date-birth.svg
│ │ ├── link.svg
│ │ └── location.svg
├── components
│ ├── LeftMenu
│ │ ├── LeftMenu.js
│ │ ├── LeftMenu.scss
│ │ └── index.js
│ ├── ListTweets
│ │ ├── ListTweets.js
│ │ ├── ListTweets.scss
│ │ └── index.js
│ ├── ListUsers
│ │ ├── ListUsers.js
│ │ ├── ListUsers.scss
│ │ ├── User.js
│ │ └── index.js
│ ├── Modal
│ │ ├── BasicModal
│ │ │ ├── BasicModal.js
│ │ │ ├── BasicModal.scss
│ │ │ └── index.js
│ │ ├── ConfigModal
│ │ │ ├── ConfigModal.js
│ │ │ ├── ConfigModal.scss
│ │ │ └── index.js
│ │ └── TweetModal
│ │ │ ├── TweetModal.js
│ │ │ ├── TweetModal.scss
│ │ │ └── index.js
│ ├── SignInForm
│ │ ├── SignInForm.js
│ │ ├── SignInForm.scss
│ │ └── index.js
│ ├── SignUpForm
│ │ ├── SignUpForm.js
│ │ ├── SignUpForm.scss
│ │ └── index.js
│ └── User
│ │ ├── BannerAvatar
│ │ ├── BannerAvatar.js
│ │ ├── BannerAvatar.scss
│ │ └── index.js
│ │ ├── EditUserForm
│ │ ├── EditUserForm.js
│ │ ├── EditUserForm.scss
│ │ └── index.js
│ │ └── InfoUser
│ │ ├── InfoUser.js
│ │ ├── InfoUser.scss
│ │ └── index.js
├── hooks
│ └── useAuth.js
├── index.js
├── index.scss
├── layout
│ └── BasicLayout
│ │ ├── BasicLayout.js
│ │ ├── BasicLayout.scss
│ │ └── index.js
├── page
│ ├── Error404
│ │ ├── Error404.js
│ │ ├── Error404.scss
│ │ └── index.js
│ ├── Home
│ │ ├── Home.js
│ │ ├── Home.scss
│ │ └── index.js
│ ├── SignInSingUp
│ │ ├── SignInSingUp.js
│ │ ├── SignInSingUp.scss
│ │ └── index.js
│ ├── User
│ │ ├── User.js
│ │ ├── User.scss
│ │ └── index.js
│ └── Users
│ │ ├── Users.js
│ │ ├── Users.scss
│ │ └── index.js
├── routes
│ ├── Routing.js
│ └── configRouting.js
├── scss
│ ├── _colors.scss
│ └── index.scss
├── serviceWorker.js
├── setupTests.js
└── utils
│ ├── Icons.js
│ ├── constant.js
│ ├── contexts.js
│ ├── functions.js
│ └── validations.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 | # React JS, Golang y MongoDB: Creando Red Social como Twitter
2 |
3 | _Curso en Udemy donde se explica paso a paso la creacion de una Red Social._
4 |
5 | **Curso:** https://courses.agustinnavarrogaldon.com/react-golang-twitter
6 |
7 | ## Comenzando 🚀
8 |
9 | _En este curso vas a aprender a **crear una aplicación web de como Twitter** donde tendremos las siguientes caracteristicas._
10 |
11 | **Perfil de usuario**
12 | _Podremos ver nuestro propio perfil donde tendremos datos del usuario, avatar, bañera y un los tweets del usuario y podremos visitar el perfil de otros usuarios, pero solo se podrá editar el perfil de usuario de uno mismo._
13 |
14 | **Sistema de Followers**
15 | _Podremos seguir Y dejar de seguir a otros usuarios que estén registrados en la aplicación y tendremos una lista de usuarios para ver a quien estamos siguiendo en todo momento._
16 |
17 | **Sistema de Tweets**
18 | _Podremos mandar tweets en cualquier momento y desde cualquier página de nuestra aplicación y cuando visitemos el perfil de otro usuario podremos ver todos sus tweets._
19 |
20 | **Buscador de usuarios**
21 | _Podremos buscar usuarios por su nombre y filtrar la búsqueda entre usuarios que no estamos siguiendo o usuarios que estamos siguiendo._
22 |
23 | **Feed de Tweets**
24 | _Tendremos una pagina donde podremos ver los últimos tweets que han enviado los usuarios que estamos siguiendo._
25 |
26 | _Este curso tiene como objetivo enseñarte a desarrollar cualquier tipo de aplicación de web usando Golang en el backend y React en el frontend._
27 |
28 | ### Estructura del curso
29 |
30 | - ¡La Biblioteca creada por Facebook! REACT JS
31 | - Añadiremos SASS al proyecto
32 | - Sistema de Login y Registro con JWR
33 | - Enrutamiento con React Router Dom
34 | - Subiremos imágenes al servidor usando Drag & Drop
35 | - Consumir un API REST
36 | - Subir la web a Netlify y GitHub Pages
37 | - El BackEnd será desarrollado en GO (creado por Google)
38 | - Incorporaremos los patrones de JWT (Jason Web Token)
39 | - Incorporaremos bCrypt para encriptar nuestras passwords
40 | - Utilizaremos la BD MongoDB, en su versión gratuita que se autoalojará en AWS (Amazon Web Services)
41 | - Estructuraremos nuestro proyecto de acuerdo a los estándares requeridos por google para las aplicaciones GOLANG
42 | - Crearemos 16 EndPoints, más de 30 archivos .GO para armar una API Rest muy versatil y potente.
43 |
44 | ---
45 |
46 | ⌨️ con ❤️ por [xAgustin93](https://github.com/xAgustin93) 😊
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twittor",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.28",
7 | "@fortawesome/free-solid-svg-icons": "^5.13.0",
8 | "@fortawesome/react-fontawesome": "^0.1.9",
9 | "@testing-library/jest-dom": "^4.2.4",
10 | "@testing-library/react": "^9.3.2",
11 | "@testing-library/user-event": "^7.1.2",
12 | "bootstrap": "^4.4.1",
13 | "classnames": "^2.2.6",
14 | "date-fns": "^2.11.1",
15 | "jwt-decode": "^2.2.0",
16 | "lodash": "^4.17.15",
17 | "moment": "^2.24.0",
18 | "node-sass": "^4.13.1",
19 | "query-string": "^6.12.0",
20 | "react": "^16.13.1",
21 | "react-bootstrap": "^1.0.0",
22 | "react-datepicker": "^2.14.1",
23 | "react-dom": "^16.13.1",
24 | "react-dropzone": "^10.2.2",
25 | "react-router-dom": "^5.1.2",
26 | "react-scripts": "3.4.1",
27 | "react-toastify": "^5.5.0",
28 | "use-debounce": "^3.4.1"
29 | },
30 | "scripts": {
31 | "dev": "react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": "react-app"
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Twittor - Que se esta hablando
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import SignInSingUp from "./page/SignInSingUp";
3 | import { ToastContainer } from "react-toastify";
4 | import { AuthContext } from "./utils/contexts";
5 | import { isUserLogedApi } from "./api/auth";
6 | import Routing from "./routes/Routing";
7 |
8 | export default function App() {
9 | const [user, setUser] = useState(null);
10 | const [loadUser, setLoadUser] = useState(false);
11 | const [refreshCheckLogin, setRefreshCheckLogin] = useState(false);
12 |
13 | useEffect(() => {
14 | setUser(isUserLogedApi());
15 | setRefreshCheckLogin(false);
16 | setLoadUser(true);
17 | }, [refreshCheckLogin]);
18 |
19 | if (!loadUser) return null;
20 |
21 | return (
22 |
23 | {user ? (
24 |
25 | ) : (
26 |
27 | )}
28 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/api/auth.js:
--------------------------------------------------------------------------------
1 | import { API_HOST, TOKEN } from "../utils/constant";
2 | import jwtDecode from "jwt-decode";
3 |
4 | export function signUpApi(user) {
5 | const url = `${API_HOST}/registro`;
6 | const userTemp = {
7 | ...user,
8 | email: user.email.toLowerCase(),
9 | fechaNacimiento: new Date()
10 | };
11 | delete userTemp.repeatPassword;
12 |
13 | const params = {
14 | method: "POST",
15 | headers: {
16 | "Content-Type": "application/json"
17 | },
18 | body: JSON.stringify(userTemp)
19 | };
20 |
21 | return fetch(url, params)
22 | .then(response => {
23 | if (response.status >= 200 && response.status < 300) {
24 | return response.json();
25 | }
26 | return { code: 404, message: "Email no disponible" };
27 | })
28 | .then(result => {
29 | return result;
30 | })
31 | .catch(err => {
32 | return err;
33 | });
34 | }
35 |
36 | export function signInApi(user) {
37 | const url = `${API_HOST}/login`;
38 |
39 | const data = {
40 | ...user,
41 | email: user.email.toLowerCase()
42 | };
43 |
44 | const params = {
45 | method: "POST",
46 | headers: {
47 | "Content-Type": "application/json"
48 | },
49 | body: JSON.stringify(data)
50 | };
51 |
52 | return fetch(url, params)
53 | .then(response => {
54 | if (response.status >= 200 && response.status < 300) {
55 | return response.json();
56 | }
57 | return { message: "Usuario o contraseña incorrectos" };
58 | })
59 | .then(result => {
60 | return result;
61 | })
62 | .catch(err => {
63 | return err;
64 | });
65 | }
66 |
67 | export function setTokenApi(token) {
68 | localStorage.setItem(TOKEN, token);
69 | }
70 |
71 | export function getTokenApi() {
72 | return localStorage.getItem(TOKEN);
73 | }
74 |
75 | export function logoutApi() {
76 | localStorage.removeItem(TOKEN);
77 | }
78 |
79 | export function isUserLogedApi() {
80 | const token = getTokenApi();
81 |
82 | if (!token) {
83 | logoutApi();
84 | return null;
85 | }
86 | if (isExpired(token)) {
87 | logoutApi();
88 | }
89 | return jwtDecode(token);
90 | }
91 |
92 | function isExpired(token) {
93 | const { exp } = jwtDecode(token);
94 | const expire = exp * 1000;
95 | const timeout = expire - Date.now();
96 |
97 | if (timeout < 0) {
98 | return true;
99 | }
100 | return false;
101 | }
102 |
--------------------------------------------------------------------------------
/src/api/follow.js:
--------------------------------------------------------------------------------
1 | import { API_HOST } from "../utils/constant";
2 | import { getTokenApi } from "./auth";
3 |
4 | export function checkFollowApi(idUser) {
5 | const url = `${API_HOST}/consultaRelacion?id=${idUser}`;
6 |
7 | const params = {
8 | headers: {
9 | Authorization: `Bearer ${getTokenApi()}`,
10 | },
11 | };
12 |
13 | return fetch(url, params)
14 | .then((response) => {
15 | return response.json();
16 | })
17 | .then((result) => {
18 | return result;
19 | })
20 | .catch((err) => {
21 | return err;
22 | });
23 | }
24 |
25 | export function followUserApi(idUser) {
26 | const url = `${API_HOST}/altaRelacion?id=${idUser}`;
27 |
28 | const params = {
29 | method: "POST",
30 | headers: {
31 | Authorization: `Bearer ${getTokenApi()}`,
32 | },
33 | };
34 |
35 | return fetch(url, params)
36 | .then((response) => {
37 | return response.json();
38 | })
39 | .then((result) => {
40 | return result;
41 | })
42 | .catch((err) => {
43 | return err;
44 | });
45 | }
46 |
47 | export function unfollowUserApi(idUser) {
48 | const url = `${API_HOST}/bajaRelacion?id=${idUser}`;
49 |
50 | const params = {
51 | method: "DELETE",
52 | headers: {
53 | Authorization: `Bearer ${getTokenApi()}`,
54 | },
55 | };
56 |
57 | return fetch(url, params)
58 | .then((response) => {
59 | return response.json();
60 | })
61 | .then((result) => {
62 | return result;
63 | })
64 | .catch((err) => {
65 | return err;
66 | });
67 | }
68 |
69 | export function getFollowsApi(paramsUrl) {
70 | const url = `${API_HOST}/listaUsuarios?${paramsUrl}`;
71 |
72 | const params = {
73 | headers: {
74 | Authorization: `Bearer ${getTokenApi()}`,
75 | },
76 | };
77 |
78 | return fetch(url, params)
79 | .then((response) => {
80 | return response.json();
81 | })
82 | .then((result) => {
83 | return result;
84 | })
85 | .catch((err) => {
86 | return err;
87 | });
88 | }
89 |
--------------------------------------------------------------------------------
/src/api/tweet.js:
--------------------------------------------------------------------------------
1 | import { API_HOST } from "../utils/constant";
2 | import { getTokenApi } from "./auth";
3 |
4 | export function addTweetApi(mensaje) {
5 | const url = `${API_HOST}/tweet`;
6 | const data = {
7 | mensaje,
8 | };
9 |
10 | const params = {
11 | method: "POST",
12 | headers: {
13 | "Content-Type": "application/json",
14 | Authorization: `Bearer ${getTokenApi()}`,
15 | },
16 | body: JSON.stringify(data),
17 | };
18 |
19 | return fetch(url, params)
20 | .then((response) => {
21 | if (response.status >= 200 && response.status < 300) {
22 | return { code: response.status, message: "Tweet enviado." };
23 | }
24 | return { code: 500, message: "Error del servidor." };
25 | })
26 | .catch((err) => {
27 | return err;
28 | });
29 | }
30 |
31 | export function getUserTweetsApi(idUser, page) {
32 | const url = `${API_HOST}/leoTweets?id=${idUser}&pagina=${page}`;
33 |
34 | const params = {
35 | headers: {
36 | "Content-Type": "application/json",
37 | Authorization: `Bearer ${getTokenApi()}`,
38 | },
39 | };
40 |
41 | return fetch(url, params)
42 | .then((response) => {
43 | return response.json();
44 | })
45 | .catch((err) => {
46 | return err;
47 | });
48 | }
49 |
50 | export function getTweetsFollowersApi(page = 1) {
51 | const url = `${API_HOST}/leoTweetsSeguidores?pagina=${page}`;
52 |
53 | const params = {
54 | headers: {
55 | "Content-Type": "application/json",
56 | Authorization: `Bearer ${getTokenApi()}`,
57 | },
58 | };
59 |
60 | return fetch(url, params)
61 | .then((response) => {
62 | return response.json();
63 | })
64 | .catch((err) => {
65 | return err;
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import { API_HOST } from "../utils/constant";
2 | import { getTokenApi } from "./auth";
3 |
4 | export function getUserApi(id) {
5 | const url = `${API_HOST}/verperfil?id=${id}`;
6 |
7 | const params = {
8 | headers: {
9 | "Content-Type": "application/json",
10 | Authorization: `Bearer ${getTokenApi()}`
11 | }
12 | };
13 |
14 | return fetch(url, params)
15 | .then(response => {
16 | // eslint-disable-next-line no-throw-literal
17 | if (response.status >= 400) throw null;
18 | return response.json();
19 | })
20 | .then(result => {
21 | return result;
22 | })
23 | .catch(err => {
24 | return err;
25 | });
26 | }
27 |
28 | export function uploadBannerApi(file) {
29 | const url = `${API_HOST}/subirBanner`;
30 |
31 | const formData = new FormData();
32 | formData.append("banner", file);
33 |
34 | const params = {
35 | method: "POST",
36 | headers: {
37 | Authorization: `Bearer ${getTokenApi()}`
38 | },
39 | body: formData
40 | };
41 |
42 | return fetch(url, params)
43 | .then(response => {
44 | return response.json();
45 | })
46 | .then(result => {
47 | return result;
48 | })
49 | .catch(err => {
50 | return err;
51 | });
52 | }
53 |
54 | export function uploadAvatarApi(file) {
55 | const url = `${API_HOST}/subirAvatar`;
56 |
57 | const formData = new FormData();
58 | formData.append("avatar", file);
59 |
60 | const params = {
61 | method: "POST",
62 | headers: {
63 | Authorization: `Bearer ${getTokenApi()}`
64 | },
65 | body: formData
66 | };
67 |
68 | return fetch(url, params)
69 | .then(response => {
70 | return response.json();
71 | })
72 | .then(result => {
73 | return result;
74 | })
75 | .catch(err => {
76 | return err;
77 | });
78 | }
79 |
80 | export function updateInfoApi(data) {
81 | const url = `${API_HOST}/modificarPerfil`;
82 |
83 | const params = {
84 | method: "PUT",
85 | headers: {
86 | Authorization: `Bearer ${getTokenApi()}`
87 | },
88 | body: JSON.stringify(data)
89 | };
90 |
91 | return fetch(url, params)
92 | .then(response => {
93 | return response;
94 | })
95 | .catch(err => {
96 | return err;
97 | });
98 | }
99 |
--------------------------------------------------------------------------------
/src/assets/png/avatar-no-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/src/assets/png/avatar-no-found.png
--------------------------------------------------------------------------------
/src/assets/png/error-404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/src/assets/png/error-404.png
--------------------------------------------------------------------------------
/src/assets/png/logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/src/assets/png/logo-white.png
--------------------------------------------------------------------------------
/src/assets/png/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAgustin93/twitter-golang-react-client/453f2a98cd336982d5bddf492f00339862e5bc88/src/assets/png/logo.png
--------------------------------------------------------------------------------
/src/assets/svg/camera.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
54 |
--------------------------------------------------------------------------------
/src/assets/svg/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/src/assets/svg/date-birth.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------
/src/assets/svg/location.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
55 |
--------------------------------------------------------------------------------
/src/components/LeftMenu/LeftMenu.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button } from "react-bootstrap";
3 | import { Link } from "react-router-dom";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import {
6 | faHome,
7 | faUser,
8 | faUsers,
9 | faPowerOff,
10 | } from "@fortawesome/free-solid-svg-icons";
11 | import TweetModal from "../Modal/TweetModal";
12 | import { logoutApi } from "../../api/auth";
13 | import useAuth from "../../hooks/useAuth";
14 | import LogoWhite from "../../assets/png/logo-white.png";
15 |
16 | import "./LeftMenu.scss";
17 |
18 | export default function LeftMenu(props) {
19 | const { setRefreshCheckLogin } = props;
20 | const [showModal, setShowModal] = useState(false);
21 | const user = useAuth();
22 |
23 | const logout = () => {
24 | logoutApi();
25 | setRefreshCheckLogin(true);
26 | };
27 |
28 | return (
29 |
30 |

31 |
32 |
33 |
Inicio
34 |
35 |
36 |
Usuarios
37 |
38 |
39 |
Perfil
40 |
41 |
42 |
Cerrar sesión
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/LeftMenu/LeftMenu.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .left-menu {
4 | display: flex;
5 | align-items: flex-start;
6 | flex-direction: column;
7 | min-height: 100vh;
8 | border-right: 1px solid $border-grey;
9 |
10 | .logo {
11 | width: 50px;
12 | padding: 20px 0 30px 0;
13 | }
14 |
15 | a {
16 | font-size: 20px;
17 | color: $font-light;
18 | font-weight: bold;
19 | margin-bottom: 30px;
20 |
21 | &:hover {
22 | text-decoration: none;
23 | color: $primary;
24 | }
25 |
26 | svg {
27 | margin-right: 20px;
28 | width: 30px !important;
29 | }
30 | }
31 |
32 | button {
33 | width: 80%;
34 | border-radius: 50px;
35 | font-size: 18px;
36 | padding: 10px;
37 | margin-top: 30px;
38 | font-weight: bold;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/LeftMenu/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./LeftMenu";
2 |
--------------------------------------------------------------------------------
/src/components/ListTweets/ListTweets.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Image } from "react-bootstrap";
3 | import { map } from "lodash";
4 | import moment from "moment";
5 | import AvatarNoFound from "../../assets/png/avatar-no-found.png";
6 | import { API_HOST } from "../../utils/constant";
7 | import { getUserApi } from "../../api/user";
8 | import { replaceURLWithHTMLLinks } from "../../utils/functions";
9 |
10 | import "./ListTweets.scss";
11 |
12 | export default function ListTweets(props) {
13 | const { tweets } = props;
14 |
15 | return (
16 |
17 | {map(tweets, (tweet, index) => (
18 |
19 | ))}
20 |
21 | );
22 | }
23 |
24 | function Tweet(props) {
25 | const { tweet } = props;
26 | const [userInfo, setUserInfo] = useState(null);
27 | const [avatarUrl, setAvatarUrl] = useState(null);
28 |
29 | useEffect(() => {
30 | getUserApi(tweet.userId).then((response) => {
31 | setUserInfo(response);
32 | setAvatarUrl(
33 | response?.avatar
34 | ? `${API_HOST}/obtenerAvatar?id=${response.id}`
35 | : AvatarNoFound
36 | );
37 | });
38 | }, [tweet]);
39 |
40 | return (
41 |
42 |
43 |
44 |
45 | {userInfo?.nombre} {userInfo?.apellidos}
46 | {moment(tweet.fecha).calendar()}
47 |
48 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ListTweets/ListTweets.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .list-tweets {
4 | .tweet {
5 | display: flex;
6 | padding: 10px 20px;
7 | border-bottom: 1px solid $border-grey;
8 |
9 | .avatar {
10 | width: 50px;
11 | height: 50px;
12 | margin-right: 20px;
13 | }
14 |
15 | .name {
16 | font-weight: bold;
17 |
18 | span {
19 | margin-left: 10px;
20 | font-size: 12px;
21 | font-weight: normal;
22 | color: $font-grey;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ListTweets/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListTweets";
2 |
--------------------------------------------------------------------------------
/src/components/ListUsers/ListUsers.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { map, isEmpty } from "lodash";
3 | import User from "./User";
4 |
5 | import "./ListUsers.scss";
6 |
7 | export default function ListUsers(props) {
8 | const { users } = props;
9 |
10 | if (isEmpty(users)) {
11 | return No hay resultados
;
12 | }
13 |
14 | return (
15 |
16 | {map(users, (user) => (
17 |
18 | ))}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/ListUsers/ListUsers.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .list-users {
4 | padding: 0;
5 |
6 | &__user {
7 | padding: 15px 30px;
8 | color: $font-light;
9 | border-bottom: 1px solid $border-grey;
10 |
11 | &:hover {
12 | color: $font-light;
13 | background-color: $background-dark-light;
14 | text-decoration: none;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/ListUsers/User.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Media, Image } from "react-bootstrap";
3 | import { Link } from "react-router-dom";
4 | import { API_HOST } from "../../utils/constant";
5 | import { getUserApi } from "../../api/user";
6 | import AvatarNoFound from "../../assets/png/avatar-no-found.png";
7 |
8 | export default function User(props) {
9 | const { user } = props;
10 | const [userInfo, setIserInfo] = useState(null);
11 |
12 | useEffect(() => {
13 | getUserApi(user.id).then((response) => {
14 | setIserInfo(response);
15 | });
16 | }, [user]);
17 |
18 | return (
19 |
20 |
32 |
33 |
34 | {user.nombre} {user.apellido}
35 |
36 | {userInfo?.biografia}
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ListUsers/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListUsers";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/BasicModal/BasicModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal } from "react-bootstrap";
3 | import LogoWhiteTwittor from "../../../assets/png/logo-white.png";
4 |
5 | import "./BasicModal.scss";
6 |
7 | export default function BasicModal(props) {
8 | const { show, setShow, children } = props;
9 |
10 | return (
11 | setShow(false)}
15 | centered
16 | size="lg"
17 | >
18 |
19 |
20 |
21 |
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Modal/BasicModal/BasicModal.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .basic-modal {
4 | .modal-content {
5 | background-color: $background-grey-dark;
6 | border-radius: 25px;
7 |
8 | .modal-header {
9 | padding: 15px;
10 | border: 0;
11 | .modal-title {
12 | margin: 0 auto;
13 |
14 | img {
15 | width: 50px;
16 | }
17 | }
18 | }
19 |
20 | .modal-body {
21 | padding: 20px 40px 40px 40px;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Modal/BasicModal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./BasicModal";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/ConfigModal/ConfigModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal } from "react-bootstrap";
3 | import { Close } from "../../../utils/Icons";
4 |
5 | import "./ConfigModal.scss";
6 |
7 | export default function ConfigModal(props) {
8 | const { show, setShow, title, children } = props;
9 |
10 | return (
11 | setShow(false)}
15 | centered
16 | size="lg"
17 | >
18 |
19 |
20 | setShow(false)} />
21 | {title}
22 |
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Modal/ConfigModal/ConfigModal.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .config-modal {
4 | .modal-content {
5 | background-color: $background-grey-dark;
6 | border-radius: 25px;
7 | // overflow: hidden;
8 |
9 | .modal-header {
10 | border-bottom: 1px solid $border-grey;
11 | padding: 15px;
12 |
13 | .modal-title {
14 | display: flex;
15 | align-items: center;
16 |
17 | svg {
18 | width: 15px;
19 | height: 15px;
20 | fill: $primary;
21 | margin-left: 10px;
22 | margin-right: 20px;
23 |
24 | &:hover {
25 | cursor: pointer;
26 | }
27 | }
28 |
29 | h2 {
30 | font-size: 18px;
31 | margin: 0;
32 | }
33 | }
34 | }
35 |
36 | .modal-body {
37 | padding: 0;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Modal/ConfigModal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ConfigModal";
2 |
--------------------------------------------------------------------------------
/src/components/Modal/TweetModal/TweetModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Modal, Form, Button } from "react-bootstrap";
3 | import classNames from "classnames";
4 | import { toast } from "react-toastify";
5 | import { Close } from "../../../utils/Icons";
6 | import { addTweetApi } from "../../../api/tweet";
7 |
8 | import "./TweetModal.scss";
9 |
10 | export default function TweetModal(props) {
11 | const { show, setShow } = props;
12 | const [message, setMessage] = useState("");
13 | const maxLength = 280;
14 |
15 | const onSubmit = (e) => {
16 | e.preventDefault();
17 |
18 | if (message.length > 0 && message.length <= maxLength) {
19 | addTweetApi(message)
20 | .then((response) => {
21 | console.log(response);
22 | if (response?.code >= 200 && response?.code < 300) {
23 | toast.success(response.message);
24 | setShow(false);
25 | window.location.reload();
26 | }
27 | })
28 | .catch(() => {
29 | toast.warning("Erorr al enviar el tweet, inténtelo más tarde.");
30 | });
31 | }
32 | };
33 |
34 | return (
35 | setShow(false)}
39 | centered
40 | size="lg"
41 | >
42 |
43 |
44 | setShow(false)} />
45 |
46 |
47 |
48 | setMessage(e.target.value)}
54 | />
55 | maxLength,
58 | })}
59 | >
60 | {message.length}
61 |
62 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/Modal/TweetModal/TweetModal.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .tweet-modal {
4 | .modal-content {
5 | background-color: $background-grey-dark;
6 | border-radius: 25px;
7 | overflow: hidden;
8 |
9 | .modal-header {
10 | border-bottom: 1px solid $border-grey;
11 | padding: 15px;
12 |
13 | .modal-title {
14 | svg {
15 | width: 15px;
16 | height: 15px;
17 | fill: $primary;
18 | margin-left: 10px;
19 | &:hover {
20 | cursor: pointer;
21 | }
22 | }
23 | }
24 | }
25 |
26 | .modal-body {
27 | padding: 0;
28 |
29 | form {
30 | textarea {
31 | background-color: $background-grey-dark;
32 | border: 0;
33 | font-size: 20px;
34 | color: $font-light;
35 | padding: 10px 20px;
36 | resize: none;
37 | &:focus {
38 | box-shadow: none;
39 | }
40 | }
41 | .count {
42 | margin: 15px;
43 | &.error {
44 | color: $danger;
45 | }
46 | }
47 | button {
48 | width: 100%;
49 | height: 60px;
50 | font-weight: bold;
51 | font-size: 18px;
52 | margin-top: 10px;
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/Modal/TweetModal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./TweetModal";
2 |
--------------------------------------------------------------------------------
/src/components/SignInForm/SignInForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Form, Button, Spinner } from "react-bootstrap";
3 | import { values, size } from "lodash";
4 | import { toast } from "react-toastify";
5 | import { isEmailValid } from "../../utils/validations";
6 | import { signInApi, setTokenApi } from "../../api/auth";
7 |
8 | import "./SignInForm.scss";
9 |
10 | export default function SignInForm(props) {
11 | const { setRefreshCheckLogin } = props;
12 | const [formData, setFormData] = useState(initialFormValue());
13 | const [signInLoading, setSignInLoading] = useState(false);
14 |
15 | const onSubmit = e => {
16 | e.preventDefault();
17 |
18 | let validCount = 0;
19 | values(formData).some(value => {
20 | value && validCount++;
21 | return null;
22 | });
23 |
24 | if (size(formData) !== validCount) {
25 | toast.warning("Completa todo los campos del formulario");
26 | } else {
27 | if (!isEmailValid(formData.email)) {
28 | toast.warning("Email es invalido");
29 | } else {
30 | setSignInLoading(true);
31 | signInApi(formData)
32 | .then(response => {
33 | if (response.message) {
34 | toast.warning(response.message);
35 | } else {
36 | setTokenApi(response.token);
37 | setRefreshCheckLogin(true);
38 | }
39 | })
40 | .catch(() => {
41 | toast.error("Error del servidor, inténtelo más tarde");
42 | })
43 | .finally(() => {
44 | setSignInLoading(false);
45 | });
46 | }
47 | }
48 | };
49 |
50 | const onChange = e => {
51 | setFormData({ ...formData, [e.target.name]: e.target.value });
52 | };
53 |
54 | return (
55 |
56 |
Entrar
57 |
59 |
65 |
66 |
67 |
73 |
74 |
77 |
78 |
79 | );
80 | }
81 |
82 | function initialFormValue() {
83 | return {
84 | email: "",
85 | password: ""
86 | };
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/SignInForm/SignInForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .sign-in-form {
4 | h2 {
5 | font-weight: bold;
6 | padding-bottom: 30px;
7 | }
8 |
9 | .form-group {
10 | padding-bottom: 20px;
11 |
12 | input {
13 | font-size: 25px;
14 | }
15 | }
16 |
17 | button {
18 | width: 100%;
19 | font-size: 20px;
20 | font-weight: bold;
21 | padding: 15px;
22 | border-radius: 50px;
23 | margin-top: 30px;
24 |
25 | .spinner-border {
26 | width: 25px;
27 | height: 25px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/SignInForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SignInForm";
2 |
--------------------------------------------------------------------------------
/src/components/SignUpForm/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Row, Col, Form, Button, Spinner } from "react-bootstrap";
3 | import { values, size } from "lodash";
4 | import { toast } from "react-toastify";
5 | import { isEmailValid } from "../../utils/validations";
6 | import { signUpApi } from "../../api/auth";
7 |
8 | import "./SignUpForm.scss";
9 |
10 | export default function SignUpForm(props) {
11 | const { setShowModal } = props;
12 | const [formData, setFormData] = useState(initialFormValue());
13 | const [signUpLoading, setSignUpLoading] = useState(false);
14 |
15 | const onSubmit = e => {
16 | e.preventDefault();
17 |
18 | let validCount = 0;
19 | values(formData).some(value => {
20 | value && validCount++;
21 | return null;
22 | });
23 |
24 | if (validCount !== size(formData)) {
25 | toast.warning("Completa todos los campos del formulario");
26 | } else {
27 | if (!isEmailValid(formData.email)) {
28 | toast.warning("Email invalido");
29 | } else if (formData.password !== formData.repeatPassword) {
30 | toast.warning("Las contraseñas tienen que ser iguales");
31 | } else if (size(formData.password) < 6) {
32 | toast.warning("La contraseña tiene que tener al menos 6 caracteres");
33 | } else {
34 | setSignUpLoading(true);
35 | signUpApi(formData)
36 | .then(response => {
37 | if (response.code) {
38 | toast.warning(response.message);
39 | } else {
40 | toast.success("El registro ha sido correcto");
41 | setShowModal(false);
42 | setFormData(initialFormValue());
43 | }
44 | })
45 | .catch(() => {
46 | toast.error("Error del servidor, inténtelo más tarde!");
47 | })
48 | .finally(() => {
49 | setSignUpLoading(false);
50 | });
51 | }
52 | }
53 | };
54 |
55 | const onChange = e => {
56 | setFormData({ ...formData, [e.target.name]: e.target.value });
57 | };
58 |
59 | return (
60 |
61 |
Crea tu cuenta
62 |
64 |
65 |
66 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
90 |
91 |
92 |
93 |
94 |
100 |
101 |
102 |
108 |
109 |
110 |
111 |
112 |
115 |
116 |
117 | );
118 | }
119 |
120 | function initialFormValue() {
121 | return {
122 | nombre: "",
123 | apellidos: "",
124 | email: "",
125 | password: "",
126 | repeatPassword: ""
127 | };
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/SignUpForm/SignUpForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .sign-up-form {
4 | h2 {
5 | font-weight: bold;
6 | padding-bottom: 30px;
7 | }
8 |
9 | .form-group {
10 | padding-bottom: 20px;
11 |
12 | input {
13 | font-size: 25px;
14 | }
15 | }
16 |
17 | button {
18 | width: 100%;
19 | font-size: 20px;
20 | font-weight: bold;
21 | padding: 15px;
22 | border-radius: 50px;
23 | margin-top: 20px;
24 |
25 | .spinner-border {
26 | width: 25px;
27 | height: 25px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/SignUpForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SignUpForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/BannerAvatar/BannerAvatar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Button } from "react-bootstrap";
3 | import ConfigModal from "../../Modal/ConfigModal";
4 | import EditUserForm from "../../User/EditUserForm";
5 | import AvatarNoFound from "../../../assets/png/avatar-no-found.png";
6 | import { API_HOST } from "../../../utils/constant";
7 | import {
8 | checkFollowApi,
9 | followUserApi,
10 | unfollowUserApi
11 | } from "../../../api/follow";
12 |
13 | import "./BannerAvatar.scss";
14 |
15 | export default function BannerAvatar(props) {
16 | const { user, loggedUser } = props;
17 | const [showModal, setShowModal] = useState(false);
18 | const [following, setFollowing] = useState(null);
19 | const [reloadFollow, setReloadFollow] = useState(false);
20 | const bannerUrl = user?.banner
21 | ? `${API_HOST}/obtenerBanner?id=${user.id}`
22 | : null;
23 | const avatarUrl = user?.avatar
24 | ? `${API_HOST}/obtenerAvatar?id=${user.id}`
25 | : AvatarNoFound;
26 |
27 | useEffect(() => {
28 | if (user) {
29 | checkFollowApi(user?.id).then(response => {
30 | if (response?.status) {
31 | setFollowing(true);
32 | } else {
33 | setFollowing(false);
34 | }
35 | });
36 | }
37 | setReloadFollow(false);
38 | }, [user, reloadFollow]);
39 |
40 | const onFollow = () => {
41 | followUserApi(user.id).then(() => {
42 | setReloadFollow(true);
43 | });
44 | };
45 |
46 | const onUnfollow = () => {
47 | unfollowUserApi(user.id).then(() => {
48 | setReloadFollow(true);
49 | });
50 | };
51 |
52 | return (
53 |
57 |
61 | {user && (
62 |
63 | {loggedUser._id === user.id && (
64 |
65 | )}
66 |
67 | {loggedUser._id !== user.id &&
68 | following !== null &&
69 | (following ? (
70 |
73 | ) : (
74 |
75 | ))}
76 |
77 | )}
78 |
79 |
84 |
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/User/BannerAvatar/BannerAvatar.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .banner-avatar {
4 | position: relative;
5 | background-color: $background-dark-light;
6 | height: 220px;
7 | margin-bottom: 60px;
8 | background-size: cover;
9 | background-position: center;
10 | background-repeat: no-repeat;
11 |
12 | .avatar {
13 | position: absolute;
14 | bottom: -50px;
15 | left: 20px;
16 | background-color: $background-dark-light;
17 | width: 150px;
18 | height: 150px;
19 | border: 4px solid $border-dark;
20 | border-radius: 50%;
21 | background-size: cover;
22 | background-position: center;
23 | background-repeat: no-repeat;
24 | }
25 |
26 | .options {
27 | position: absolute;
28 | bottom: -50px;
29 | right: 15px;
30 |
31 | button {
32 | border-radius: 50px;
33 | background-color: $primary;
34 | width: 150px;
35 |
36 | &:hover {
37 | background-color: $primary;
38 | opacity: 0.6;
39 | }
40 |
41 | &.unfollow {
42 | &:hover {
43 | border-color: $danger;
44 | background-color: $danger;
45 |
46 | &::before {
47 | content: "Deja de seguir";
48 | }
49 |
50 | span {
51 | display: none;
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/User/BannerAvatar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./BannerAvatar";
2 |
--------------------------------------------------------------------------------
/src/components/User/EditUserForm/EditUserForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { Form, Button, Row, Col, Spinner } from "react-bootstrap";
3 | import DatePicker from "react-datepicker";
4 | import es from "date-fns/locale/es";
5 | import { useDropzone } from "react-dropzone";
6 | import { toast } from "react-toastify";
7 | import { API_HOST } from "../../../utils/constant";
8 | import { Camera } from "../../../utils/Icons";
9 | import {
10 | uploadBannerApi,
11 | uploadAvatarApi,
12 | updateInfoApi,
13 | } from "../../../api/user";
14 |
15 | import "./EditUserForm.scss";
16 |
17 | export default function EditUserForm(props) {
18 | const { user, setShowModal } = props;
19 | const [formData, setFormData] = useState(initialValue(user));
20 | const [bannerUrl, setBannerUrl] = useState(
21 | user?.banner ? `${API_HOST}/obtenerBanner?id=${user.id}` : null
22 | );
23 | const [bannerFile, setBannerFile] = useState(null);
24 | const [avatarUrl, setAvatarUrl] = useState(
25 | user?.avatar ? `${API_HOST}/obtenerAvatar?id=${user.id}` : null
26 | );
27 | const [avatarFile, setAvatarFile] = useState(null);
28 | const [loading, setLoading] = useState(false);
29 |
30 | // eslint-disable-next-line react-hooks/exhaustive-deps
31 | const onDropBanner = useCallback((acceptedFile) => {
32 | const file = acceptedFile[0];
33 | setBannerUrl(URL.createObjectURL(file));
34 | setBannerFile(file);
35 | });
36 | const {
37 | getRootProps: getRootBannerProps,
38 | getInputProps: getInputBannerProps,
39 | } = useDropzone({
40 | accept: "image/jpeg, image/png",
41 | noKeyboard: true,
42 | multiple: false,
43 | onDrop: onDropBanner,
44 | });
45 |
46 | // eslint-disable-next-line react-hooks/exhaustive-deps
47 | const onDropAvatar = useCallback((acceptedFile) => {
48 | const file = acceptedFile[0];
49 | setAvatarUrl(URL.createObjectURL(file));
50 | setAvatarFile(file);
51 | });
52 | const {
53 | getRootProps: getRootAvatarProps,
54 | getInputProps: getInputAvatarProps,
55 | } = useDropzone({
56 | accept: "image/jpeg, image/png",
57 | noKeyboard: true,
58 | multiple: false,
59 | onDrop: onDropAvatar,
60 | });
61 |
62 | const onChange = (e) => {
63 | setFormData({ ...formData, [e.target.name]: e.target.value });
64 | };
65 |
66 | const onSubmit = async (e) => {
67 | e.preventDefault();
68 | setLoading(true);
69 |
70 | if (bannerFile) {
71 | await uploadBannerApi(bannerFile).catch(() => {
72 | toast.error("Error al subir el nuevo banner");
73 | });
74 | }
75 | if (avatarFile) {
76 | await uploadAvatarApi(avatarFile).catch(() => {
77 | toast.error("Error al subir el nuevo avatar");
78 | });
79 | }
80 |
81 | await updateInfoApi(formData)
82 | .then(() => {
83 | setShowModal(false);
84 | })
85 | .catch(() => {
86 | toast.error("Error al actualizar los datos");
87 | });
88 |
89 | setLoading(false);
90 | window.location.reload();
91 | };
92 |
93 | return (
94 |
95 |
100 |
101 |
102 |
103 |
104 |
109 |
110 |
111 |
112 |
113 |
115 |
116 |
117 |
124 |
125 |
126 |
133 |
134 |
135 |
136 |
137 |
138 |
147 |
148 |
149 |
150 |
157 |
158 |
159 |
160 |
165 | setFormData({ ...formData, fechaNacimiento: value })
166 | }
167 | />
168 |
169 |
170 |
173 |
174 |
175 | );
176 | }
177 |
178 | function initialValue(user) {
179 | return {
180 | nombre: user.nombre || "",
181 | apellidos: user.apellidos || "",
182 | biografia: user.biografia || "",
183 | ubicacion: user.ubicacion || "",
184 | sitioWeb: user.sitioWeb || "",
185 | fechaNacimiento: user.fechaNacimiento || "",
186 | };
187 | }
188 |
--------------------------------------------------------------------------------
/src/components/User/EditUserForm/EditUserForm.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .edit-user-form {
4 | .banner {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | height: 180px;
9 | background-color: $background-dark-light;
10 | background-size: cover;
11 | background-position: center;
12 | background-repeat: no-repeat;
13 | padding: -20px !important;
14 |
15 | &:hover {
16 | cursor: pointer;
17 | }
18 |
19 | svg {
20 | width: 30px;
21 | fill: $font-light;
22 | &:hover {
23 | fill: $font-dark;
24 | }
25 | }
26 | }
27 |
28 | .avatar {
29 | position: relative;
30 | top: -50px;
31 | left: 20px;
32 | background-color: $background-dark-light;
33 | width: 120px;
34 | height: 120px;
35 | border: 4px solid $border-dark;
36 | border-radius: 50%;
37 | background-size: cover;
38 | background-position: center;
39 | background-repeat: no-repeat;
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 |
44 | &:hover {
45 | cursor: pointer;
46 | }
47 |
48 | svg {
49 | width: 30px;
50 | fill: $font-light;
51 | &:hover {
52 | fill: $font-dark;
53 | }
54 | }
55 | }
56 |
57 | form {
58 | padding: 0 20px 20px 20px;
59 |
60 | .btn-submit {
61 | position: absolute;
62 | top: -44px;
63 | right: 25px;
64 | border-radius: 50px;
65 | font-size: 16px;
66 | padding: 4px 15px 4px 15px;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/User/EditUserForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./EditUserForm";
2 |
--------------------------------------------------------------------------------
/src/components/User/InfoUser/InfoUser.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import moment from "moment";
3 | import localization from "moment/locale/es";
4 | import { Location, Link, DateBirth } from "../../../utils/Icons";
5 |
6 | import "./InfoUser.scss";
7 |
8 | export default function InfoUser(props) {
9 | const { user } = props;
10 |
11 | return (
12 |
13 |
14 | {user?.nombre} {user?.apellidos}
15 |
16 |
{user?.email}
17 | {user?.biografia &&
{user.biografia}
}
18 |
19 |
20 | {user?.ubicacion && (
21 |
22 |
23 | {user.ubicacion}
24 |
25 | )}
26 | {user?.sitioWeb && (
27 |
33 | {user.sitioWeb}
34 |
35 | )}
36 | {user?.fechaNacimiento && (
37 |
38 |
39 | {moment(user.fechaNacimiento)
40 | .locale("es", localization)
41 | .format("LL")}
42 |
43 | )}
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/User/InfoUser/InfoUser.scss:
--------------------------------------------------------------------------------
1 | @import "../../../scss/index.scss";
2 |
3 | .info-user {
4 | margin: 20px;
5 |
6 | * {
7 | margin: 0;
8 | }
9 |
10 | svg {
11 | width: 15px;
12 | height: 15px;
13 | fill: $font-grey;
14 | }
15 |
16 | .name {
17 | font-size: 18px;
18 | font-weight: bold;
19 | }
20 | .email {
21 | font-size: 14px;
22 | color: $font-grey;
23 | }
24 |
25 | .description {
26 | margin: 10px 0;
27 | white-space: pre-line;
28 | }
29 |
30 | .more-info {
31 | display: flex;
32 | align-items: center;
33 |
34 | p,
35 | a {
36 | color: $font-grey;
37 | padding-right: 20px;
38 |
39 | svg {
40 | margin-right: 10px;
41 | }
42 | }
43 |
44 | a {
45 | color: $primary;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/User/InfoUser/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./InfoUser";
2 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { AuthContext } from "../utils/contexts";
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 |
6 | import "react-datepicker/dist/react-datepicker.css";
7 | import "react-toastify/dist/ReactToastify.css";
8 | import "bootstrap/dist/css/bootstrap.min.css";
9 | import "./index.scss";
10 |
11 | ReactDOM.render(, document.getElementById("root"));
12 |
13 | // If you want your app to work offline and load faster, you can change
14 | // unregister() to register() below. Note this comes with some pitfalls.
15 | // Learn more about service workers: https://bit.ly/CRA-PWA
16 | serviceWorker.unregister();
17 |
--------------------------------------------------------------------------------
/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-dark;
11 | color: $font-light;
12 | }
13 |
14 | code {
15 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
16 | monospace;
17 | }
18 |
19 | .btn {
20 | box-shadow: none !important;
21 | }
22 |
23 | .btn.btn-primary {
24 | background-color: $primary;
25 | border-color: $primary;
26 | &:hover {
27 | background-color: rgba(26, 145, 218, 1);
28 | }
29 | }
30 |
31 | .btn.btn-outline-primary {
32 | border-color: $border-primary;
33 | color: $primary;
34 | &:hover,
35 | &:active,
36 | &:focus {
37 | background-color: rgba(29, 261, 241, 0.1) !important;
38 | }
39 | }
40 |
41 | input.form-control,
42 | textarea.form-control {
43 | background-color: $background-dark-light;
44 | border-radius: 0;
45 | border: 0;
46 | border-bottom: 1px solid $border-grey;
47 | color: $font-light !important;
48 |
49 | &:focus {
50 | background-color: $background-dark-light;
51 | box-shadow: none;
52 | border-bottom: 1px solid $border-primary;
53 | }
54 | }
55 |
56 | .react-datepicker-wrapper {
57 | width: 100%;
58 | height: 38px;
59 |
60 | .react-datepicker__input-container {
61 | height: 100%;
62 |
63 | input {
64 | background-color: $background-dark-light;
65 | border-radius: 0;
66 | border: 0;
67 | border-bottom: 1px solid $border-grey;
68 | color: $font-light !important;
69 | width: 100%;
70 | height: 100%;
71 | padding: 0 12px;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/layout/BasicLayout/BasicLayout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import LeftMenu from "../../components/LeftMenu";
4 |
5 | import "./BasicLayout.scss";
6 |
7 | export default function BasicLayout(props) {
8 | const { className, setRefreshCheckLogin, children } = props;
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/layout/BasicLayout/BasicLayout.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .basic-layout {
4 | &__menu {
5 | padding-right: 0 !important;
6 | }
7 |
8 | &__content {
9 | height: 100vh;
10 | overflow-y: scroll;
11 | padding: 0 !important;
12 | border-right: 1px solid $border-grey;
13 | &::-webkit-scrollbar {
14 | display: none;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/layout/BasicLayout/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./BasicLayout";
2 |
--------------------------------------------------------------------------------
/src/page/Error404/Error404.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import Error404Image from "../../assets/png/error-404.png";
4 | import Logo from "../../assets/png/logo.png";
5 |
6 | import "./Error404.scss";
7 |
8 | export default function Error404() {
9 | return (
10 |
11 |

12 |

13 |
Volver al inicio
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/page/Error404/Error404.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .error404 {
4 | min-height: 100vh;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | flex-direction: column;
9 |
10 | img {
11 | &:first-of-type {
12 | width: 150px;
13 | margin-bottom: 30px;
14 | }
15 | &:last-of-type {
16 | width: 600px;
17 | }
18 | }
19 |
20 | a {
21 | margin-top: 30px;
22 | color: $font-light;
23 | font-size: 30px;
24 | font-weight: bold;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/page/Error404/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Error404";
2 |
--------------------------------------------------------------------------------
/src/page/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Button, Spinner } from "react-bootstrap";
3 | import BasicLayout from "../../layout/BasicLayout";
4 | import ListTweets from "../../components/ListTweets";
5 | import { getTweetsFollowersApi } from "../../api/tweet";
6 |
7 | import "./Home.scss";
8 |
9 | export default function Home(props) {
10 | const { setRefreshCheckLogin } = props;
11 | const [tweets, setTweets] = useState(null);
12 | const [page, setPage] = useState(1);
13 | const [loadingTweets, setLoadingTweets] = useState(false);
14 |
15 | useEffect(() => {
16 | getTweetsFollowersApi(page)
17 | .then((response) => {
18 | if (!tweets && response) {
19 | setTweets(formatModel(response));
20 | } else {
21 | if (!response) {
22 | setLoadingTweets(0);
23 | } else {
24 | const data = formatModel(response);
25 | setTweets([...tweets, ...data]);
26 | setLoadingTweets(false);
27 | }
28 | }
29 | })
30 | .catch(() => {});
31 | // eslint-disable-next-line react-hooks/exhaustive-deps
32 | }, [page]);
33 |
34 | const moreData = () => {
35 | setLoadingTweets(true);
36 | setPage(page + 1);
37 | };
38 |
39 | return (
40 |
41 |
42 |
Inicio
43 |
44 | {tweets && }
45 |
62 |
63 | );
64 | }
65 |
66 | function formatModel(tweets) {
67 | const tweetsTemp = [];
68 | tweets.forEach((tweet) => {
69 | tweetsTemp.push({
70 | _id: tweet._id,
71 | userId: tweet.userRelationId,
72 | mensaje: tweet.Tweet.mensaje,
73 | fecha: tweet.Tweet.fecha,
74 | });
75 | });
76 | return tweetsTemp;
77 | }
78 |
--------------------------------------------------------------------------------
/src/page/Home/Home.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .home {
4 | &__title {
5 | border-bottom: 1px solid $border-grey;
6 | padding: 15px;
7 |
8 | h2 {
9 | font-size: 22px;
10 | margin-bottom: 0;
11 | font-weight: bold;
12 | }
13 | }
14 |
15 | .load-more.btn.btn-primary {
16 | display: flex;
17 | margin: 0 auto;
18 | margin-top: 10px;
19 | margin-bottom: 10px;
20 | background-color: transparent;
21 | border: 0;
22 |
23 | &:hover,
24 | &focus {
25 | background-color: transparent !important;
26 | color: $primary;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/page/Home/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Home";
2 |
--------------------------------------------------------------------------------
/src/page/SignInSingUp/SignInSingUp.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Container, Row, Col, Button } from "react-bootstrap";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import {
5 | faSearch,
6 | faUsers,
7 | faComment
8 | } from "@fortawesome/free-solid-svg-icons";
9 | import BasicModal from "../../components/Modal/BasicModal";
10 | import SignUpForm from "../../components/SignUpForm";
11 | import SignInForm from "../../components/SignInForm";
12 | import LogoTwittor from "../../assets/png/logo.png";
13 | import LogoWhiteTwittor from "../../assets/png/logo-white.png";
14 |
15 | import "./SignInSingUp.scss";
16 |
17 | export default function SignInSingUp(props) {
18 | const { setRefreshCheckLogin } = props;
19 | const [showModal, setShowModal] = useState(false);
20 | const [contentModal, setContentModal] = useState(null);
21 |
22 | const openModal = content => {
23 | setShowModal(true);
24 | setContentModal(content);
25 | };
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 | {contentModal}
42 |
43 | >
44 | );
45 | }
46 |
47 | function LeftComponent() {
48 | return (
49 |
50 |
51 |
52 |
53 |
54 | Sigue lo que te interesa.
55 |
56 |
57 |
58 | Entérate de qué está hablando la gente.
59 |
60 |
61 |
62 | Únete a la conversación.
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | function RightComponent(props) {
70 | const { openModal, setShowModal, setRefreshCheckLogin } = props;
71 |
72 | return (
73 |
74 |
75 |

76 |
Mira lo que está pasando en el mundo en este momento
77 |
Únete a Twittor hot mimso.
78 |
84 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/page/SignInSingUp/SignInSingUp.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .signin-signup {
4 | &__left {
5 | overflow: hidden;
6 | min-height: 100vh;
7 | background-color: $primary-light;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 |
12 | img {
13 | position: absolute;
14 | right: -35%;
15 | }
16 |
17 | div {
18 | z-index: 1;
19 |
20 | h2 {
21 | display: flex;
22 | align-items: center;
23 | font-size: 30px;
24 | margin-bottom: 40px;
25 | font-weight: bold;
26 |
27 | svg {
28 | margin-right: 20px;
29 | font-size: 30px;
30 | width: 40px !important;
31 | }
32 | }
33 | }
34 | }
35 |
36 | &__right {
37 | display: flex;
38 | align-items: center;
39 |
40 | > div {
41 | width: 500px;
42 | margin: 0 auto;
43 |
44 | img {
45 | width: 100px;
46 | margin-bottom: 20px;
47 | }
48 |
49 | h2 {
50 | font-weight: bold;
51 | font-size: 40px;
52 | margin-bottom: 70px;
53 | }
54 |
55 | h3 {
56 | font-weight: bold;
57 | font-size: 20px;
58 | margin-bottom: 30px;
59 | }
60 |
61 | button {
62 | display: block;
63 | width: 100%;
64 | border-radius: 50px;
65 | font-size: 20px;
66 | font-weight: bold;
67 | padding: 10px 0;
68 |
69 | &:first-of-type {
70 | margin-bottom: 20px;
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/page/SignInSingUp/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SignInSingUp";
2 |
--------------------------------------------------------------------------------
/src/page/User/User.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Button, Spinner } from "react-bootstrap";
3 | import { withRouter } from "react-router-dom";
4 | import { toast } from "react-toastify";
5 | import useAuth from "../../hooks/useAuth";
6 | import BasicLayout from "../../layout/BasicLayout";
7 | import BannerAvatar from "../../components/User/BannerAvatar";
8 | import InfoUser from "../../components/User/InfoUser";
9 | import ListTweets from "../../components/ListTweets";
10 | import { getUserApi } from "../../api/user";
11 | import { getUserTweetsApi } from "../../api/tweet";
12 |
13 | import "./User.scss";
14 |
15 | function User(props) {
16 | const { match, setRefreshCheckLogin } = props;
17 | const [user, setUser] = useState(null);
18 | const [tweets, setTweets] = useState(null);
19 | const [page, setPage] = useState(1);
20 | const [loadingTweets, setLoadingTweets] = useState(false);
21 | const { params } = match;
22 | const loggedUser = useAuth();
23 |
24 | useEffect(() => {
25 | getUserApi(params.id)
26 | .then((response) => {
27 | if (!response) toast.error("El usuario que has visitado no existe");
28 | setUser(response);
29 | })
30 | .catch(() => {
31 | toast.error("El usuario que has visitado no existe");
32 | });
33 | }, [params]);
34 |
35 | useEffect(() => {
36 | getUserTweetsApi(params.id, 1)
37 | .then((response) => {
38 | setTweets(response);
39 | })
40 | .catch(() => {
41 | setTweets([]);
42 | });
43 | }, [params]);
44 |
45 | const moreData = () => {
46 | const pageTemp = page + 1;
47 | setLoadingTweets(true);
48 |
49 | getUserTweetsApi(params.id, pageTemp).then((response) => {
50 | if (!response) {
51 | setLoadingTweets(0);
52 | } else {
53 | setTweets([...tweets, ...response]);
54 | setPage(pageTemp);
55 | setLoadingTweets(false);
56 | }
57 | });
58 | };
59 |
60 | return (
61 |
62 |
63 |
64 | {user ? `${user.nombre} ${user.apellidos}` : "Este usuario no existe"}
65 |
66 |
67 |
68 |
69 |
70 |
Tweets
71 | {tweets && }
72 |
85 |
86 |
87 | );
88 | }
89 |
90 | export default withRouter(User);
91 |
--------------------------------------------------------------------------------
/src/page/User/User.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .user {
4 | &__title {
5 | border-bottom: 1px solid $border-grey;
6 | padding: 15px;
7 |
8 | h2 {
9 | font-size: 22px;
10 | margin-bottom: 0;
11 | font-weight: bold;
12 | }
13 | }
14 |
15 | &__tweets {
16 | h3 {
17 | padding: 15px;
18 | font-size: 18px;
19 | border-bottom: 1px solid $border-grey;
20 | }
21 |
22 | > .btn.btn-primary {
23 | display: flex;
24 | margin: 0 auto;
25 | margin-top: 10px;
26 | margin-bottom: 10px;
27 | background-color: transparent;
28 | border: 0;
29 |
30 | &:hover,
31 | &:focus {
32 | background-color: transparent !important;
33 | color: $primary;
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/page/User/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./User";
2 |
--------------------------------------------------------------------------------
/src/page/Users/Users.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Spinner, ButtonGroup, Button } from "react-bootstrap";
3 | import { withRouter } from "react-router-dom";
4 | import queryString from "query-string";
5 | import { isEmpty } from "lodash";
6 | import { useDebouncedCallback } from "use-debounce";
7 | import BasicLayout from "../../layout/BasicLayout";
8 | import ListUsers from "../../components/ListUsers";
9 | import { getFollowsApi } from "../../api/follow";
10 |
11 | import "./Users.scss";
12 |
13 | function Users(props) {
14 | const { setRefreshCheckLogin, location, history } = props;
15 | const [users, setUsers] = useState(null);
16 | const params = useUsersQuery(location);
17 | const [typeUser, setTypeUser] = useState(params.type || "follow");
18 | const [btnLoading, setBtnLoading] = useState(false);
19 |
20 | const [onSearch] = useDebouncedCallback((value) => {
21 | setUsers(null);
22 | history.push({
23 | search: queryString.stringify({ ...params, search: value, page: 1 }),
24 | });
25 | }, 200);
26 |
27 | useEffect(() => {
28 | getFollowsApi(queryString.stringify(params))
29 | .then((response) => {
30 | // eslint-disable-next-line eqeqeq
31 | if (params.page == 1) {
32 | if (isEmpty(response)) {
33 | setUsers([]);
34 | } else {
35 | setUsers(response);
36 | }
37 | } else {
38 | if (!response) {
39 | setBtnLoading(0);
40 | } else {
41 | setUsers([...users, ...response]);
42 | setBtnLoading(false);
43 | }
44 | }
45 | })
46 | .catch(() => {
47 | setUsers([]);
48 | });
49 | // eslint-disable-next-line react-hooks/exhaustive-deps
50 | }, [location]);
51 |
52 | const onChangeType = (type) => {
53 | setUsers(null);
54 | if (type === "new") {
55 | setTypeUser("new");
56 | } else {
57 | setTypeUser("follow");
58 | }
59 | history.push({
60 | search: queryString.stringify({ type: type, page: 1, search: "" }),
61 | });
62 | };
63 |
64 | const moreData = () => {
65 | setBtnLoading(true);
66 | const newPage = parseInt(params.page) + 1;
67 | history.push({
68 | search: queryString.stringify({ ...params, page: newPage }),
69 | });
70 | };
71 |
72 | return (
73 |
78 |
79 |
Usuarios
80 | onSearch(e.target.value)}
84 | />
85 |
86 |
87 |
88 |
94 |
100 |
101 |
102 | {!users ? (
103 |
104 |
105 | Buscando usuarios
106 |
107 | ) : (
108 | <>
109 |
110 |
123 | >
124 | )}
125 |
126 | );
127 | }
128 |
129 | function useUsersQuery(location) {
130 | const { page = 1, type = "follow", search } = queryString.parse(
131 | location.search
132 | );
133 |
134 | return { page, type, search };
135 | }
136 |
137 | export default withRouter(Users);
138 |
--------------------------------------------------------------------------------
/src/page/Users/Users.scss:
--------------------------------------------------------------------------------
1 | @import "../../scss/index.scss";
2 |
3 | .users {
4 | &__title {
5 | padding: 15px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 |
10 | h2 {
11 | font-size: 22px;
12 | margin-bottom: 0;
13 | font-weight: bold;
14 | }
15 |
16 | input {
17 | width: 50%;
18 | border-radius: 50px;
19 | border: 0;
20 | background-color: $background-dark-light;
21 | padding: 5px 20px;
22 | color: $font-light;
23 |
24 | &:focus {
25 | outline: none;
26 | }
27 | }
28 | }
29 |
30 | &__options {
31 | display: flex !important;
32 |
33 | .btn.btn-primary {
34 | background-color: transparent !important;
35 | font-weight: bold;
36 | border: 0;
37 | border-bottom: 1px solid $border-grey;
38 | padding: 15px 0;
39 | border-radius: 0;
40 |
41 | &:hover {
42 | background-color: rgba(29, 161, 242, 0.1) !important;
43 | }
44 | &.active {
45 | border-bottom: 3px solid $primary;
46 | }
47 | }
48 | }
49 |
50 | &__loading {
51 | display: flex;
52 | align-items: center;
53 | justify-content: center;
54 | flex-direction: column;
55 | margin-top: 50px;
56 |
57 | > div {
58 | margin-bottom: 10px;
59 | }
60 | }
61 |
62 | .load-more.btn.btn-primary {
63 | display: flex;
64 | margin: 0 auto;
65 | margin-top: 10px;
66 | margin-bottom: 10px;
67 | background-color: transparent;
68 | border: 0;
69 |
70 | &:hover,
71 | &:focus {
72 | background-color: transparent !important;
73 | color: $primary;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/page/Users/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Users";
2 |
--------------------------------------------------------------------------------
/src/routes/Routing.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
3 | import { map } from "lodash";
4 | import configRouting from "./configRouting";
5 |
6 | export default function Routing(props) {
7 | const { setRefreshCheckLogin } = props;
8 |
9 | return (
10 |
11 |
12 | {map(configRouting, (route, index) => (
13 |
14 |
15 |
16 | ))}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/routes/configRouting.js:
--------------------------------------------------------------------------------
1 | import Home from "../page/Home";
2 | import User from "../page/User";
3 | import Users from "../page/Users";
4 | import Error404 from "../page/Error404";
5 |
6 | export default [
7 | {
8 | path: "/users",
9 | exact: true,
10 | page: Users,
11 | },
12 | {
13 | path: "/:id",
14 | exact: true,
15 | page: User,
16 | },
17 | {
18 | path: "/",
19 | exact: true,
20 | page: Home,
21 | },
22 | {
23 | path: "*",
24 | page: Error404,
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/src/scss/_colors.scss:
--------------------------------------------------------------------------------
1 | $primary: #1da0f2;
2 | $primary-light: #71c9f8;
3 | $primary-grey: #8799a6;
4 | $primary-dark-light: #192734;
5 | $primary-dark: #15212b;
6 |
7 | $font-primary: $primary;
8 | $font-light: #fff;
9 | $font-grey: $primary-grey;
10 | $font-dark: #000;
11 |
12 | $border-primary: $primary;
13 | $border-light: #fff;
14 | $border-grey: #38444d;
15 | $border-dark: $primary-dark;
16 |
17 | $background-light: #fff;
18 | $background-grey-dark: $primary-dark;
19 | $background-dark-light: $primary-dark-light;
20 |
21 | $danger: #e0245e;
22 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import "./colors";
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/Icons.js:
--------------------------------------------------------------------------------
1 | export { ReactComponent as Location } from "../assets/svg/location.svg";
2 | export { ReactComponent as Link } from "../assets/svg/link.svg";
3 | export { ReactComponent as DateBirth } from "../assets/svg/date-birth.svg";
4 | export { ReactComponent as Close } from "../assets/svg/close.svg";
5 | export { ReactComponent as Camera } from "../assets/svg/camera.svg";
6 |
--------------------------------------------------------------------------------
/src/utils/constant.js:
--------------------------------------------------------------------------------
1 | export const API_HOST = "https://twittor-golang-react-server.herokuapp.com";
2 | export const TOKEN = "token";
3 |
--------------------------------------------------------------------------------
/src/utils/contexts.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export const AuthContext = createContext();
4 |
--------------------------------------------------------------------------------
/src/utils/functions.js:
--------------------------------------------------------------------------------
1 | export function replaceURLWithHTMLLinks(text) {
2 | // eslint-disable-next-line no-useless-escape
3 | var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
4 | return text?.replace(exp, "$1");
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/validations.js:
--------------------------------------------------------------------------------
1 | export function isEmailValid(email) {
2 | // eslint-disable-next-line no-useless-escape
3 | const emailValid = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
4 | return emailValid.test(String(email).toLowerCase());
5 | }
6 |
--------------------------------------------------------------------------------