├── backend ├── .env.example ├── Dockerfile ├── src │ ├── tweets │ │ ├── model │ │ │ └── Tweet.js │ │ ├── typedefs.js │ │ └── resolvers.js │ ├── server.js │ └── app.js ├── package.json └── yarn.lock ├── using.gif ├── twitter-clone.png ├── frontend ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── frontend.tar.gz ├── src │ ├── components │ │ ├── Feed │ │ │ ├── styles.js │ │ │ ├── Header │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ │ ├── WhatsHappening │ │ │ │ ├── ActionBar │ │ │ │ │ ├── index.js │ │ │ │ │ └── styles.js │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── Tweets │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ ├── TweetButton │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Discover │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── Menu │ │ │ ├── index.js │ │ │ └── styles.js │ ├── global-styles.js │ ├── config │ │ ├── locale-config.js │ │ └── graphql-config.js │ ├── index.js │ ├── pages │ │ ├── Main │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── Login │ │ │ ├── index.js │ │ │ └── styles.js │ ├── repository │ │ └── index.js │ ├── App.js │ └── routes.js ├── package.json └── README.md ├── Makefile ├── .gitignore └── README.md /backend/.env.example: -------------------------------------------------------------------------------- 1 | DB_HOST=mongodb://localhost:27017 -------------------------------------------------------------------------------- /using.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/using.gif -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | COPY . . 3 | RUN npm install 4 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /twitter-clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/twitter-clone.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/frontend.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/frontend/frontend.tar.gz -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusestevam/twitter-clone/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/components/Feed/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { grey } from '../../global-styles'; 3 | 4 | export const Container = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | background-color: ${grey}; 8 | `; 9 | -------------------------------------------------------------------------------- /frontend/src/global-styles.js: -------------------------------------------------------------------------------- 1 | export const blue = '#1da1f2'; 2 | export const darkBlue = '#1987c6'; 3 | export const lightBlue = 'rgba(29, 161, 242, 0.1)'; 4 | export const grey = '#E6ECF0'; 5 | export const lightGrey = '#f7f7f7'; 6 | export const darkGrey = '#989da0'; 7 | -------------------------------------------------------------------------------- /backend/src/tweets/model/Tweet.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const TweetSchema = new mongoose.Schema( 4 | { 5 | author: String, 6 | user: String, 7 | message: String 8 | }, 9 | { timestamps: true } 10 | ); 11 | 12 | export default mongoose.model('Tweet', TweetSchema); 13 | -------------------------------------------------------------------------------- /frontend/src/components/TweetButton/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container } from './styles'; 4 | 5 | export default function TweetButton({ onClick }) { 6 | return ( 7 | 8 | Tweet 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/Feed/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container, Title, StarIcon } from './styles'; 4 | 5 | export default function Header() { 6 | return ( 7 | 8 | Home 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | cd frontend; yarn 4 | cd backend; yarn 5 | cd backend; docker build -t twitter-clone . 6 | docker run --name twitter-db -p 27017:27107 -d mongo:latest; 7 | .PHONY: up 8 | up: 9 | cd backend; docker run --name twitter-backend -d -p 8000:8000 twitter-clone 10 | cd frontend; yarn start -------------------------------------------------------------------------------- /frontend/src/config/locale-config.js: -------------------------------------------------------------------------------- 1 | import JavascriptTimeAgo from 'javascript-time-ago'; 2 | 3 | // The desired locales. 4 | import en from 'javascript-time-ago/locale/en'; 5 | import br from 'javascript-time-ago/locale/br'; 6 | 7 | // Initialize the desired locales. 8 | JavascriptTimeAgo.locale(en); 9 | JavascriptTimeAgo.locale(br); 10 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { ApolloProvider } from 'react-apollo'; 4 | 5 | import { client } from './config/graphql-config'; 6 | 7 | import App from './App'; 8 | import './config/locale-config.js'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /.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 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/src/pages/Main/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | flex-direction: row; 8 | `; 9 | export const Column = styled.div` 10 | display: flex; 11 | height: 100%; 12 | width: ${props => props.width}; 13 | background-color: ${props => props.color}; 14 | justify-content: ${props => (props.justifyRight ? 'flex-end' : 'flex-start')}; 15 | `; 16 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import httpServer from './app'; 2 | import mongoose from 'mongoose'; 3 | 4 | mongoose 5 | .connect(process.env.DB_HOST, { 6 | useNewUrlParser: true, 7 | useCreateIndex: true, 8 | useUnifiedTopology: true 9 | }) 10 | .then(() => { 11 | console.log('[DB] => Connected'); 12 | httpServer.listen({ port: process.env.PORT || 8000 }, () => { 13 | console.log('[SERVER] => Apollo Server on http://localhost:8000/graphql'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /backend/src/tweets/typedefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default gql` 4 | type Query { 5 | tweets: [Tweet!]! 6 | } 7 | 8 | type Subscription { 9 | newTweet: Tweet 10 | } 11 | 12 | type Mutation { 13 | createTweet(author: String!, user: String!, message: String!): Tweet! 14 | } 15 | 16 | type Tweet { 17 | id: ID! 18 | author: String! 19 | user: String! 20 | message: String! 21 | createdAt: String! 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node -r esm -r dotenv/config src/server.js" 8 | }, 9 | "dependencies": { 10 | "apollo-server": "^2.11.0", 11 | "apollo-server-express": "^2.11.0", 12 | "cors": "^2.8.5", 13 | "esm": "^3.2.25", 14 | "express": "^4.17.1", 15 | "mongoose": "^5.9.5" 16 | }, 17 | "devDependencies": { 18 | "dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/TweetButton/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { blue, darkBlue } from '../../global-styles'; 3 | 4 | export const Container = styled.button` 5 | height: 100%; 6 | border: none; 7 | cursor: pointer; 8 | width: 100%; 9 | border-radius: 50px; 10 | background-color: ${blue}; 11 | color: #fff; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | font-size: 16px; 16 | :hover { 17 | background-color: ${props => (props.dontHover ? '' : darkBlue)}; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /backend/src/app.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express'; 2 | import { createServer } from 'http'; 3 | import express from 'express'; 4 | import cors from 'cors'; 5 | 6 | import resolvers from './tweets/resolvers'; 7 | import typeDefs from './tweets/typedefs'; 8 | 9 | const app = express(); 10 | const server = new ApolloServer({ 11 | typeDefs, 12 | resolvers 13 | }); 14 | 15 | app.use(cors()); 16 | server.applyMiddleware({ app, path: '/graphql' }); 17 | 18 | const httpServer = createServer(app); 19 | server.installSubscriptionHandlers(httpServer); 20 | 21 | export default httpServer; 22 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/repository/index.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const GET_TWEETS = gql` 4 | query { 5 | tweets { 6 | id 7 | author 8 | user 9 | message 10 | createdAt 11 | } 12 | } 13 | `; 14 | 15 | export const NEW_TWEET = gql` 16 | subscription { 17 | newTweet { 18 | id 19 | author 20 | user 21 | message 22 | createdAt 23 | } 24 | } 25 | `; 26 | 27 | export const CREATE_TWEET = gql` 28 | mutation createTweet($author: String!, $user: String!, $message: String!) { 29 | createTweet(author: $author, user: $user, message: $message) { 30 | id 31 | } 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | 4 | import Routes from './routes'; 5 | 6 | export const GlobalStyles = createGlobalStyle` 7 | * { 8 | margin: 0; 9 | padding: 0; 10 | outline: 0; 11 | box-sizing: border-box; 12 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 13 | transition: 0.05s; 14 | } 15 | 16 | html, 17 | body, 18 | #root { 19 | height: 100%; 20 | } 21 | 22 | body { 23 | background: #fff; 24 | } 25 | `; 26 | 27 | function App() { 28 | return ( 29 | <> 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /frontend/src/pages/Main/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useEffect } from 'react'; 3 | import { Container, Column } from './styles'; 4 | 5 | import Menu from '../../components/Menu'; 6 | import Feed from '../../components/Feed'; 7 | import Discover from '../../components/Discover'; 8 | 9 | export default function Main() { 10 | useEffect(() => { 11 | document.title = 'Home / Twitter'; 12 | }, []); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/tweets/resolvers.js: -------------------------------------------------------------------------------- 1 | import Tweet from './model/Tweet'; 2 | import { PubSub } from 'apollo-server'; 3 | 4 | const pubsub = new PubSub(); 5 | const MESSAGE_CREATED = 'MESSAGE_CREATED'; 6 | 7 | export default { 8 | Query: { 9 | tweets: () => { 10 | return Tweet.find() 11 | .sort({ createdAt: -1 }) 12 | .limit(10); 13 | } 14 | }, 15 | Subscription: { 16 | newTweet: { 17 | subscribe: () => pubsub.asyncIterator(MESSAGE_CREATED) 18 | } 19 | }, 20 | Mutation: { 21 | createTweet: async (_, args) => { 22 | const tweet = await Tweet.create({ ...args }); 23 | pubsub.publish(MESSAGE_CREATED, { 24 | newTweet: tweet 25 | }); 26 | return tweet; 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/Feed/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { grey, blue, lightBlue } from '../../../global-styles'; 3 | import { Star } from 'styled-icons/boxicons-regular'; 4 | 5 | export const Container = styled.div` 6 | display: flex; 7 | width: 100%; 8 | height: 60px; 9 | align-items: center; 10 | background-color: #fff; 11 | justify-content: space-between; 12 | border-bottom: 1px solid ${grey}; 13 | border-left: 1px solid ${grey}; 14 | border-right: 1px solid ${grey}; 15 | `; 16 | 17 | export const Title = styled.h3` 18 | font-weight: bold; 19 | margin-left: 20px; 20 | `; 21 | 22 | export const StarIcon = styled(Star).attrs({ 23 | height: '30px' 24 | })` 25 | color: ${blue}; 26 | padding: 5px 10px; 27 | margin-right: 10px; 28 | :hover { 29 | background-color: ${lightBlue}; 30 | cursor: pointer; 31 | border-radius: 50%; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /frontend/src/components/Feed/WhatsHappening/ActionBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Container, 5 | LeftIconsContainer, 6 | RightIconsContainer, 7 | ImageIcon, 8 | GifIcon, 9 | StatsIcon, 10 | SmileIcon, 11 | ButtonContainer, 12 | PlusIcon, 13 | Divider, 14 | Circle 15 | } from './styles'; 16 | 17 | import TweetButton from '../../../TweetButton'; 18 | 19 | export default function ActionBar({ handleTweetButton }) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/config/graphql-config.js: -------------------------------------------------------------------------------- 1 | import { getMainDefinition } from 'apollo-utilities'; 2 | import { ApolloLink, split } from 'apollo-link'; 3 | import { HttpLink } from 'apollo-link-http'; 4 | import { WebSocketLink } from 'apollo-link-ws'; 5 | import { InMemoryCache } from 'apollo-cache-inmemory'; 6 | import { ApolloClient } from 'apollo-client'; 7 | 8 | const httpLink = new HttpLink({ 9 | uri: 'http://0.0.0.0:8000/graphql' 10 | }); 11 | 12 | const wsLink = new WebSocketLink({ 13 | uri: `ws://0.0.0.0:8000/graphql`, 14 | options: { 15 | reconnect: true 16 | } 17 | }); 18 | 19 | const terminatingLink = split( 20 | ({ query }) => { 21 | const { kind, operation } = getMainDefinition(query); 22 | return kind === 'OperationDefinition' && operation === 'subscription'; 23 | }, 24 | wsLink, 25 | httpLink 26 | ); 27 | 28 | const link = ApolloLink.from([terminatingLink]); 29 | const cache = new InMemoryCache(); 30 | 31 | export const client = new ApolloClient({ 32 | link, 33 | cache 34 | }); 35 | -------------------------------------------------------------------------------- /frontend/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, BrowserRouter, Redirect } from 'react-router-dom'; 3 | 4 | import Main from './pages/Main'; 5 | import Login from './pages/Login'; 6 | 7 | function isAuthenticated() { 8 | const username = sessionStorage.getItem('username'); 9 | const name = sessionStorage.getItem('name'); 10 | return username && name; 11 | } 12 | 13 | const PrivateRoute = ({ component: Component, ...rest }) => { 14 | return ( 15 | 18 | isAuthenticated() ? : 19 | } 20 | /> 21 | ); 22 | }; 23 | 24 | export default function Routes() { 25 | return ( 26 | 27 | 28 | 32 | isAuthenticated() ? : 33 | } 34 | /> 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/Feed/WhatsHappening/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darkGrey, grey } from '../../../global-styles'; 3 | import { UserCircle } from 'styled-icons/boxicons-solid'; 4 | 5 | export const Container = styled.div` 6 | width: 100%; 7 | height: 150px; 8 | background-color: #fff; 9 | display: flex; 10 | border-left: 1px solid ${grey}; 11 | border-right: 1px solid ${grey}; 12 | `; 13 | 14 | export const ProfileContainer = styled.div` 15 | height: 100%; 16 | width: 10%; 17 | display: flex; 18 | justify-content: center; 19 | `; 20 | 21 | export const ProfileIcon = styled(UserCircle).attrs({ 22 | height: '70px' 23 | })` 24 | color: ${darkGrey}; 25 | padding: 10px; 26 | margin: 20px 0px; 27 | `; 28 | 29 | export const RightContainer = styled.div` 30 | display: flex; 31 | width: 90%; 32 | height: 100%; 33 | flex-direction: column; 34 | `; 35 | 36 | export const TextArea = styled.textarea` 37 | width: 100%; 38 | height: 60%; 39 | padding: 20px 10px; 40 | border: none; 41 | font-size: 20px; 42 | overflow: hidden; 43 | resize: none; 44 | `; 45 | -------------------------------------------------------------------------------- /frontend/src/components/Feed/WhatsHappening/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Container, 5 | ProfileContainer, 6 | ProfileIcon, 7 | TextArea, 8 | RightContainer 9 | } from './styles'; 10 | import ActionBar from './ActionBar'; 11 | import { useState } from 'react'; 12 | import { useMutation } from 'react-apollo'; 13 | 14 | import { CREATE_TWEET } from '../../../repository'; 15 | 16 | export default function WhatsHappening() { 17 | const [inputValue, setInputValue] = useState(''); 18 | const [createTweet] = useMutation(CREATE_TWEET); 19 | 20 | function handleTweetButton() { 21 | const tweet = { 22 | author: sessionStorage.getItem('name'), 23 | user: sessionStorage.getItem('username'), 24 | message: inputValue 25 | }; 26 | const { author, user, message } = tweet; 27 | createTweet({ variables: { author, user, message } }); 28 | } 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 |