├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── actions
│ ├── users.js
│ ├── authedUser.js
│ ├── shared.js
│ └── tweets.js
├── middleware
│ ├── index.js
│ └── logger.js
├── reducers
│ ├── authedUser.js
│ ├── users.js
│ ├── index.js
│ └── tweets.js
├── utils
│ ├── api.js
│ ├── helpers.js
│ └── _DATA.js
├── components
│ ├── Nav.js
│ ├── Dashboard.js
│ ├── TweetPage.js
│ ├── App.js
│ ├── NewTweet.js
│ └── Tweet.js
├── index.js
└── index.css
├── .gitignore
├── README.md
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psatler/twitter-clone-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/actions/users.js:
--------------------------------------------------------------------------------
1 | export const RECEIVE_USERS = "RECEIVE_USERS";
2 |
3 | export function receiveUsers(users) {
4 | return {
5 | type: RECEIVE_USERS,
6 | users
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/actions/authedUser.js:
--------------------------------------------------------------------------------
1 | export const SET_AUTHED_USER = "SET_AUTHED_USER";
2 |
3 | export function setAuthedUser(id) {
4 | return {
5 | type: SET_AUTHED_USER,
6 | id
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/middleware/index.js:
--------------------------------------------------------------------------------
1 | import thunk from "redux-thunk";
2 | import logger from "./logger";
3 |
4 | import { applyMiddleware } from "redux";
5 |
6 | export default applyMiddleware(thunk, logger);
7 |
--------------------------------------------------------------------------------
/src/reducers/authedUser.js:
--------------------------------------------------------------------------------
1 | import { SET_AUTHED_USER } from "../actions/authedUser";
2 |
3 | export default function authedUser(state = null, action) {
4 | switch (action.type) {
5 | case SET_AUTHED_USER:
6 | return action.id;
7 |
8 | default:
9 | return state;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/middleware/logger.js:
--------------------------------------------------------------------------------
1 | const logger = store => next => action => {
2 | console.group(action.type);
3 | console.log("The action: ", action);
4 | const returnValue = next(action);
5 | console.log("The new state: ", store.getState());
6 | console.groupEnd();
7 |
8 | return returnValue;
9 | };
10 |
11 | export default logger;
12 |
--------------------------------------------------------------------------------
/src/reducers/users.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_USERS } from "../actions/users";
2 |
3 | //creating the user reducer
4 | export default function users(state = {}, action) {
5 | switch (action.type) {
6 | case RECEIVE_USERS:
7 | return {
8 | ...state,
9 | ...action.users
10 | };
11 |
12 | default:
13 | return state;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import authedUser from "./authedUser";
4 | import users from "./users";
5 | import tweets from "./tweets";
6 |
7 | import { loadingBarReducer } from "react-redux-loading";
8 |
9 | export default combineReducers({
10 | authedUser,
11 | users,
12 | tweets,
13 | loadingBar: loadingBarReducer
14 | });
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/api.js:
--------------------------------------------------------------------------------
1 | import {
2 | _getUsers,
3 | _getTweets,
4 | _saveLikeToggle,
5 | _saveTweet,
6 | } from './_DATA.js'
7 |
8 | export function getInitialData () {
9 | return Promise.all([
10 | _getUsers(),
11 | _getTweets(),
12 | ]).then(([users, tweets]) => ({
13 | users,
14 | tweets,
15 | }))
16 | }
17 |
18 | export function saveLikeToggle (info) {
19 | return _saveLikeToggle(info)
20 | }
21 |
22 | export function saveTweet (info) {
23 | return _saveTweet(info)
24 | }
--------------------------------------------------------------------------------
/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | export default function Nav() {
5 | return (
6 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./components/App";
5 |
6 | //store stuff
7 | import { createStore } from "redux";
8 | import { Provider } from "react-redux";
9 | import reducer from "./reducers"; //importing the default export from index inside reducers folder
10 |
11 | //importing middlewares
12 | import middleware from "./middleware";
13 |
14 | const store = createStore(reducer, middleware);
15 |
16 | ReactDOM.render(
17 |
18 |
19 | ,
20 | document.getElementById("root")
21 | );
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Tweet-like app
2 |
3 | # How to run
4 |
5 | Do the following:
6 |
7 | ```
8 | git clone
9 | npm install
10 | npm start
11 | ```
12 |
13 | ## Store architecture
14 |
15 | The architecture of the redux store is as follows:
16 |
17 | ```js
18 | {
19 | tweets: {
20 | tweetId: { tweetId, authorId, timestamp, text, likes, replies, replyingTo},
21 | tweetId: { tweetId, authorId, timestamp, text, likes, replies, replyingTo}
22 | },
23 | users: {
24 | userId: {userId, userName, avatar, tweets array},
25 | userId: {userId, userName, avatar, tweets array}
26 | },
27 | authedUser: userId
28 | }
29 | ```
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitter-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "react-icons": "^3.2.2",
9 | "react-redux": "^5.1.0",
10 | "react-redux-loading": "^1.0.1",
11 | "react-router-dom": "^4.3.1",
12 | "react-scripts": "1.1.1",
13 | "redux": "^4.0.1",
14 | "redux-thunk": "^2.3.0"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/helpers.js:
--------------------------------------------------------------------------------
1 | export function formatDate (timestamp) {
2 | const d = new Date(timestamp)
3 | const time = d.toLocaleTimeString('en-US')
4 | return time.substr(0, 5) + time.slice(-2) + ' | ' + d.toLocaleDateString()
5 | }
6 |
7 | export function formatTweet (tweet, author, authedUser, parentTweet) {
8 | const { id, likes, replies, text, timestamp } = tweet
9 | const { name, avatarURL } = author
10 |
11 | return {
12 | name,
13 | id,
14 | timestamp,
15 | text,
16 | avatar: avatarURL,
17 | likes: likes.length,
18 | replies: replies.length,
19 | hasLiked: likes.includes(authedUser),
20 | parent: !parentTweet ? null : {
21 | author: parentTweet.author,
22 | id: parentTweet.id,
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/actions/shared.js:
--------------------------------------------------------------------------------
1 | import { getInitialData } from "../utils/api";
2 |
3 | //importing action creators
4 | import { receiveTweets } from "./tweets";
5 | import { receiveUsers } from "./users";
6 | import { setAuthedUser } from "./authedUser";
7 |
8 | //importing action creators of loading bar
9 | import { showLoading, hideLoading } from "react-redux-loading";
10 |
11 | //hard-coding an autherized user
12 | const AUTHED_ID = "tylermcginnis";
13 |
14 | export function handleInitialData() {
15 | return dispatch => {
16 | //before retrieving info, show loading bar
17 | dispatch(showLoading());
18 |
19 | return getInitialData().then(({ users, tweets }) => {
20 | dispatch(receiveTweets(tweets));
21 | dispatch(receiveUsers(users));
22 | dispatch(setAuthedUser(AUTHED_ID)); //hard-coded above
23 |
24 | //after everything has loaded, hide loading bar
25 | dispatch(hideLoading());
26 | });
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 |
4 | import Tweet from "./Tweet";
5 |
6 | class Dashboard extends Component {
7 | render() {
8 | console.log(this.props);
9 | return (
10 |
11 |
Your Timeline
12 |
13 | {this.props.tweetsIds.map(id => (
14 | -
15 | {/*
TWEET ID: {id}
*/}
16 |
17 |
18 | ))}
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | //destructuring tweets from state
26 | function mapStateToProps({ tweets }) {
27 | return {
28 | tweetsIds: Object.keys(tweets).sort(
29 | //sorting from the newest to the oldest tweet
30 | //If compareFunction(a, b) is greater than 0, sort b to an index lower than a, i.e. b comes first.
31 | (a, b) => tweets[b].timestamp - tweets[a].timestamp
32 | )
33 | };
34 | }
35 |
36 | export default connect(mapStateToProps)(Dashboard);
37 |
--------------------------------------------------------------------------------
/src/components/TweetPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 |
4 | import Tweet from "./Tweet";
5 | import NewTweet from "./NewTweet";
6 |
7 | class TweetPage extends Component {
8 | render() {
9 | const { id, replies } = this.props;
10 |
11 | return (
12 |
13 |
14 | {/* passing the parent tweet id */}
15 |
16 |
17 | {replies.length !== 0 &&
Replies
}
18 |
19 | {replies.map(replyId => (
20 | -
21 |
22 |
23 | ))}
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | function mapStateToProps({ authedUser, tweets, users }, props) {
31 | const { id } = props.match.params;
32 |
33 | return {
34 | id,
35 | replies: !tweets[id]
36 | ? [] //if doesn't exist a tweet with this id, the reply will be an empty array
37 | : tweets[id].replies.sort(
38 | (a, b) => tweets[b].timestamp - tweets[a].timestamp
39 | )
40 | };
41 | }
42 |
43 | export default connect(mapStateToProps)(TweetPage);
44 |
--------------------------------------------------------------------------------
/src/reducers/tweets.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_TWEETS, TOGGLE_TWEET, ADD_TWEET } from "../actions/tweets";
2 |
3 | export default function tweets(state = {}, action) {
4 | switch (action.type) {
5 | case RECEIVE_TWEETS:
6 | return {
7 | ...state,
8 | ...action.tweets
9 | };
10 |
11 | case ADD_TWEET:
12 | const { tweet } = action; //getting the newly added tweet from action
13 |
14 | let replyingTo = {};
15 | if (tweet.replyingTo !== null) {
16 | replyingTo = {
17 | [tweet.replyingTo]: {
18 | //id of the tweet we are replying to
19 | ...state[tweet.replyingTo], //everything that was before
20 | replies: state[tweet.replyingTo].replies.concat([tweet.id])
21 | }
22 | };
23 | }
24 |
25 | return {
26 | ...state,
27 | [action.tweet.id]: action.tweet,
28 | ...replyingTo
29 | };
30 |
31 | case TOGGLE_TWEET:
32 | return {
33 | ...state,
34 | [action.id]: {
35 | ...state[action.id],
36 | likes:
37 | action.hasLiked === true
38 | ? state[action.id].likes.filter(uid => uid !== action.authedUser) //if has liked, remove it (dislike it)
39 | : state[action.id].likes.concat([action.authedUser])
40 | }
41 | };
42 |
43 | default:
44 | return state;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import { BrowserRouter as Router, Route } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { handleInitialData } from "../actions/shared";
5 |
6 | import LoadingBar from "react-redux-loading"; //importing the loading bar given by react-redux-loading
7 |
8 | import Dashboard from "./Dashboard";
9 | import NewTweet from "./NewTweet";
10 | import TweetPage from "./TweetPage";
11 | import Nav from "./Nav";
12 |
13 | class App extends Component {
14 | componentDidMount() {
15 | this.props.dispatch(handleInitialData());
16 | }
17 |
18 | render() {
19 | return (
20 |
21 | {/* using a fragment so we don't add another element (div) to the DOM */}
22 |
23 |
24 |
25 |
26 | {this.props.loading === true ? null : (
27 |
28 |
29 |
30 |
31 |
32 | )}
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | function mapStateToProps({ authedUser }) {
41 | return {
42 | loading: authedUser === null
43 | };
44 | }
45 |
46 | export default connect(mapStateToProps)(App);
47 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/actions/tweets.js:
--------------------------------------------------------------------------------
1 | import { saveLikeToggle, saveTweet } from "../utils/api";
2 |
3 | //importing loading bar to show when we submit a tweet
4 | import { showLoading, hideLoading } from "react-redux-loading";
5 |
6 | export const RECEIVE_TWEETS = "RECEIVE_TWEETS";
7 | export const TOGGLE_TWEET = "TOGGLE_TWEET";
8 | export const ADD_TWEET = "ADD_TWEET";
9 |
10 | function addTweet(tweet) {
11 | return {
12 | type: ADD_TWEET,
13 | tweet
14 | };
15 | }
16 |
17 | //args: tweet text and the tweet that the newTweet is replying to, if any
18 | export function handleAddTweet(text, replyingTo) {
19 | //using getState to get the current state of our store
20 | return (dispatch, getState) => {
21 | const { authedUser } = getState();
22 | dispatch(showLoading()); //show loading bar
23 | return saveTweet({
24 | text,
25 | author: authedUser,
26 | replyingTo
27 | })
28 | .then(tweet => dispatch(addTweet(tweet)))
29 | .then(() => dispatch(hideLoading()));
30 | };
31 | }
32 |
33 | //action creator
34 | export function receiveTweets(tweets) {
35 | return {
36 | type: RECEIVE_TWEETS,
37 | tweets
38 | };
39 | }
40 |
41 | //functions for toggling tweet likes
42 | function toggleTweet({ id, authedUser, hasLiked }) {
43 | return {
44 | type: TOGGLE_TWEET,
45 | id,
46 | authedUser,
47 | hasLiked
48 | };
49 | }
50 | //assynchronous action creator (which is exported)
51 | export function handleToggleTweet(info) {
52 | return dispatch => {
53 | //using optimistic updates here, so first we toggle the tweet and then update the backend (server)
54 | dispatch(toggleTweet(info));
55 |
56 | //now update inside server and watch for possible errors
57 | return saveLikeToggle(info).catch(e => {
58 | console.warn("Error in handleToggleTweet ", e);
59 | dispatch(toggleTweet(info)); //resetting back to what it was initially
60 | alert("There was an error liking the tweet. Try again!");
61 | });
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/NewTweet.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { handleAddTweet } from "../actions/tweets";
4 |
5 | import { Redirect } from "react-router-dom";
6 |
7 | class NewTweet extends Component {
8 | state = {
9 | text: "",
10 | toHome: false
11 | };
12 |
13 | handleChange = e => {
14 | const text = e.target.value;
15 |
16 | this.setState(() => ({
17 | text
18 | }));
19 | };
20 |
21 | handleSubmit = e => {
22 | e.preventDefault();
23 | const { text } = this.state;
24 |
25 | //if we are at route /new, there is no id, so we are not replying to any tweet
26 | //if we are at route /tweet/:id, we are replying to that id
27 | const { dispatch, id } = this.props; //if id is a thing, it means we are replying to this id
28 |
29 | //todo: Add tweet to store
30 | dispatch(handleAddTweet(text, id));
31 | // console.log("New Tweet: ", text);
32 |
33 | //reset state to default
34 | this.setState(() => ({
35 | text: "",
36 | toHome: id ? false : true //if id is a thing, do not redirect, otherwise, you are at /new, so, after submit, redirect back to home
37 | }));
38 | };
39 |
40 | render() {
41 | const { text, toHome } = this.state;
42 | const tweetLeft = 280 - text.length;
43 |
44 | // redirect to home view if submitted from /new
45 | if (toHome === true) {
46 | return ;
47 | }
48 |
49 | return (
50 |
51 |
Compose new Tweet
52 |
68 |
69 | );
70 | }
71 | }
72 |
73 | export default connect()(NewTweet);
74 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | color: #252525;
6 | }
7 |
8 | a {
9 | text-decoration: none;
10 | color: #252525;
11 | }
12 |
13 | .container {
14 | /* border: 1px solid #dad7d7; */
15 | max-width: 1000px;
16 | margin: 0 auto;
17 | padding: 10px;
18 | }
19 |
20 | .btn {
21 | text-transform: uppercase;
22 | margin: 35px auto;
23 | padding: 10px;
24 | border: 1px solid rgba(0, 0, 0, 0.29);
25 | cursor: pointer;
26 | background: #fff;
27 | font-size: 16px;
28 | width: 250px;
29 | position: relative;
30 | }
31 |
32 | .btn:hover {
33 | border-color: rgba(0, 0, 0, 0.5);
34 | text-decoration: none;
35 | }
36 |
37 | .btn:focus {
38 | outline: 0;
39 | font-weight: 700;
40 | border-width: 2px;
41 | }
42 |
43 | .center {
44 | text-align: center;
45 | }
46 |
47 | .active {
48 | font-weight: bold;
49 | }
50 |
51 | .nav ul {
52 | display: flex;
53 | flex-direction: row;
54 | justify-content: flex-start;
55 | text-decoration: none;
56 | }
57 |
58 | .nav li:first-child {
59 | padding-left: 0;
60 | }
61 |
62 | ul {
63 | padding-left: 0;
64 | }
65 |
66 | li {
67 | list-style-type: none;
68 | padding: 10px;
69 | text-decoration: none;
70 | }
71 |
72 | .avatar {
73 | height: 50px;
74 | border-radius: 25px;
75 | margin: 10px;
76 | }
77 |
78 | .tweet {
79 | width: 100%;
80 | max-width: 590px;
81 | padding: 10px;
82 | display: flex;
83 | margin: 0 auto;
84 | border: 1px solid #dad7d7;
85 | border-radius: 3px;
86 | cursor: pointer;
87 | }
88 |
89 | .tweet-info {
90 | margin: 10px;
91 | display: flex;
92 | flex-direction: column;
93 | }
94 |
95 | /* Hacky but less typing in the video */
96 | .tweet-info > div > span:first-child {
97 | font-weight: bold;
98 | margin-right: 5px;
99 | }
100 | .tweet-info > div > span:nth-child(2) {
101 | color: #969696;
102 | font-size: 16px;
103 | }
104 | .tweet-info > div > div {
105 | color: #969696;
106 | font-size: 15px;
107 | }
108 | .tweet-info > div > p {
109 | font-size: 18px;
110 | margin: 10px 0;
111 | }
112 | .tweet-icons {
113 | display: flex;
114 | align-items: center;
115 | }
116 | .tweet-icons > span {
117 | color: #969696;
118 | margin: 0 15px 0 5px;
119 | font-size: 18px !important;
120 | }
121 | .tweet-icon {
122 | font-size: 27px;
123 | color: #697784;
124 | }
125 | .tweet-icon:hover {
126 | color: #00b5d6;
127 | cursor: pointer;
128 | }
129 | .replying-to {
130 | border: none;
131 | background: transparent;
132 | margin: 0;
133 | padding: 0;
134 | color: #969696;
135 | cursor: pointer;
136 | }
137 |
138 | .heart-button {
139 | background: transparent;
140 | border: none;
141 | }
142 |
143 | .new-tweet {
144 | width: 100%;
145 | max-width: 590px;
146 | margin: 0 auto;
147 | display: flex;
148 | flex-direction: column;
149 | }
150 |
151 | .textarea {
152 | border-radius: 4px;
153 | border: 1px solid #dad7d7;
154 | padding: 10px;
155 | font-size: 15px;
156 | height: 100px;
157 | }
158 |
159 | .tweet-length {
160 | text-align: right;
161 | margin-top: 10px;
162 | font-size: 20px;
163 | color: #c3392a;
164 | }
165 |
--------------------------------------------------------------------------------
/src/components/Tweet.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { formatTweet, formatDate } from "../utils/helpers";
4 | import { Link, withRouter } from "react-router-dom";
5 |
6 | //importing icons from react-icons
7 | import { TiArrowBackOutline } from "react-icons/ti";
8 | import { TiHeartOutline } from "react-icons/ti";
9 | import { TiHeartFullOutline } from "react-icons/ti";
10 |
11 | import { handleToggleTweet } from "../actions/tweets";
12 |
13 | class Tweet extends Component {
14 | toParent = (e, id) => {
15 | e.preventDefault();
16 | //todo: redirect to parent tweet
17 | this.props.history.push(`/tweet/${id}`);
18 | };
19 |
20 | handleLike = e => {
21 | e.preventDefault();
22 |
23 | const { dispatch, tweet, authedUser } = this.props;
24 |
25 | //dispatching the action creator
26 | dispatch(
27 | handleToggleTweet({
28 | id: tweet.id,
29 | hasLiked: tweet.hasLiked,
30 | authedUser
31 | })
32 | );
33 |
34 | //
35 | };
36 |
37 | render() {
38 | console.log(this.props);
39 | const { tweet } = this.props;
40 |
41 | if (tweet === null) {
42 | return This tweet doesn't exist
;
43 | }
44 |
45 | const {
46 | name,
47 | avatar,
48 | timestamp,
49 | text,
50 | hasLiked,
51 | likes,
52 | replies,
53 | id,
54 | parent
55 | } = tweet;
56 |
57 | return (
58 |
59 |
60 |
61 |
62 |
63 |
{name}
64 |
{formatDate(timestamp)}
65 | {parent && (
66 |
72 | )}
73 |
{text}
74 |
75 |
76 |
77 |
78 | {/* show number only if it's not zero */}
79 | {replies !== 0 && replies}
80 |
87 | {likes !== 0 && likes}
88 |
89 |
90 |
91 | );
92 | }
93 | }
94 |
95 | //id comes from the props passed by a parent component
96 | function mapStateToProps({ authedUser, users, tweets }, { id }) {
97 | const tweet = tweets[id]; //getting the specific tweet by its id
98 | const parentTweet = tweet ? tweets[tweet.replyingTo] : null; //check if the specific tweet is a reply to another one. If so, get information about that parent tweet
99 |
100 | return {
101 | authedUser,
102 | tweet: tweet //making sure a tweet exists
103 | ? formatTweet(tweet, users[tweet.author], authedUser, parentTweet)
104 | : null
105 | };
106 | }
107 |
108 | //using withRouter because this component is not being rendered by react router, so to have access to history props, we need to use withRouter
109 | export default withRouter(connect(mapStateToProps)(Tweet));
110 |
--------------------------------------------------------------------------------
/src/utils/_DATA.js:
--------------------------------------------------------------------------------
1 | let users = {
2 | sarah_edo: {
3 | id: "sarah_edo",
4 | name: "Sarah Drasner",
5 | avatarURL: "https://tylermcginnis.com/would-you-rather/sarah.jpg",
6 | tweets: ['8xf0y6ziyjabvozdd253nd', 'hbsc73kzqi75rg7v1e0i6a', '2mb6re13q842wu8n106bhk', '6h5ims9iks66d4m7kqizmv', '3sklxkf9yyfowrf0o1ftbb'],
7 | },
8 | tylermcginnis: {
9 | id: "tylermcginnis",
10 | name: "Tyler McGinnis",
11 | avatarURL: "https://tylermcginnis.com/would-you-rather/tyler.jpg",
12 | tweets: ['5c9qojr2d1738zlx09afby', 'f4xzgapq7mu783k9t02ghx', 'nnvkjqoevs8t02lzcc0ky', '4pt0px8l0l9g6y69ylivti', 'fap8sdxppna8oabnxljzcv', 'leqp4lzfox7cqvsgdj0e7', '26p5pskqi88i58qmza2gid', 'xi3ca2jcfvpa0i3t4m7ag'],
13 | },
14 | dan_abramov: {
15 | id: "dan_abramov",
16 | name: "Dan Abramov",
17 | avatarURL: "https://tylermcginnis.com/would-you-rather/dan.jpg",
18 | tweets: ['5w6k1n34dkp1x29cuzn2zn', 'czpa59mg577x1oo45cup0d', 'omdbjl68fxact38hk7ypy6', '3km0v4hf1ps92ajf4z2ytg', 'njv20mq7jsxa6bgsqc97', 'sfljgka8pfddbcer8nuxv', 'r0xu2v1qrxa6ygtvf2rkjw'],
19 | }
20 | }
21 |
22 | let tweets = {
23 | "8xf0y6ziyjabvozdd253nd": {
24 | id: "8xf0y6ziyjabvozdd253nd",
25 | text: "Shoutout to all the speakers I know for whom English is not a first language, but can STILL explain a concept well. It's hard enough to give a good talk in your mother tongue!",
26 | author: "sarah_edo",
27 | timestamp: 1518122597860,
28 | likes: ['tylermcginnis'],
29 | replies: ['fap8sdxppna8oabnxljzcv', '3km0v4hf1ps92ajf4z2ytg'],
30 | replyingTo: null,
31 | },
32 | "5c9qojr2d1738zlx09afby": {
33 | id: "5c9qojr2d1738zlx09afby",
34 | text: "I hope one day the propTypes pendulum swings back. Such a simple yet effective API. Was one of my favorite parts of React.",
35 | author: "tylermcginnis",
36 | timestamp: 1518043995650,
37 | likes: ['sarah_edo', 'dan_abramov'],
38 | replies: ['njv20mq7jsxa6bgsqc97'],
39 | replyingTo: null,
40 | },
41 | "f4xzgapq7mu783k9t02ghx": {
42 | id: "f4xzgapq7mu783k9t02ghx",
43 | text: "Want to work at Facebook/Google/:BigCompany? Start contributing code long before you ever interview there.",
44 | author: "tylermcginnis",
45 | timestamp: 1517043995650,
46 | likes: ['dan_abramov'],
47 | replies: [],
48 | replyingTo: null,
49 | },
50 | "hbsc73kzqi75rg7v1e0i6a": {
51 | id: "hbsc73kzqi75rg7v1e0i6a",
52 | text: "Puppies 101: buy a hamper with a lid on it.",
53 | author: "sarah_edo",
54 | timestamp: 1516043995650,
55 | likes: ['tylermcginnis'],
56 | replies: ['leqp4lzfox7cqvsgdj0e7', 'sfljgka8pfddbcer8nuxv'],
57 | replyingTo: null,
58 | },
59 | "5w6k1n34dkp1x29cuzn2zn": {
60 | id: "5w6k1n34dkp1x29cuzn2zn",
61 | text: "Is there a metric like code coverage, but that shows lines that, if changed (in a syntactically correct way), wouldn’t cause tests to fail?",
62 | author: "dan_abramov",
63 | timestamp: 1515043995650,
64 | likes: ['sarah_edo'],
65 | replies: [],
66 | replyingTo: null,
67 | },
68 | "czpa59mg577x1oo45cup0d": {
69 | id: "czpa59mg577x1oo45cup0d",
70 | text: "React came out 'rethinking best practices'. It has since accumulated 'best practices' of its own. Let’s see if we can do better.",
71 | author: "dan_abramov",
72 | timestamp: 1515043995650,
73 | likes: ['tylermcginnis', 'sarah_edo'],
74 | replies: ['3sklxkf9yyfowrf0o1ftbb'],
75 | replyingTo: null,
76 | },
77 | "2mb6re13q842wu8n106bhk": {
78 | id: "2mb6re13q842wu8n106bhk",
79 | text: "I think I realized I like dogs so much because I can really relate to being motivated by snacks",
80 | author: "sarah_edo",
81 | timestamp: 1514043995650,
82 | likes: ['dan_abramov'],
83 | replies: ['26p5pskqi88i58qmza2gid'],
84 | replyingTo: null,
85 | },
86 | "nnvkjqoevs8t02lzcc0ky": {
87 | id: "nnvkjqoevs8t02lzcc0ky",
88 | text: "Maybe the real benefit of open source was the friendships we made along the way?",
89 | author: "tylermcginnis",
90 | timestamp: 1513043995650,
91 | likes: [],
92 | replies: [],
93 | replyingTo: null,
94 | },
95 | "omdbjl68fxact38hk7ypy6": {
96 | id: "omdbjl68fxact38hk7ypy6",
97 | text: "A 7-minute Paul Joseph Watson video being translated and aired by a Russian state TV channel is the most surreal thing I’ve seen in 2018 yet",
98 | author: "dan_abramov",
99 | timestamp: 1512043995650,
100 | likes: [],
101 | replies: [],
102 | replyingTo: null,
103 | },
104 | "4pt0px8l0l9g6y69ylivti": {
105 | id: "4pt0px8l0l9g6y69ylivti",
106 | text: "Talking less about the downsides of OSS and focusing on some of the huge potential upsides for once might just help get more people into it.",
107 | author: "tylermcginnis",
108 | timestamp: 1511043995650,
109 | likes: ['dan_abramov'],
110 | replies: [],
111 | replyingTo: null,
112 | },
113 | "6h5ims9iks66d4m7kqizmv": {
114 | id: "6h5ims9iks66d4m7kqizmv",
115 | text: "By the way, if you have a blog post sitting around and want to get some eyes on it, we take guest submissions! That's how I started.",
116 | author: "sarah_edo",
117 | timestamp: 1510043995650,
118 | likes: ['dan_abramov', 'tylermcginnis'],
119 | replies: ['xi3ca2jcfvpa0i3t4m7ag', 'r0xu2v1qrxa6ygtvf2rkjw'],
120 | replyingTo: null,
121 | },
122 | "fap8sdxppna8oabnxljzcv": {
123 | id: "fap8sdxppna8oabnxljzcv",
124 | author: "tylermcginnis",
125 | text: "I agree. I'm always really impressed when I see someone giving a talk in a language that's not their own.",
126 | timestamp: 1518122677860,
127 | likes: ['sarah_edo'],
128 | replyingTo: "8xf0y6ziyjabvozdd253nd",
129 | replies: [],
130 | },
131 | "3km0v4hf1ps92ajf4z2ytg": {
132 | id: "3km0v4hf1ps92ajf4z2ytg",
133 | author: "dan_abramov",
134 | text: "It can be difficult at times.",
135 | timestamp: 1518122667860,
136 | likes: [],
137 | replyingTo: "8xf0y6ziyjabvozdd253nd",
138 | replies: [],
139 | },
140 | "njv20mq7jsxa6bgsqc97": {
141 | id: "njv20mq7jsxa6bgsqc97",
142 | author: "dan_abramov",
143 | text: "Sometimes you have to sacrifice simplicity for power.",
144 | timestamp: 1518044095650,
145 | likes: ['tylermcginnis'],
146 | replyingTo: "5c9qojr2d1738zlx09afby",
147 | replies: [],
148 | },
149 | "leqp4lzfox7cqvsgdj0e7": {
150 | id: "leqp4lzfox7cqvsgdj0e7",
151 | author: "tylermcginnis",
152 | text: "Also trashcans. Learned this the hard way.",
153 | timestamp: 1516043255650,
154 | likes: [],
155 | replyingTo: "hbsc73kzqi75rg7v1e0i6a",
156 | replies: [],
157 | },
158 | "sfljgka8pfddbcer8nuxv": {
159 | id: "sfljgka8pfddbcer8nuxv",
160 | author: "dan_abramov",
161 | text: "Puppies are the best.",
162 | timestamp: 1516045995650,
163 | likes: ['sarah_edo', 'tylermcginnis'],
164 | replyingTo: "hbsc73kzqi75rg7v1e0i6a",
165 | replies: [],
166 | },
167 | "3sklxkf9yyfowrf0o1ftbb": {
168 | id: "3sklxkf9yyfowrf0o1ftbb",
169 | author: "sarah_edo",
170 | text: "The idea of best practices being a negative thing is an interesting concept.",
171 | timestamp: 1515044095650,
172 | likes: ['dan_abramov'],
173 | replyingTo: "czpa59mg577x1oo45cup0d",
174 | replies: [],
175 | },
176 | "26p5pskqi88i58qmza2gid": {
177 | id: "26p5pskqi88i58qmza2gid",
178 | author: "tylermcginnis",
179 | text: "Too relatable",
180 | timestamp: 1514044994650,
181 | likes: ['sarah_edo'],
182 | replyingTo: "2mb6re13q842wu8n106bhk",
183 | replies: [],
184 | },
185 | "xi3ca2jcfvpa0i3t4m7ag": {
186 | id: "xi3ca2jcfvpa0i3t4m7ag",
187 | author: "tylermcginnis",
188 | text: "Just DMd you!",
189 | timestamp: 1510043995650,
190 | likes: [],
191 | replyingTo: "6h5ims9iks66d4m7kqizmv",
192 | replies: [],
193 | },
194 | "r0xu2v1qrxa6ygtvf2rkjw": {
195 | id: "r0xu2v1qrxa6ygtvf2rkjw",
196 | author: "dan_abramov",
197 | text: "This is a great idea.",
198 | timestamp: 1510044395650,
199 | likes: ['tylermcginnis'],
200 | replyingTo: "6h5ims9iks66d4m7kqizmv",
201 | replies: [],
202 | },
203 | }
204 |
205 | export function _getUsers () {
206 | return new Promise((res, rej) => {
207 | setTimeout(() => res({...users}), 1000)
208 | })
209 | }
210 |
211 | export function _getTweets () {
212 | return new Promise((res, rej) => {
213 | setTimeout(() => res({...tweets}), 1000)
214 | })
215 | }
216 |
217 | export function _saveLikeToggle ({ id, hasLiked, authedUser }) {
218 | return new Promise((res, rej) => {
219 | setTimeout(() => {
220 | tweets = {
221 | ...tweets,
222 | [id]: {
223 | ...tweets[id],
224 | likes: hasLiked === true
225 | ? tweets[id].likes.filter((uid) => uid !== authedUser)
226 | : tweets[id].likes.concat([authedUser])
227 | }
228 | }
229 |
230 | res()
231 | }, 500)
232 | })
233 | }
234 |
235 | function generateUID () {
236 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
237 | }
238 |
239 | function formatTweet ({ author, text, replyingTo = null }) {
240 | return {
241 | author,
242 | id: generateUID(),
243 | likes: [],
244 | replies: [],
245 | text,
246 | timestamp: Date.now(),
247 | replyingTo,
248 | }
249 | }
250 |
251 | export function _saveTweet ({ text, author, replyingTo }) {
252 | return new Promise((res, rej) => {
253 | const formattedTweet = formatTweet({
254 | text,
255 | author,
256 | replyingTo
257 | })
258 |
259 | setTimeout(() => {
260 | tweets = {
261 | ...tweets,
262 | [formattedTweet.id]: formattedTweet,
263 | }
264 |
265 | users = {
266 | ...users,
267 | [author]: {
268 | ...users[author],
269 | tweets: users[author].tweets.concat([formattedTweet.id])
270 | }
271 | }
272 |
273 | res(formattedTweet)
274 | }, 1000)
275 | })
276 | }
277 |
--------------------------------------------------------------------------------