├── .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 | 5 | 6 | 7 | 8 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/assets/svg/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/svg/date-birth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/assets/svg/location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 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 | Twittor 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 | 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 | {`${user.nombre} 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 | Twittor 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 |
49 | 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 |
58 | 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 |
63 | 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 |
114 | 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 | Twittor 12 | Error404 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 | Twittor 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 | Twittor 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 | --------------------------------------------------------------------------------