├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── _redirects
├── index.html
├── manifest.json
└── robots.txt
└── src
├── App.js
├── App.scss
├── App.test.js
├── Icons.js
├── components
├── Alerts
│ ├── index.js
│ └── style.scss
├── Bookmarks
│ ├── index.js
│ └── style.scss
├── ChatPage
│ ├── index.js
│ └── style.scss
├── Explore
│ ├── index.js
│ └── style.scss
├── Feed
│ ├── index.js
│ └── style.scss
├── Home
│ ├── index.js
│ └── style.scss
├── ListPage
│ ├── index.js
│ └── style.scss
├── Lists
│ ├── index.js
│ └── style.scss
├── Loader
│ ├── index.js
│ └── style.css
├── Login
│ ├── index.js
│ └── style.scss
├── Messages
│ ├── index.js
│ └── style.scss
├── Nav
│ ├── index.js
│ └── style.scss
├── Notifications
│ ├── index.js
│ └── style.scss
├── Profile
│ ├── index.js
│ └── style.scss
├── Signup
│ ├── index.js
│ └── style.scss
├── Tweet
│ ├── index.js
│ └── style.scss
└── TweetCard
│ ├── index.js
│ └── style.scss
├── config.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
├── setupTests.js
└── store
├── actions.js
├── middleware.js
├── reducers.js
├── store.js
└── typeActions.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .env
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Twitter-Clone
2 |
3 | A website I build based on twitter please enjoy. This website is Not for actual usage only learning purposes.
4 |
5 | [Website Link](https://twitterapp-clone.netlify.app)
6 |
7 | [Backend Repo](https://github.com/Ali-hd/TwitterClone-Backend)
8 |
9 |
10 |
11 | ## Technologies used
12 | - Reactjs
13 | - Context API
14 | - Node.js express
15 | - MongoDB mongoose
16 | - Image uploaded on Amazon S3
17 | - socket-io for real-time chatting
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitter-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "axios": "^0.19.2",
10 | "darkreader": "^4.9.10",
11 | "dotenv": "^8.2.0",
12 | "jwt-decode": "^2.2.0",
13 | "moment": "^2.26.0",
14 | "node-sass": "^4.13.1",
15 | "react": "^16.13.1",
16 | "react-contenteditable": "^3.3.4",
17 | "react-dom": "^16.13.1",
18 | "react-responsive": "^8.1.0",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "3.4.1",
21 | "serve": "^11.3.2",
22 | "socket.io-client": "^2.3.0"
23 | },
24 | "scripts": {
25 | "dev": "react-scripts start",
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test --env=jsdom",
29 | "eject": "react-scripts eject",
30 | "heroku-postbuild": "npm run build"
31 | },
32 | "eslintConfig": {
33 | "extends": "react-app"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
28 | Twitter Clone
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Twitter Clone",
3 | "name": "Twitter Clone",
4 | "icons": [
5 |
6 | ],
7 | "start_url": ".",
8 | "display": "standalone",
9 | "theme_color": "#000000",
10 | "background_color": "#ffffff"
11 | }
12 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from 'react'
2 | import { Route, Switch, BrowserRouter, Redirect, withRouter } from 'react-router-dom'
3 | import { StoreProvider } from './store/store'
4 | import 'dotenv/config'
5 | import './App.scss'
6 | import Loader from './components/Loader'
7 | import Nav from './components/Nav'
8 | import Login from './components/Login'
9 | import Signup from './components/Signup'
10 | import Tweet from './components/Tweet'
11 | import Bookmarks from './components/Bookmarks'
12 | import Lists from './components/Lists'
13 | import ListPage from './components/ListPage'
14 | import Explore from './components/Explore'
15 | import Feed from './components/Feed'
16 | import Notifications from './components/Notifications'
17 | import Messages from './components/Messages'
18 | import Alerts from './components/Alerts'
19 | import ChatPage from './components/ChatPage'
20 |
21 | const Home = lazy(() => import('./components/Home'))
22 | const Profile = lazy(() => import('./components/Profile'))
23 |
24 | const DefaultContainer = withRouter(({ history }) => {
25 | return (
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 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {history.location.pathname.slice(0,9) !== '/messages' &&
63 |
64 |
65 |
66 | }
67 |
68 |
71 |
)
72 | });
73 |
74 | function App() {
75 | return (
76 |
77 |
78 |
79 | }>
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | )
95 | }
96 |
97 | export default App
98 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 |
2 | .body-wrap{
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: center;
6 | }
7 |
8 | .header{
9 | display: flex;
10 | justify-content: flex-end;
11 | position: relative;
12 | order: -1;
13 | flex-wrap: wrap;
14 | }
15 |
16 | .main{
17 | width: 990px;
18 | display: flex;
19 | justify-content: space-between;
20 | }
21 |
22 | .middle-section{
23 | max-width: 600px;
24 | min-height: 100vh;
25 | }
26 |
27 | .ms-width{
28 | width: 100%;
29 | }
30 |
31 | .right-section{
32 | width: 350px;
33 | margin-right: 10px;
34 | min-height: 1000px;
35 | height: 100%;
36 | }
37 |
38 | .modal-edit{
39 | // display: none;
40 | position: fixed;
41 | z-index: 250;
42 | // padding-top: 100px;
43 | left: 0;
44 | top: 0;
45 | width: 100%;
46 | height: 100%;
47 | overflow: auto;
48 | background-color: rgba(0,0,0,0.4);
49 | }
50 |
51 | .modal-content{
52 | min-height: 400px;
53 | max-height: 90vh;
54 | height: 650px;
55 | width: 100%;
56 | max-width: 600px;
57 | border-radius: 14px;
58 | background-color: #fff;
59 | position: fixed;
60 | top: 50%;
61 | left: 50%;
62 | z-index: 50;
63 | transform: translate(-50%, -50%);
64 | overflow: hidden;
65 | }
66 |
67 | .modal-header{
68 | height: 53px;
69 | z-index: 3;
70 | display: flex;
71 | align-items: center;
72 | padding: 0 15px;
73 | border-bottom: 1px solid rgb(204, 214, 221);
74 | max-width: 1000px;
75 | width: 100%;
76 |
77 | }
78 |
79 | .modal-closeIcon{
80 | display: flex;
81 | justify-content: flex-start;
82 | margin-left: 4px;
83 | align-items: center;
84 | min-width: 59px;
85 | min-height: 30px;
86 | }
87 |
88 | .modal-closeIcon-wrap{
89 | display: flex;
90 | align-items: center;
91 | justify-content: center;
92 | transition: 0.2s ease-in-out;
93 | border: 1px solid rgba(0,0,0,0);
94 | border-radius: 9999px;
95 | width: 39px;
96 | height: 39px;
97 | cursor: pointer;
98 | }
99 |
100 | .modal-closeIcon-wrap:hover{
101 | background-color: rgba(29,161,242,0.1);
102 | }
103 |
104 | .modal-closeIcon-wrap svg{
105 | fill: rgb(29, 161, 242);
106 | height: 22.5px;
107 | }
108 |
109 | .modal-title{
110 | font-weight: bold;
111 | font-size: 19px;
112 | width: 100%;
113 | }
114 |
115 | .save-modal-wrapper{
116 | margin-right: 8px;
117 | min-height: 39px;
118 | min-width: 66px;
119 | width: 100%;
120 | display: flex;
121 | align-items: center;
122 | justify-content: flex-end;
123 | }
124 |
125 | .save-modal-btn{
126 | // width: 48px;
127 | min-height: 30px;
128 | transition: 0.2s ease-in-out;
129 | padding: 0 16px;
130 | display: flex;
131 | justify-content: center;
132 | align-items: center;
133 | font-weight: bold;
134 | color: #fff;
135 | background-color: rgb(29,161,242);
136 | border: 1px solid rgba(0,0,0,0);
137 | border-radius: 9999px;
138 | line-height: 20px;
139 | cursor: pointer;
140 | }
141 |
142 | .save-modal-btn:hover{
143 | background-color: rgb(26, 145, 218);
144 | }
145 |
146 | .modal-body{
147 | display: flex;
148 | flex-direction: column;
149 | height: 100%;
150 |
151 | }
152 |
153 | .modal-banner{
154 | max-height: 200px;
155 | height: 200px;
156 | display: flex;
157 | justify-content: center;
158 | border: 2px solid rgba(0, 0, 0, 0);
159 | background-color: rgba(0, 0, 0, 0.3);
160 | position: relative;
161 | }
162 |
163 | .modal-banner img{
164 | max-width: 100%;
165 | width: 100%;
166 | max-height: 100%;
167 | object-fit: cover;
168 | display: block;
169 | opacity: 0.75;
170 | }
171 |
172 | .modal-banner div{
173 | position: absolute;
174 | width: 100%;
175 | height: 100%;
176 | top: 0;
177 | display: flex;
178 | align-items: center;
179 | justify-content: center;
180 | }
181 |
182 | .modal-banner div input{
183 | width: 22.5px;
184 | min-width: 22.5px;
185 | height: 22.5px;
186 | overflow: hidden;
187 | z-index: 20;
188 | padding: 10px 0 10px 30px;
189 | cursor: pointer;
190 | color: inherit;
191 | background-color: initial;
192 | outline: none;
193 | border: initial;
194 | }
195 |
196 |
197 | .modal-banner div svg{
198 | cursor: pointer;
199 | position: absolute;
200 | fill: #fff;
201 | width: 22.5px;
202 | min-width: 22.5px;
203 | height: 22.5px;
204 | }
205 |
206 | .modal-scroll{
207 | overflow-y: scroll;
208 | height: 100%;
209 | margin-bottom: 55px;
210 | }
211 |
212 | .modal-profile-pic{
213 | height: 120px;
214 | width: 120px;
215 | border: 4px solid #fff;
216 | border-radius: 50%;
217 | margin-left: 16px;
218 | margin-top: -48px;
219 | z-index: 5;
220 | background-color: #fff;
221 | display: flex;
222 | justify-content: center;
223 | align-items: center;
224 | }
225 |
226 | .modal-back-pic{
227 | height: 100%;
228 | width: 100%;
229 | border-radius: 50%;
230 | z-index: 5;
231 | background-color: rgba(0, 0, 0, 1);
232 | position: relative;
233 | }
234 |
235 | .modal-back-pic img{
236 | width: 100%;
237 | height: 100%;
238 | object-fit: cover;
239 | display: block;
240 | opacity: 0.6;
241 | border-radius: 50%;
242 | }
243 |
244 | .modal-back-pic div{
245 | position: absolute;
246 | width: 100%;
247 | height: 100%;
248 | top: 0;
249 | display: flex;
250 | align-items: center;
251 | justify-content: center;
252 | }
253 |
254 | .modal-back-pic div input{
255 | width: 22.5px;
256 | min-width: 22.5px;
257 | height: 22.5px;
258 | overflow: hidden;
259 | z-index: 20;
260 | padding: 10px 0 10px 30px;
261 | cursor: pointer;
262 | color: inherit;
263 | background-color: initial;
264 | outline: none;
265 | border: initial;
266 | }
267 |
268 |
269 | .modal-back-pic div svg{
270 | cursor: pointer;
271 | position: absolute;
272 | fill: #fff;
273 | width: 22.5px;
274 | min-width: 22.5px;
275 | height: 22.5px;
276 | }
277 |
278 | .edit-form{
279 | width: 100%;
280 | }
281 |
282 | .edit-input{
283 | background-color: inherit;
284 | border: inherit;
285 | }
286 |
287 | .edit-input:focus{
288 | background-color: inherit;
289 | border: inherit;
290 | }
291 |
292 | .edit-input-wrap{
293 | padding: 10px 15px;
294 | margin-bottom: 15px;
295 | }
296 |
297 | .edit-input-content{
298 | border-bottom: 1px solid rgb(64, 67, 70);
299 | background-color: rgb(245, 248, 250);
300 | label{
301 | color: rgb(101, 119, 134);
302 | display: block;
303 | padding: 5px 10px 0 10px;
304 | }
305 | input{
306 | width: 100%;
307 | outline: none;
308 | font-size: 19px;
309 | padding: 2px 10px 5px 10px;
310 | }
311 | }
312 |
313 |
314 | //////from home
315 |
316 |
317 | .Tweet-input-wrapper{
318 | padding: 10px 15px 5px 15px;
319 | display: flex;
320 | margin-bottom: 2px;
321 | }
322 |
323 | .Tweet-profile-wrapper{
324 | flex-basis: 49px;
325 | padding-top: 5px;
326 | margin-right: 10px;
327 | img{
328 | object-fit: cover;
329 | }
330 | }
331 | .Tweet-input-side{
332 | display: flex;
333 | flex-direction: column;
334 | justify-content: space-between;
335 | position: static;
336 | width: calc(100% - 49px);
337 | border: 2px solid rgba(0, 0, 0, 0);
338 | border-radius: 5px;
339 | padding-top: 5px;
340 | line-height: 1.3125;
341 | cursor: text;
342 | }
343 |
344 | .inner-input-box{
345 | padding: 10px 0;
346 | font-size: 19px;
347 | color: #9197a3;
348 | position: relative;
349 | }
350 |
351 | .inner-input-box div{
352 | outline: none;
353 | white-space: pre-wrap;
354 | max-width: 506px;
355 | }
356 |
357 | .inner-input-box div:focus{
358 | outline: none;
359 | }
360 |
361 | .inner-input-links{
362 | display: flex;
363 | justify-content: space-between;
364 | margin: 0 2px;
365 |
366 | }
367 |
368 | .input-links-side{
369 | margin-top: 10px;
370 | display: flex;
371 | align-items: center;
372 | }
373 |
374 | .input-attach-wrapper{
375 | width: 39px;
376 | height: 39px;
377 | cursor: pointer;
378 | padding: 8.3px;
379 | position: relative;
380 | input{
381 | position: absolute;
382 | width: 0.3px;
383 | height: 0.3px;
384 | overflow: hidden;
385 | z-index: 4;
386 | padding-top: 21px;
387 | padding-bottom: 15px;
388 | padding-right: 0px;
389 | padding-left: 32px;
390 | top: 2px;
391 | left: 3px;
392 | cursor: pointer;
393 | outline: none;
394 | color: inherit;
395 | background-color: initial;
396 | border: initial;
397 | text-align: start !important;
398 | }
399 | }
400 | .input-attach-wrapper:hover{
401 | border-radius: 50%;
402 | background-color: rgba(29, 161, 242,0.1);
403 | }
404 |
405 | .tweet-btn-side{
406 | margin-left: 10px;
407 | min-height: 39px;
408 | min-width: calc(62.79px);
409 | background-color: rgb(29, 161, 242);
410 | padding: 0 1em;
411 | border: 1px solid rgba(0, 0, 0, 0);
412 | border-radius: 9999px;
413 | display: flex;
414 | justify-content: center;
415 | align-items: center;
416 | color: #fff;
417 | font-weight: 700;
418 | // cursor: pointer;
419 | opacity: 0.5;
420 | transition: 0.15s ease-in-out;
421 | }
422 |
423 | .tweet-btn-active{
424 | cursor: pointer;
425 | opacity: 1;
426 | &:hover{
427 | background-color: rgba(11, 137, 216, 0.876);
428 | }
429 | }
430 |
431 | .Tweet-input-divider{
432 | min-height: 10px;
433 | height: 10px;
434 | background-color: rgb(230, 236, 240);
435 | content: '';
436 | }
437 |
438 | [contenteditable=true]{display: inline-block;}
439 |
440 | [contenteditable=true]:empty:before{
441 | content: attr(placeholder);
442 | pointer-events: none;
443 | display: block; /* For Firefox */
444 | }
445 |
446 | /* */
447 |
448 | [contenteditable=true]:empty:focus{
449 | opacity: 0.7;
450 | }
451 |
452 | .card-content-info{
453 | word-wrap: break-word;
454 | }
455 |
456 | .tweet-input-active{
457 | color: rgb(20, 23, 26);
458 | }
459 |
460 | .tweet-upload-image{
461 | margin-top: 10px;
462 | border-radius: 14px;
463 | max-height: 253px;
464 | width: 100%;
465 | height: 100%;
466 | object-fit: cover;
467 | }
468 |
469 | .inner-image-box{
470 | position: relative;
471 | }
472 |
473 | .cancel-image{
474 | position: absolute;
475 | color: white;
476 | left: 9px;
477 | top: 18px;
478 | width: 33px;
479 | align-items: center;
480 | justify-content: center;
481 | line-height: 23px;
482 | text-align: center;
483 | display: flex;
484 | height: 33px;
485 | background-color: rgba(0, 0, 0, 0.5);
486 | border-radius: 50%;
487 | font-size: 22px;
488 | font-weight: bold;
489 | cursor: pointer;
490 | padding-bottom: 3.5px;
491 | }
492 |
493 | .workInProgress{
494 | max-width: 600px;
495 | border-right: 1px solid rgb(230, 236, 240);
496 | width: 100%;
497 | font-weight: bold;
498 | font-size: 17px;
499 | text-align: center;
500 | padding-top: 20%;
501 | color: #657786;
502 | min-height: 2000px;
503 | }
504 |
505 | // .dark-mode{
506 | // background-color: #1a1919 !important;
507 | // }
508 |
509 | .alert-wrapper{
510 | position: fixed;
511 | top: 0;
512 |
513 | }
514 |
515 | .tweet-btn-holder{
516 | margin-left: 10px;
517 | display: flex;
518 | align-items: center;
519 | }
520 |
521 |
522 | .header-back-wrapper{
523 | margin-left: -5px;
524 | width: 39px;
525 | height: 39px;
526 | transition: 0.2s ease-in-out;
527 | will-change: background-color;
528 | border: 1px solid rgba(0, 0, 0, 0);
529 | border-radius: 9999px;
530 | display: flex;
531 | justify-content: center;
532 | align-items: center;
533 | cursor: pointer;
534 | }
535 |
536 |
537 |
538 |
539 | @media only screen and (max-width: 1196px) {
540 | .right-section{
541 | width: 290px !important;
542 | }
543 | .main{
544 | width: 920px;
545 | }
546 | }
547 |
548 | @media only screen and (max-width: 1005px) {
549 | .right-section{
550 | display: none;
551 | }
552 | .main{
553 | width: 100%;
554 | }
555 | //flex grow
556 | }
557 |
558 | @media only screen and (max-width: 888px){
559 | .chat-right{
560 | display: none;
561 | }
562 | .messages-wrapper{
563 | width: 100% !important;
564 | }
565 | .messages-header-wrapper{
566 | max-width: 100% !important;
567 | }
568 | .middle-section{
569 | width: 100%;
570 | }
571 | .chat-height{
572 | height: 100vh !important;
573 | }
574 | }
575 |
576 | @media only screen and (max-width: 450px){
577 |
578 | body{
579 | overflow-y: auto !important;
580 | overflow-x: hidden;
581 | width: 100%;
582 | }
583 |
584 | .chat-height {
585 | height: calc(100vh - 46px) !important;
586 | }
587 | .chat-bottom-wrapper{
588 | bottom: 50px !important;
589 | }
590 |
591 | .body-wrap{
592 | flex-direction: column;
593 | }
594 |
595 | .header{
596 | position: sticky;
597 | width: 100vw;
598 | bottom: 0;
599 | height: 53px;
600 | order: 1;
601 | }
602 |
603 | .Nav-component{
604 | width: 100vw;
605 | }
606 |
607 | .Nav-width{
608 | width: 100vw !important;
609 | height: 53px;
610 | }
611 |
612 | .Nav{
613 | top: auto !important;
614 | width: 100vw;
615 | position: relative !important;
616 | }
617 |
618 | .Nav-Content{
619 | width: 100% !important;
620 | display: block;
621 | padding: 0 !important;
622 | height: auto;
623 | overflow: hidden;
624 | }
625 |
626 | .Nav-wrapper{
627 | background-color: #fff;
628 | border-top: 2px solid rgb(245, 248, 250);
629 | display: flex;
630 | align-content: center;
631 | flex-direction: row !important;
632 | margin: 0;
633 | justify-content: space-evenly;
634 |
635 | .Nav-link{
636 | padding: 0;
637 | }
638 |
639 | a:nth-child(1){
640 | display: none;
641 | }
642 | a:nth-child(4){
643 | display: none;
644 | }
645 | a:nth-child(6){
646 | display: none;
647 | }
648 | a:nth-child(9){
649 | display: none;
650 | }
651 | }
652 |
653 | .Nav-tweet{
654 | display: none !important;
655 | }
656 |
657 | .more-menu-content{
658 | top: auto !important;
659 | bottom: 46px !important;
660 | left: 47% !important;
661 | overflow: hidden;
662 | height: 154px;
663 | }
664 |
665 | .more-item{
666 | display: flex !important;
667 | }
668 | }
--------------------------------------------------------------------------------
/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/components/Alerts/index.js:
--------------------------------------------------------------------------------
1 | import React, {useContext} from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 |
5 | const Alert = (props) => {
6 | const { state, actions } = useContext(StoreContext)
7 |
8 | return(
9 |
10 |
11 | {state.msg}
12 |
13 |
14 | )
15 | }
16 |
17 | export default Alert
18 |
--------------------------------------------------------------------------------
/src/components/Alerts/style.scss:
--------------------------------------------------------------------------------
1 | .alert-wrapper{
2 | position: fixed;
3 | top: 100px;
4 | width: 150px;
5 | left: 50%;
6 | transform: translate(-50%, 0);
7 | text-align: center;
8 | z-index: 1000;
9 | position: fixed;
10 | transition: top 0.4s ease-in;
11 | }
12 |
13 | .alert-content{
14 | background-color: #1da1f2;
15 | color: #fff;
16 | font-weight: 600;
17 | border: 1px solid #e6ecf0;
18 | -webkit-box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
19 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
20 | display:inline-block;
21 | padding: 8px 14px;
22 | border-radius: 200px;
23 | }
--------------------------------------------------------------------------------
/src/components/Bookmarks/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useContext } from 'react'
2 | import './style.scss'
3 | import { withRouter } from 'react-router-dom'
4 | import { StoreContext } from '../../store/store'
5 | import TweetCard from '../TweetCard'
6 |
7 | const Bookmarks = (props) => {
8 |
9 | const { state, actions } = useContext(StoreContext)
10 |
11 | const {account, bookmarks} = state
12 | // const userParam = props.match.params.username
13 |
14 | useEffect(() => {
15 | window.scrollTo(0, 0)
16 | actions.getBookmarks()
17 | // actions.startChat({id: '5eee5f050cc0ae0017ed2fb2', content: 'hi there buddy'})
18 | }, [])
19 |
20 |
21 | return(
22 |
23 |
24 |
25 |
26 | Bookmarks
27 |
28 |
29 | @{account && account.username}
30 |
31 |
32 |
33 | {/* add loader for bookmarks when empty using dispatch */}
34 | {account && account.bookmarks.length < 1 ?
You don't have any bookmarks
: bookmarks.map(t=>{
35 | return
36 | })}
37 |
38 | )
39 | }
40 |
41 | export default withRouter(Bookmarks)
--------------------------------------------------------------------------------
/src/components/Bookmarks/style.scss:
--------------------------------------------------------------------------------
1 | .bookmarks-wrapper{
2 | max-width: 600px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | // height: 100vh;
6 | display: flex;
7 | flex-direction: column;
8 | min-height: 2000px;
9 | }
10 |
11 | .bookmarks-header-wrapper{
12 | position: sticky;
13 | border-bottom: 1px solid rgb(230, 236, 240);
14 | border-left: 1px solid rgb(230, 236, 240);
15 | background-color: #fff;
16 | z-index: 8;
17 | top: 0px;
18 | display: flex;
19 | align-items: center;
20 | cursor: pointer;
21 | height: 53px;
22 | min-height: 53px;
23 | padding-left: 15px;
24 | padding-right: 15px;
25 | max-width: 1000px;
26 | margin: 0 auto;
27 | width: 100%;
28 | }
29 |
30 | .bookmarks-header-content{
31 | display: flex;
32 | flex-direction: column;
33 | }
34 |
35 | .bookmarks-header-name{
36 | font-weight: 800;
37 | font-size: 19px;
38 | }
39 |
40 | .bookmarks-header-tweets{
41 | font-size: 14px;
42 | line-height: calc(19.6875px);
43 | color: rgb(101, 119, 134);
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ChatPage/index.js:
--------------------------------------------------------------------------------
1 | import React, {useContext, useState, useEffect, useRef} from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 | import {API_URL} from '../../config'
5 | import { withRouter } from 'react-router-dom';
6 | import { token } from '../../store/middleware'
7 | import io from 'socket.io-client'
8 | import moment from 'moment'
9 | import {useMediaQuery} from 'react-responsive'
10 | import { ICON_ARROWBACK, ICON_SEND} from '../../Icons'
11 |
12 | let socket = io.connect(API_URL,{
13 | query: {token: token()}
14 | })
15 |
16 | const ChatPage = (props) => {
17 |
18 | const { state, actions } = useContext(StoreContext)
19 | const [room, setRoom] = useState(null)
20 | const [conversation, setConversation] = useState([])
21 | const [text, setText] = useState('')
22 | const mounted = useRef()
23 | const roomRef = useRef()
24 |
25 | const {account} = state
26 | useEffect(() => {
27 | if(props.history.location.pathname.slice(10).length === 24)
28 | getConversation(props.history.location.pathname.slice(10))
29 | //check when component unmounts
30 | return () => {
31 | if(roomRef.current){ socket.emit('leaveRoom', roomRef.current) } }
32 | }, [props.history.location.pathname])
33 |
34 |
35 | useEffect(() => {
36 | if(!mounted.current){
37 | mounted.current = true
38 | }else{
39 | if(document.querySelector('#messageBody')){
40 | let messageBody = document.querySelector('#messageBody');
41 | messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
42 | }
43 | socket.on('output', msg => {
44 | let currConversation = conversation
45 | currConversation.push(msg)
46 | setConversation(currConversation)
47 | setText((text)=>[...text, ''])
48 | let messageBody = document.querySelector('#messageBody');
49 | messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
50 | })
51 |
52 | }
53 | }, [conversation])
54 |
55 | const fillConversation = params => {
56 | setConversation(params)
57 | }
58 |
59 | const sendMsg = () => {
60 | if(text.length>0){
61 | document.getElementById('chat').value = "";
62 | let id = state.conversation.participants[0] !== state.account._id ? state.conversation.participants[0] : state.conversation.participants[1]
63 | socket.emit('chat', { room: room, id, content: text })
64 | }
65 | }
66 |
67 | const getConversation = (id) => {
68 | if(room){ socket.emit('leaveRoom', room) }
69 | socket.emit('subscribe', id);
70 | setRoom(id)
71 | roomRef.current = id
72 | actions.getSingleConversation({id:id, func: fillConversation})
73 | }
74 |
75 | const handleInputChange = (e) => {
76 | setText(e.target.value)
77 | }
78 |
79 | const handleKeyDown = (e) => {
80 | console.log(conversation)
81 | if(e.keyCode === 13){
82 | sendMsg()
83 | }
84 | }
85 |
86 | const isTabletOrMobile = useMediaQuery({ query: '(max-width: 888px)' })
87 |
88 | return(
89 |
90 | {account ? isTabletOrMobile && !props.res ? null :
91 |
92 |
93 | {props.res &&
94 |
window.history.back()} className="header-back-wrapper">
95 |
96 |
97 |
}
98 | {/*
99 | Ali hd
100 |
101 |
102 | @alihd
103 | */}
104 |
105 |
106 |
107 | {room ?
108 | conversation.map((msg,i) => {
109 | return
110 | {msg.sender.username === account.username ?
111 |
112 |
113 |
114 | {msg.content}
115 |
116 |
117 | {i>0 && moment.duration(moment(msg.createdAt).diff(moment(conversation[i-1].createdAt))).asMinutes() > 1 ?
118 |
119 | {moment(msg.createdAt).format("MMM D, YYYY, h:mm A")}
120 |
:
}
121 |
122 | :
123 |
124 |
125 |
126 | {msg.content}
127 |
128 |
129 | {i>0 && moment.duration(moment(msg.createdAt).diff(moment(conversation[i-1].createdAt))).asMinutes() > 1 ?
130 |
131 | {moment(msg.createdAt).format("MMM D, YYYY, h:mm A")}
132 |
:
}
133 |
}
134 |
135 | }) :
136 |
137 |
138 | You dont have a message selected
139 |
140 |
Choose one from your existing messages, on the left.
141 |
}
142 |
143 |
144 |
145 |
146 |
handleKeyDown(e)} onChange={(e)=>handleInputChange(e)} placeholder="Start a new message" id="chat" type="text" name="message" />
147 |
148 |
149 |
150 |
151 |
152 |
: null }
153 |
154 | )
155 | }
156 |
157 | export default withRouter(ChatPage)
--------------------------------------------------------------------------------
/src/components/ChatPage/style.scss:
--------------------------------------------------------------------------------
1 | .chat-wrapper{
2 | max-width: 100%;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | color: #657786;
6 | min-height: 100vh;
7 | }
8 |
9 | .chat-header-wrapper{
10 | position: sticky;
11 | border-bottom: 1px solid rgb(230, 236, 240);
12 | background-color: #fff;
13 | z-index: 3;
14 | top: 0px;
15 | display: flex;
16 | align-items: center;
17 | height: 53px;
18 | min-height: 53px;
19 | padding: 3px 15px;
20 | max-width: 100%;
21 | width: 100%;
22 | font-size: 19px;
23 | h4{
24 | color: #000;
25 | }
26 |
27 | span{
28 | font-weight: 400;
29 | color: rgb(101, 119, 134);
30 | margin-left: 5px;
31 | }
32 | }
33 |
34 |
35 | .chat-height{
36 | height: 100%;
37 | }
38 |
39 | .conv-div{
40 | position: relative;
41 | height: 100%;
42 | }
43 |
44 | .conversation-wrapper{
45 | padding: 20px 15px 0 15px;
46 | margin: 0 auto;
47 | height: calc(100% - 110px);
48 | overflow-y: auto;
49 | position: absolute;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | bottom: 0;
54 | }
55 |
56 |
57 | .users-box{
58 | padding-bottom: 20px;
59 | display: flex;
60 | flex-direction: column;
61 | }
62 |
63 | .users-msg{
64 | display: flex;
65 | justify-content: flex-end;
66 | }
67 |
68 | .users-content{
69 | padding: 10px 15px;
70 | background-color: rgb(29, 161, 242);
71 | color: #fff;
72 | max-width: 100%;
73 | border-radius: 30px 30px 0 30px;
74 | margin-bottom: 3px;
75 | }
76 |
77 | .users-date{
78 | display: flex;
79 | justify-content: flex-end;;
80 | font-size: 13px;
81 | }
82 |
83 | .sender-box{
84 | padding-bottom: 20px;
85 | display: flex;
86 | flex-direction: column;
87 | }
88 |
89 | .sender-msg{
90 | display: flex;
91 | }
92 |
93 | .sender-content{
94 | padding: 10px 15px;
95 | background-color: rgb(230, 236, 240);
96 | color: #000;
97 | max-width: 100%;
98 | border-radius: 30px 30px 30px 0px;
99 | margin-bottom: 3px;
100 | }
101 |
102 | .sender-date{
103 | display: flex;
104 | font-size: 13px;
105 | }
106 |
107 | .chat-bottom-wrapper{
108 | height: 53px;
109 | width: 100%;
110 | max-height: 53px;
111 | bottom: 0;
112 | position: sticky;
113 | padding: 0px 15px;
114 | border-top: 1px solid rgb(230, 236, 240);
115 | display: flex;
116 | align-items: center;
117 | background-color: #fff;
118 | }
119 |
120 | .chat-input-container{
121 | background-color: #e6ecf0;
122 | border: 1px solid transparent;
123 | border-radius: 9999px;
124 | display: flex;
125 | align-items: center;
126 | min-height: 38px;
127 | width: 100%;
128 |
129 | input{
130 | background-color: inherit;
131 | border: inherit;
132 | width: 100%;
133 | font-size: 15px;
134 | color: #657786;
135 | outline: none;
136 | padding: 6px 10px;
137 | border-radius: 9999px;
138 | }
139 |
140 | div{
141 | display: flex;
142 | align-items: center;
143 | }
144 |
145 | svg{
146 | height: 22.5px;
147 | width: 22.5px;
148 | margin-left: 10px;
149 | fill: rgb(29, 161, 242);
150 | cursor: pointer;
151 | }
152 | }
153 |
154 | .active{
155 | background-color: #fff;
156 | input{
157 | border: 1px solid rgb(29, 161, 242);
158 | }
159 | }
160 |
161 | .not-selected-msg{
162 | height: 100%;
163 | display: flex;
164 | justify-content: center;
165 | align-items: center;
166 | flex-direction: column;
167 | margin-top: -53px;
168 | div{
169 | font-size: 19px;
170 | font-weight: bold;
171 | color: #000;
172 | margin-bottom: 10px;
173 | }
174 | p{
175 | font-size: 16px;
176 | color: rgb(101, 119, 134);
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/Explore/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 | import { withRouter } from 'react-router-dom'
5 | import { ICON_SEARCH, ICON_ARROWBACK } from '../../Icons'
6 | import Loader from '../Loader'
7 | import TweetCard from '../TweetCard'
8 |
9 |
10 | const Explore = (props) => {
11 | const { state, actions } = useContext(StoreContext)
12 | const { trends, result, tagTweets} = state
13 | const [tab, setTab] = useState('Trends')
14 | const [trendOpen, setTrendOpen] = useState(false)
15 |
16 |
17 | const searchOnChange = (param) => {
18 | if(tab !== 'Search'){setTab('Search')}
19 | if(param.length>0){
20 | actions.search({description: param})
21 | }
22 | }
23 |
24 | useEffect(() => {
25 | window.scrollTo(0, 0)
26 | actions.getTrend()
27 | // if(props.history.location.search.length>0){
28 | // goToTrend(props.history.location.search.substring(1))
29 |
30 | // }
31 | }, [])
32 |
33 | // const followUser = (e, id) => {
34 | // e.stopPropagation()
35 | // actions.followUser(id)
36 | // }
37 |
38 | // const goToUser = (id) => {
39 | // props.history.push(`/profile/${id}`)
40 | // }
41 |
42 | const goToTrend = (hash) => {
43 | setTrendOpen(true)
44 | let hashtag = hash.substring(1)
45 | actions.getTrendTweets(hashtag)
46 | }
47 |
48 |
49 | return(
50 |
51 |
52 | {trendOpen &&
53 |
54 |
setTrendOpen(false)} className="explore-back-wrapper">
55 |
56 |
57 |
}
58 |
59 |
60 |
61 |
62 |
63 | searchOnChange(e.target.value)} placeholder="Search for hashtags or people" type="text" name="search"/>
64 |
65 |
66 |
67 | {!trendOpen ?
68 |
69 |
70 |
setTab('Trends')} className={tab === 'Trends' ? `explore-nav-item activeTab` : `explore-nav-item`}>
71 | Trending
72 |
73 |
setTab('Search')} className={tab === 'Search' ? `explore-nav-item activeTab` : `explore-nav-item`}>
74 | Search
75 |
76 |
77 | {tab === 'Trends' ?
78 | trends.length>0 ?
79 | trends.map((t,i)=>{
80 | return
goToTrend(t.content)} key={t._id} className="trending-card-wrapper">
81 |
{i+1} · Trending
82 |
{t.content}
83 |
{t.count} Tweets
84 |
85 | }) :
86 | :
87 | result.length ? result.map(r=>{
88 | return
89 | }) :
90 | Nothing to see here ..
91 |
92 | Try searching for people, usernames, or keywords
93 |
94 |
95 | }
96 |
:
97 | {tagTweets.length>0 && tagTweets.map(t=>{
98 | return
99 | })}
100 |
}
101 |
102 | )
103 | }
104 |
105 | export default withRouter(Explore)
--------------------------------------------------------------------------------
/src/components/Explore/style.scss:
--------------------------------------------------------------------------------
1 | .explore-wrapper{
2 | max-width: 600px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | // height: 100vh;
6 | display: flex;
7 | flex-direction: column;
8 | min-height: 2000px;
9 | }
10 |
11 |
12 | .explore-header{
13 | position: sticky;
14 | border-left: 1px solid rgb(230, 236, 240);
15 | background-color: #fff;
16 | z-index: 8;
17 | top: 0px;
18 | display: flex;
19 | align-items: center;
20 | height: 53px;
21 | min-height: 53px;
22 | padding-left: 15px;
23 | padding-right: 15px;
24 | max-width: 600px;
25 | margin: 0 auto;
26 | width: 100%;
27 | }
28 |
29 | .header-border{
30 | border-bottom: 1px solid rgb(230, 236, 240);
31 | }
32 |
33 | .explore-search-wrapper{
34 | background-color: rgb(230, 236, 240);
35 | border: 1px solid transparent;
36 | border-radius: 9999px;
37 | display: flex;
38 | align-items: center;
39 | min-height: 38px;
40 | width: 100%;
41 |
42 | }
43 |
44 | .explore-search-icon{
45 | display: flex;
46 | align-items: center;
47 | }
48 |
49 | .explore-search-icon svg{
50 | width: 40px;
51 | height: 18.75px;
52 | fill: rgb(101, 119, 134);
53 | padding-left: 10px;
54 | }
55 |
56 | .explore-search-input{
57 | width: 100%;
58 | }
59 |
60 | .explore-search-input input{
61 | background-color: inherit;
62 | border: inherit;
63 | padding: 6px 10px;
64 | width: 100%;
65 | font-size: 15px;
66 | color: rgb(101, 119, 134);
67 | outline: none;
68 | }
69 |
70 | .explore-nav-menu{
71 | margin-top: 10px;
72 | display: flex;
73 | justify-content: space-around;
74 | align-items: center;
75 | border-bottom: 1px solid rgb(230, 236, 240);
76 | }
77 |
78 | .explore-nav-item{
79 | padding: 15px;
80 | width: 100%;
81 | text-align: center;
82 | cursor: pointer;
83 | font-weight: bold;
84 | color: rgb(101, 119, 134);
85 | transition: 0.2s;
86 | will-change: background-color;
87 | box-sizing:border-box;
88 | border-bottom: 2px solid transparent;
89 | &:hover{
90 | background-color: rgba(29, 161, 242, 0.1);
91 | color: rgb(29, 161, 242);
92 | }
93 | }
94 |
95 | .activeTab{
96 | border-bottom: 2px solid rgb(29, 161, 242);
97 | color: rgb(29, 161, 242);
98 | }
99 |
100 | .search-result-wapper{
101 | border-bottom: 1px solid rgb(230, 236, 240);
102 | padding: 10px 15px;
103 | transition: 0.2s;
104 | cursor: pointer;
105 | display: flex;
106 | &:hover{
107 | background-color: rgb(245,248,250);
108 | }
109 | }
110 |
111 | .search-userPic-wrapper{
112 | flex-basis: 49px;
113 | margin-right: 10px;
114 | img{
115 | object-fit: cover;
116 | }
117 | }
118 |
119 | .search-user-details{
120 | display: flex;
121 | flex-direction: column;
122 | width: 100%;
123 | }
124 |
125 | .search-user-info{
126 | display: flex;
127 | flex-direction: column;
128 | }
129 |
130 | .search-user-name{
131 | font-weight: bold;
132 | }
133 |
134 | .search-user-username{
135 | color: rgb(101, 119, 134);
136 | line-height: 1;
137 | }
138 |
139 | .search-user-bio{
140 | margin-top: 7px;
141 | }
142 |
143 | .search-user-warp{
144 | display: flex;
145 | flex-direction: row;
146 | align-items: center;
147 | justify-content: space-between;
148 | }
149 |
150 | .follow-btn-wrap{
151 | min-height: 30px;
152 | min-width: 70px;
153 | transition: 0.2s ease-in-out;
154 | cursor: pointer;
155 | border: 1px solid #1da1f2;
156 | border-radius: 9999px;
157 | display: flex;
158 | justify-content: center;
159 | align-items: center;
160 | padding-left: 1em;
161 | padding-right: 1em;
162 | &:hover{
163 | background-color: rgba(29, 161, 242, 0.1);
164 | }
165 | }
166 |
167 | .follow-btn-wrap span{
168 | text-align: center;
169 | font-weight: 800;
170 | color: #1da1f2;
171 | width: 100%;
172 | }
173 |
174 |
175 |
176 | .trending-card-wrapper{
177 | border-bottom: 1px solid rgb(245,248,250);
178 | padding: 10px 15px;
179 | transition: 0.2s;
180 | cursor: pointer;
181 | display: flex;
182 | flex-direction: column;
183 | &:hover{
184 | background-color: rgb(245, 248, 250);
185 | }
186 | }
187 |
188 | .trending-card-header{
189 | color: rgb(101, 119, 134);
190 | font-size: 14px;
191 | span{
192 | padding: 0 3px;
193 | }
194 | }
195 |
196 | .trending-card-content{
197 | font-weight: bold;
198 | font-size: 19px;
199 | padding-top: 2px;
200 | padding-bottom: 2px;
201 | }
202 |
203 | .trending-card-count{
204 | font-size: 15px;
205 | color: rgb(101, 119, 134);
206 | }
207 |
208 | .try-searching{
209 | font-weight: bold;
210 | font-size: 17px;
211 | text-align: center;
212 | margin-top: 40px;
213 | color: #657786;
214 | div{
215 | margin-bottom: 15px;
216 | }
217 | }
218 |
219 |
220 | .unfollow-switch{
221 | background-color: rgb(29, 161, 242);
222 | span{color: #fff !important;}
223 | }
224 |
225 | .unfollow-switch:hover{
226 | background-color: rgb(202,32,85) !important;
227 | border: 1px solid transparent;
228 | span{
229 | color: #fff;
230 | span{display: none;}
231 | &:before{
232 | content: 'Unfollow';
233 | }
234 | }
235 | }
236 |
237 | .explore-header-back{
238 | min-width: 55px;
239 | min-height: 30px;
240 | justify-content: center;
241 | align-items: flex-start;
242 | }
243 |
244 | .explore-back-wrapper{
245 | margin-left: -5px;
246 | width: 39px;
247 | height: 39px;
248 | transition: 0.2s ease-in-out;
249 | will-change: background-color;
250 | border: 1px solid rgba(0, 0, 0, 0);
251 | border-radius: 9999px;
252 | display: flex;
253 | justify-content: center;
254 | align-items: center;
255 | cursor: pointer;
256 | }
257 | .explore-back-wrapper svg{
258 | height: 1.5em;
259 | fill: rgb(29,161,242);
260 | }
261 | .explore-back-wrapper:hover{
262 | background-color: rgba(29,161,242,0.1);
263 | }
--------------------------------------------------------------------------------
/src/components/Feed/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useContext } from 'react'
2 | import './style.scss'
3 | import { withRouter, Link } from 'react-router-dom'
4 | import { StoreContext } from '../../store/store'
5 | import Loader from '../Loader'
6 |
7 |
8 | const Feed = (props) => {
9 |
10 | const { state, actions } = useContext(StoreContext)
11 |
12 | const {account, trends, suggestions, session} = state
13 | // const userParam = props.match.params.username
14 |
15 | useEffect(() => {
16 | actions.getTrend()
17 | if(session){
18 | actions.whoToFollow()
19 | }
20 | }, [])
21 |
22 | const goToUser = (id) => {
23 | props.history.push(`/profile/${id}`)
24 | }
25 |
26 | const followUser = (e, id) => {
27 | e.stopPropagation()
28 | actions.followUser(id)
29 | }
30 |
31 |
32 | return(
33 |
34 |
35 |
Trending
36 | {trends.length>0 ? trends.slice(0,3).map((t,i)=>{
37 | return
props.history.push('/explore')} key={t._id} className="feed-card-trend">
38 |
{i+1} · Trending
39 |
{t.content}
40 |
{t.count} Tweets
41 |
42 | }) :
}
43 |
props.history.push(`/explore`)} className="feed-more">
44 | Show more
45 |
46 |
47 | {account ?
48 |
49 |
Who to follow
50 | {suggestions.length > 0 ?
51 | suggestions.map(s=>{
52 | if(s.username !== account.username) {
53 | return
54 |
goToUser(s.username)} className="sugg-result-wapper">
55 |
56 |

57 |
58 |
59 |
60 |
61 |
{s.name}
62 |
@{s.username}
63 |
64 |
followUser(e, s._id)} className={account.following.includes(s._id) ?"follow-btn-wrap unfollow-switch":"follow-btn-wrap"}>
65 | { account.following.includes(s._id) ? 'Following' : 'Follow'}
66 |
67 |
68 |
69 |
70 |
71 | }
72 | })
73 | :
}
74 |
75 | {/* Show more */}
76 |
77 |
: null }
78 |
79 | )
80 | }
81 |
82 | export default withRouter(Feed)
--------------------------------------------------------------------------------
/src/components/Feed/style.scss:
--------------------------------------------------------------------------------
1 | .feed-wrapper{
2 | display: flex;
3 | justify-content: center;
4 | flex-direction: column;
5 | position: sticky;
6 | top: 5px;
7 | }
8 |
9 | .feed-trending-card{
10 | width: 100%;
11 | background-color: rgb(245, 248, 250);
12 | margin-top: 10px;
13 | margin-bottom: 15px;
14 | border: 1px solid rgb(245, 248, 250);
15 | border-top-left-radius: 14px;
16 | border-top-right-radius: 14px;
17 | border-bottom-left-radius: 14px;
18 | border-bottom-right-radius: 14px;
19 | }
20 |
21 | .feed-card-header{
22 | border-bottom: 1px solid rgb(230, 236, 240);
23 | padding: 10px 15px;
24 | display: flex;
25 | align-items: center;
26 | font-size: 19px;
27 | font-weight: bold;
28 | }
29 |
30 | .feed-card-trend{
31 | border-bottom: 1px solid rgb(230, 236, 240);
32 | padding: 10px 15px;
33 | cursor: pointer;
34 | transition: 0.2s ease-in-out;
35 | &:hover{
36 | background-color: rgba(0, 0, 0, 0.03);
37 | }
38 | div:nth-child(1){
39 | font-size: 13px;
40 | color: rgb(101, 119, 134);
41 | }
42 | div:nth-child(2){
43 | font-size: 15px;
44 | color: rgb(20, 23, 26);
45 | font-weight: bold;
46 | padding-top: 2px;
47 | }
48 | div:nth-child(3){
49 | font-size: 15px;
50 | color: rgb(101, 119, 134);
51 | padding-top: 2px;
52 | }
53 | }
54 |
55 | .feed-more{
56 | padding: 15px;
57 | transition: 0.2s ease-in-out;
58 | cursor: pointer;
59 | font-size: 15px;
60 | color: rgb(29,161,242);
61 | }
62 |
63 |
64 | ////@extend
65 |
66 | .sugg-result-wapper{
67 | cursor: pointer;
68 | display: flex;
69 |
70 | }
71 |
72 | .search-userPic-wrapper{
73 | flex-basis: 49px;
74 | margin-right: 10px;
75 | }
76 |
77 | .search-user-details{
78 | display: flex;
79 | flex-direction: column;
80 | width: 100%;
81 | }
82 |
83 | .search-user-info{
84 | display: flex;
85 | flex-direction: column;
86 | }
87 |
88 | .search-user-name{
89 | font-weight: bold;
90 | color: rgb(20, 23, 26) !important;
91 | font-weight: 600 !important;
92 | }
93 |
94 | .search-user-username{
95 | color: #657786 !important;
96 | font-weight: 400 !important;
97 | line-height: 1;
98 | }
99 |
100 | .search-user-bio{
101 | margin-top: 7px;
102 | }
103 |
104 | .search-user-warp{
105 | display: flex;
106 | flex-direction: row;
107 | align-items: center;
108 | justify-content: space-between;
109 | }
110 |
111 | .follow-btn-wrap{
112 | min-height: 30px;
113 | min-width: 70px;
114 | transition: 0.2s ease-in-out;
115 | cursor: pointer;
116 | border: 1px solid #1da1f2;
117 | border-radius: 9999px;
118 | display: flex;
119 | justify-content: center;
120 | align-items: center;
121 | padding-left: 1em;
122 | padding-right: 1em;
123 | &:hover{
124 | background-color: rgba(29, 161, 242, 0.1);
125 | }
126 | }
127 |
128 | .follow-btn-wrap span{
129 | text-align: center;
130 | font-weight: 800;
131 | color: #1da1f2;
132 | width: 100%;
133 | }
--------------------------------------------------------------------------------
/src/components/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext, useRef } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 | import axios from 'axios'
5 | import ContentEditable from 'react-contenteditable'
6 | import { ICON_IMGUPLOAD } from '../../Icons'
7 | import { Link } from 'react-router-dom'
8 | import { API_URL } from '../../config'
9 | import Loader from '../Loader'
10 | import TweetCard from '../TweetCard'
11 |
12 | const Home = () => {
13 | const { state, actions } = useContext(StoreContext)
14 | const { account, session } = state
15 | useEffect(() => {
16 | window.scrollTo(0, 0)
17 | actions.getTweets()
18 | }, [])
19 |
20 | //used for contenteditable divs on react hooks
21 | const tweetT = useRef('');
22 | const handleChange = (evt, e) => {
23 | if (tweetT.current.trim().length <= 280
24 | && tweetT.current.split(/\r\n|\r|\n/).length <= 30) {
25 | tweetT.current = evt.target.value;
26 | setTweetText(tweetT.current)
27 | }
28 | // document.getElementById('tweet-box').innerHTML = document.getElementById('tweet-box').innerHTML.replace(/(\#\w+)/g, '$1')
29 | };
30 | const [tweetText, setTweetText] = useState("")
31 | const [tweetImage, setTweetImage] = useState(null)
32 | const [imageLoaded, setImageLoaded] = useState(false)
33 | const [imageLoading, setImageLoading] = useState(false)
34 |
35 | const submitTweet = () => {
36 | if (!tweetText.length) { return }
37 |
38 | let hashtags = tweetText.match(/#(\w+)/g)
39 |
40 | const values = {
41 | description: tweetText,
42 | images: [tweetImage],
43 | hashtags
44 | }
45 | actions.tweet(values)
46 | tweetT.current = ''
47 | setTweetText('')
48 | setTweetImage(null)
49 | }
50 |
51 | const onchangefile = () => {
52 | setImageLoading(true)
53 | let file = document.getElementById('file').files[0];
54 |
55 | let bodyFormData = new FormData()
56 | bodyFormData.append('image', file)
57 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}` } })
58 | .then(res => {
59 | setTweetImage(res.data.imageUrl)
60 | setImageLoading(false)
61 | })
62 | .catch(err => alert('error uploading image'))
63 | }
64 |
65 | const removeImage = () => {
66 | document.getElementById('file').value = "";
67 | setTweetImage(null)
68 | setImageLoaded(false)
69 | }
70 |
71 | return (
72 |
73 |
74 |
75 | Latest Tweets
76 |
77 |
78 | {session ?
79 |
80 |
81 |
82 | {account &&

}
83 |
84 |
85 |
document.getElementById('tweet-box').focus()} className="Tweet-input-side">
86 |
87 | e.preventDefault()} id="tweet-box" className={tweetText.length ? 'tweet-input-active' : null} onKeyDown={(e)=>tweetT.current.length>279 ? e.keyCode !== 8 && e.preventDefault(): null} placeholder="What's happening?" html={tweetT.current} onChange={(e) => handleChange(e)} />
88 |
89 |
90 | {imageLoading ? : null}
91 |
92 | {tweetImage &&
93 |
![]()
setImageLoaded(true)} className="tweet-upload-image" src={tweetImage} alt="tweet" />
94 | {imageLoaded &&
x}
95 |
}
96 |
97 |
98 |
99 |
100 | onchangefile()} />
101 |
102 |
103 |
104 |
= 280 ? 'red' : null }}>
105 | {tweetText.length > 0 && tweetText.length + '/280'}
106 |
107 |
108 | Tweet
109 |
110 |
111 |
112 |
113 |
: null }
114 |
115 | {/* { state.account &&
} */}
117 | {state.tweets.length > 0 ? state.tweets.map(t => {
118 | return
120 | }) :
}
121 |
122 | )
123 | }
124 |
125 | export default Home
--------------------------------------------------------------------------------
/src/components/Home/style.scss:
--------------------------------------------------------------------------------
1 | .Home-wrapper{
2 | max-width: 600px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | //but doesnt show when there are less elements
6 | // height: 100vh;
7 | display: flex;
8 | flex-direction: column;
9 | min-height: 2000px;
10 | }
11 |
12 | .Home-header-wrapper{
13 | position: sticky;
14 | border-bottom: 1px solid rgb(230, 236, 240);
15 | border-left: 1px solid rgb(230, 236, 240);
16 | background-color: rgb(255, 255, 255);
17 | z-index: 3;
18 | top: 0px;
19 | display: flex;
20 | align-items: center;
21 | cursor: pointer;
22 | height: 53px;
23 | min-height: 53px;
24 | padding-left: 15px;
25 | padding-right: 15px;
26 | max-width: 1000px;
27 | margin: 0 auto;
28 | width: 100%;
29 | }
30 |
31 | .Home-header{
32 | font-weight: 800;
33 | font-size: 19px;
34 | color: rgb(20, 23, 26);
35 | line-height: 1.3125;
36 | }
37 |
38 | .blue{
39 | color: rgb(29, 161, 242);
40 | }
--------------------------------------------------------------------------------
/src/components/ListPage/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useState, useContext, useRef } from 'react'
2 | import './style.scss'
3 | import { withRouter, Link } from 'react-router-dom'
4 | import { StoreContext } from '../../store/store'
5 | import Loader from '../Loader'
6 | import TweetCard from '../TweetCard'
7 | import {API_URL} from '../../config'
8 | import axios from 'axios'
9 | import {ICON_ARROWBACK, ICON_UPLOAD, ICON_CLOSE,ICON_SEARCH } from '../../Icons'
10 |
11 | const ListPage = (props) => {
12 |
13 | const { state, actions } = useContext(StoreContext)
14 | const [modalOpen, setModalOpen] = useState(false)
15 |
16 | const [editName, setName] = useState('')
17 | const [editDescription, setDescription] = useState('')
18 | const [banner, setBanner] = useState('')
19 | const [saved, setSaved] = useState(false)
20 | const [memOpen, setMemOpen] = useState(false)
21 | const [tab, setTab] = useState('Members')
22 | const [bannerLoading, setBannerLoading] = useState(false)
23 | const [styleBody, setStyleBody] = useState(false)
24 | const {account, list, listTweets, resultUsers} = state
25 |
26 | useEffect(() => {
27 | window.scrollTo(0, 0)
28 | actions.getList(props.match.params.id)
29 | }, [])
30 |
31 | const isInitialMount = useRef(true);
32 | useEffect(() => {
33 | if (isInitialMount.current){ isInitialMount.current = false }
34 | else {
35 | document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
36 | }
37 | }, [styleBody])
38 |
39 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
40 |
41 | const editList = () => {
42 | let values = {
43 | id: props.match.params.id,
44 | name: editName,
45 | description: editDescription,
46 | banner: banner
47 | }
48 | actions.editList(values)
49 | setSaved(true)
50 | toggleModal()
51 | }
52 |
53 | const toggleModal = (param) => {
54 | if(param === 'edit'){setSaved(false)}
55 | if(param === 'members'){setMemOpen(true)}
56 | if(param === 'close'){setMemOpen(false)}
57 | setStyleBody(!styleBody)
58 | setTimeout(()=>{ setModalOpen(!modalOpen) },20)
59 | }
60 |
61 | const handleModalClick = (e) => {
62 | e.stopPropagation()
63 | }
64 |
65 | const uploadImage = (file) => {
66 | let bodyFormData = new FormData()
67 | bodyFormData.append('image', file)
68 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}`}})
69 | .then(res=>{
70 | setBanner(res.data.imageUrl)
71 | setBannerLoading(false)
72 | })
73 | .catch(err=>alert('error uploading image'))
74 | }
75 |
76 | const changeBanner = () => {
77 | setBannerLoading(true)
78 | let file = document.getElementById('banner').files[0];
79 | uploadImage(file)
80 | }
81 |
82 | const deleteList = () => {
83 | actions.deleteList(props.match.params.id)
84 | props.history.push('/lists')
85 | }
86 |
87 | const goToUser = (id) => {
88 | props.history.push(`/profile/${id}`)
89 | }
90 |
91 | const searchOnChange = (param) => {
92 | if(param.length>0){
93 | actions.searchUsers({username: param})
94 | }
95 | }
96 |
97 | const addToList = (e,username,userId, profileImg,name) => {
98 | e.stopPropagation()
99 | let values = {id: props.match.params.id, username, userId, profileImg,name}
100 | actions.addToList(values)
101 | }
102 |
103 | return(
104 |
105 | {list ?
106 |
107 |
108 |
109 |
110 |
window.history.back()} className="header-back-wrapper">
111 |
112 |
113 |
114 |
115 |
116 | {list.name}
117 |
118 |
119 | @{list.user.username}
120 |
121 |
122 |
123 |
124 |

0 ? banner : list.banner.length>0? list.banner : "https://pbs-o.twimg.com/media/EXZ3BXhUwAEFNBE?format=png&name=small" } alt="list-banner"/>
125 |
126 |
127 |
{saved && editName.length>0 ? editName : list.name}
128 | {list.description.length> 0 || saved ?
{saved && editDescription.length>0 ? editDescription : list.description }
: null }
129 |
130 |
{list.user.name}
131 |
@{list.user.username}
132 |
133 |
toggleModal('members')} className="list-owner-wrap Members">
134 |
{list.users.length}
135 |
Members
136 |
137 |
toggleModal('edit')} className="listp-edit-btn">
138 | Edit List
139 |
140 |
141 | {listTweets && listTweets.map(t=>{
142 | return
143 | })}
144 |
145 |
toggleModal('close')} style={{display: modalOpen ? 'block' : 'none'}} className="modal-edit">
146 |
handleModalClick(e)} className="modal-content">
147 |
148 |
149 |
toggleModal('close')} className="modal-closeIcon-wrap">
150 |
151 |
152 |
153 |
{memOpen ? 'List members' : 'Edit List'}
154 | {memOpen ? null :
155 |
156 | Done
157 |
158 |
}
159 |
160 | {memOpen ?
161 |
162 |
163 |
setTab('Members')} className={tab =='Members' ? `explore-nav-item activeTab` : `explore-nav-item`}>
164 | Members ({list.users.length})
165 |
166 |
setTab('Search')} className={tab =='Search' ? `explore-nav-item activeTab` : `explore-nav-item`}>
167 | Search
168 |
169 |
170 |
171 | {tab === 'Members' ?
172 | list.users.map(u=>{
173 | return
goToUser(u.username)} key={u._id} className="search-result-wapper">
174 |
175 |

176 |
177 |
178 |
179 |
180 |
{u.name}
181 |
@{u.username}
182 |
183 | {u._id === account._id ? null :
184 |
addToList(e,u.username,u._id,u.profileImg,u.name)} className={list.users.some(x => x._id === u._id) ? "follow-btn-wrap Remove-switch":"follow-btn-wrap"}>
185 | {list.users.some(x => x._id === u._id) ? 'Remove' : 'Add'}
186 |
}
187 |
188 |
189 | {/* {account.description.substring(0,160)} */}
190 |
191 |
192 |
193 | })
194 | :
195 |
196 |
197 |
198 |
199 |
200 |
201 | searchOnChange(e.target.value)} placeholder="Search People" type="text" name="search"/>
202 |
203 |
204 | {resultUsers.length ? resultUsers.map(u=>{
205 | return
goToUser(u.username)} key={u._id} className="search-result-wapper">
206 |
207 |

208 |
209 |
210 |
211 |
212 |
{u.name}
213 |
@{u.username}
214 |
215 | {u._id === account._id ? null :
216 |
addToList(e,u.username,u._id, u.profileImg,u.name)} className={list.users.some(x => x._id === u._id) ? "follow-btn-wrap Remove-switch":"follow-btn-wrap"}>
217 | {list.users.some(x => x._id === u._id) ? 'Remove' : 'Add'}
218 |
}
219 |
220 |
221 | {u.description.substring(0,160)}
222 |
223 |
224 |
225 | }) : null}
226 |
}
227 |
228 |
229 | :
230 |
231 |
232 | {list.banner.length>0 || banner.length> 0 ?

0 ? banner : list.banner} alt="modal-banner" />: null}
233 |
234 |
235 | changeBanner()} title=" " id="banner" style={{opacity:'0'}} type="file"/>
236 |
237 |
238 |
252 |
253 | Delete List
254 |
255 |
}
256 |
257 |
258 |
259 |
:
}
260 |
261 | )
262 | }
263 |
264 | export default withRouter(ListPage)
--------------------------------------------------------------------------------
/src/components/ListPage/style.scss:
--------------------------------------------------------------------------------
1 | .listp-banner{
2 | max-height: 200px;
3 | height: 200px;
4 | width: 100%;
5 | img{
6 | width: 100%;
7 | height: 100%;
8 | object-fit: cover;
9 | }
10 | }
11 |
12 | .listp-details-wrap{
13 | padding: 10px 10px 0 10px;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | border-bottom: 1px solid rgb(230, 236, 240);;
19 | }
20 |
21 | .list-owner-wrap{
22 | display: flex;
23 | align-items: center;
24 | margin-top: 10px;
25 | cursor: pointer;
26 | &:hover{
27 | h4{text-decoration: underline;}
28 | }
29 | div{
30 | margin-left: 5px;
31 | color: rgb(101, 119, 134);
32 | }
33 | }
34 |
35 | .Members:hover{
36 | text-decoration: underline;
37 | }
38 |
39 | .listp-edit-btn{
40 | margin: 20px 0;
41 | min-width: 92px;
42 | min-height: 39px;
43 | display: flex;
44 | font-weight: bold;
45 | color: rgb(29, 161, 242);
46 | justify-content: center;
47 | align-items: center;
48 | text-align: center;
49 | cursor: pointer;
50 | border: 1px solid rgb(29, 161, 242);
51 | border-radius: 9999px;
52 | transition: 0.2s ease-in-out;
53 | &:hover{
54 | background-color: rgba(29,161,242,0.1);
55 | }
56 | }
57 |
58 | .list-description{
59 | margin-top: 10px;
60 | }
61 |
62 |
63 | .modal-delete-box{
64 | display: flex;
65 | justify-content: center;
66 | align-items: center;
67 | color: rgb(224, 36, 94);
68 | border-top: 1px solid rgb(230, 236, 240);
69 | border-bottom: 1px solid rgb(230, 236, 240);
70 | padding: 15px;
71 | min-height: 49px;
72 | cursor: pointer;
73 | transition: 0.1s ease-in-out;
74 | will-change: background-color;
75 | margin-top: 30px;
76 | &:hover{
77 | font-weight: 600;
78 | background-color: rgba(224, 36, 94, 0.1);
79 | }
80 | }
81 |
82 |
83 | .no-b-border{
84 | border-bottom: 1px solid transparent !important;
85 | }
86 |
87 | .Remove-switch{
88 | background-color: rgb(224, 36, 94) !important;
89 | border-color: rgb(224, 36, 94) !important;
90 | span{
91 | color: #fff !important;
92 | }
93 | }
--------------------------------------------------------------------------------
/src/components/Lists/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useState, useContext, useRef } from 'react'
2 | import './style.scss'
3 | import { Link, withRouter } from 'react-router-dom'
4 | import { StoreContext } from '../../store/store'
5 | import {API_URL} from '../../config'
6 | import axios from 'axios'
7 | import {ICON_ARROWBACK, ICON_CLOSE, ICON_NEWLIST, ICON_UPLOAD} from '../../Icons'
8 |
9 | const Lists = (props) => {
10 |
11 |
12 |
13 |
14 | const { state, actions } = useContext(StoreContext)
15 | const [modalOpen, setModalOpen] = useState(false)
16 | const [name, setName] = useState('')
17 | const [description, setDescription] = useState('')
18 | const [banner, setBanner] = useState('')
19 | const [styleBody, setStyleBody] = useState(false)
20 |
21 | const {account, lists} = state
22 |
23 | useEffect(() => {
24 | window.scrollTo(0, 0)
25 | actions.getLists()
26 | }, [])
27 |
28 | const isInitialMount = useRef(true);
29 | useEffect(() => {
30 | if (isInitialMount.current){ isInitialMount.current = false }
31 | else {
32 | document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
33 | }
34 | }, [styleBody])
35 |
36 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
37 |
38 | const createList = () => {
39 | let values = { name, description, banner }
40 | actions.createList(values)
41 | toggleModal()
42 | }
43 |
44 | const toggleModal = () => {
45 | setStyleBody(!styleBody)
46 | setTimeout(()=>{ setModalOpen(!modalOpen) },20)
47 | }
48 |
49 | const handleModalClick = (e) => {
50 | e.stopPropagation()
51 | }
52 |
53 | const uploadImage = (file) => {
54 | let bodyFormData = new FormData()
55 | bodyFormData.append('image', file)
56 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}`}})
57 | .then(res=>{setBanner(res.data.imageUrl)})
58 | .catch(err=>alert('error uploading image'))
59 | }
60 |
61 | const changeBanner = () => {
62 | let file = document.getElementById('banner').files[0];
63 | uploadImage(file)
64 | }
65 |
66 |
67 |
68 | return(
69 |
70 |
71 |
72 |
73 |
window.history.back()} className="header-back-wrapper">
74 |
75 |
76 |
77 |
78 |
79 | Your Lists
80 |
81 |
82 | @{account && account.username}
83 |
84 |
85 |
toggleModal()} className="newlist-icon-wrap">
86 | new list
87 |
88 |
89 | {lists.map(l=>{
90 | return
91 |
92 |

0 ? l.banner : "https://pbs-o.twimg.com/media/EXZ3BXhUwAEFNBE?format=png&name=small"} alt="list"/>
93 |
94 |
95 |
{l.name}
96 |
97 |
{account && account.name}
98 |
@{account && account.username}
99 |
100 |
101 |
102 | })}
103 |
104 | {/* add loader for bookmarks when empty using dispatch */}
105 | {/* {bookmarks.map(t=>{
106 | console.log(t)
107 | return
108 | })} */}
109 |
110 |
toggleModal()} style={{display: modalOpen ? 'block' : 'none'}} className="modal-edit">
111 |
handleModalClick(e)} className="modal-content">
112 |
113 |
114 |
toggleModal()} className="modal-closeIcon-wrap">
115 |
116 |
117 |
118 |
Create a new List
119 |
120 |
121 | Create
122 |
123 |
124 |
125 |
126 |
127 | {banner.length>0 &&

}
128 |
129 |
130 | changeBanner()} title=" " id="banner" style={{opacity:'0'}} type="file"/>
131 |
132 |
133 |
147 |
148 |
149 |
150 |
151 |
152 | )
153 | }
154 |
155 | export default withRouter(Lists)
--------------------------------------------------------------------------------
/src/components/Lists/style.scss:
--------------------------------------------------------------------------------
1 | .profile-header-back{
2 | min-width: 55px;
3 | min-height: 30px;
4 | justify-content: center;
5 | align-items: flex-start;
6 | }
7 |
8 |
9 |
10 | .header-back-wrapper:hover{
11 | background-color: rgba(29,161,242,0.1);
12 | }
13 |
14 | ////////////////
15 |
16 |
17 |
18 | .list-card-wrapper{
19 | padding: 10px 15px;
20 | border-bottom: 1px solid rgb(230, 236, 240);
21 | display: flex;
22 | align-items: center;
23 | cursor: pointer;
24 | transition: 0.2s ease-in-out;
25 | &:hover{
26 | background-color: rgb(245,248,250);
27 | }
28 | }
29 |
30 | .list-img-wrap{
31 | margin-right: 15px;
32 | height: 49px;
33 | width: 49px;
34 | border-radius: 14px;
35 | img{
36 | width: 100%;
37 | height: 100%;
38 | object-fit: cover;
39 | border-radius: 14px;
40 | }
41 | }
42 |
43 | .list-content-wrap{
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | height: 40px;
48 | align-items: flex-start;
49 | }
50 |
51 | .list-details-wrap{
52 | display: flex;
53 | align-items: center;
54 | margin-top: 2px;
55 | div{
56 | margin-left: 5px;
57 | font-size: 13px;
58 | line-height: 1.5;
59 | color: rgb(101, 119, 134);
60 | }
61 | }
62 |
63 | .newlist-icon-wrap{
64 | display: flex;
65 | margin-left: auto;
66 | span{
67 | margin-right: 5px;
68 | font-weight: 500;
69 | line-height: 1.5;
70 |
71 | }
72 | }
73 |
74 | .newlist-icon-wrap svg{
75 | fill: rgb(29, 161, 242);
76 | width: 22.5px;
77 | height: 22.5px;
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.css'
3 |
4 | const Loader = () => {
5 | return(
6 |
7 |
19 |
20 | )
21 | }
22 |
23 | export default Loader
--------------------------------------------------------------------------------
/src/components/Loader/style.css:
--------------------------------------------------------------------------------
1 |
2 | .loader-wrapper{
3 | position: relative;
4 | height: 50%;
5 | margin: 50px 0;
6 | }
7 |
8 | .loader_svg{
9 | fill: yellow;
10 | enable-background:new 0 0 50 50;
11 | position: absolute;
12 | left: 50%;
13 | top: 15%;
14 | transform: translate(-50%,50%);
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Login/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 | import { Link, Redirect } from 'react-router-dom'
5 | import { ICON_LOGO } from '../../Icons'
6 |
7 | const LoginPage = () => {
8 | const { state, actions } = useContext(StoreContext)
9 |
10 | const [username, setUsername] = useState('')
11 | const [password, setPassword] = useState('')
12 |
13 | const Login = (e) => {
14 | e.preventDefault()
15 | if(username.length && password.length){
16 | const values = {
17 | username,
18 | password
19 | }
20 | actions.login(values)
21 | }
22 | }
23 |
24 | return(
25 |
26 | {state.loggedin &&
}
27 |
28 |
29 | Log in to Twitter
30 |
31 | {state.msg === 'Incorrect email or password' &&
The username/email or password you entered is incorrect.
}
32 |
49 |
50 |
51 | Sign up for Twitter
52 |
53 |
54 |
55 | )
56 | }
57 |
58 | export default LoginPage
--------------------------------------------------------------------------------
/src/components/Login/style.scss:
--------------------------------------------------------------------------------
1 | .login-wrapper{
2 | max-width: 600px;
3 | padding: 0 15px;
4 | margin: 20px auto 0 auto;
5 | }
6 |
7 | .login-wrapper svg{
8 | height: 39px;
9 | margin: 0 auto;
10 | display: block;
11 | }
12 |
13 | .login-header{
14 | margin-top: 30px;
15 | font-size: 23px;
16 | margin-bottom: 10px;
17 | font-weight: bold;
18 | text-align: center;
19 | }
20 |
21 | .login-form{
22 | width: 100%;
23 | }
24 |
25 | .login-input{
26 | background-color: inherit;
27 | border: inherit;
28 | }
29 |
30 | .login-input:focus{
31 | background-color: inherit;
32 | border: inherit;
33 | }
34 |
35 | .login-input-wrap{
36 | padding: 10px 15px;
37 | }
38 |
39 | .login-input-content{
40 | border-bottom: 2px solid rgb(64, 67, 70);
41 | background-color: #e3e3e4;
42 | label{
43 | display: block;
44 | padding: 5px 10px 0 10px;
45 | }
46 | input{
47 | width: 100%;
48 | outline: none;
49 | font-size: 19px;
50 | padding: 2px 10px 5px 10px;
51 | }
52 | }
53 |
54 | .login-btn-wrap{
55 | width: calc(100% - 20px);
56 | min-height: 49px;
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | transition: 0.2s ease-in-out;
61 | margin: 10px;
62 | padding: 0 30px;
63 | border-radius: 9999px;
64 | background-color: rgb(29, 161, 242);
65 | opacity: 0.5;
66 | color: #fff;
67 | font-weight: bold;
68 | outline: none;
69 | border: 1px solid rgba(0,0,0,0);
70 | }
71 |
72 | .signup-option{
73 | margin-top: 20px;
74 | font-size: 15px;
75 | color: rgb(29, 161, 242);
76 | text-align: center;
77 | &:hover{
78 | text-decoration: underline;
79 | cursor: pointer;
80 | }
81 | }
82 |
83 | .button-active{
84 | opacity: 1;
85 | cursor: pointer;
86 | }
87 |
88 | .login-error{
89 | text-align: center;
90 | color: red;
91 | }
--------------------------------------------------------------------------------
/src/components/Messages/index.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useContext} from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import {withRouter, Link} from 'react-router-dom'
4 | import './style.scss'
5 | import moment from 'moment'
6 | import {useMediaQuery} from 'react-responsive'
7 | import Chat from '../ChatPage'
8 |
9 | const Messages = (props) => {
10 | const { state, actions } = useContext(StoreContext)
11 | const {account, conversations} = state
12 | const path = props.history.location.pathname
13 |
14 | useEffect(() => {
15 | actions.getConversations()
16 | document.getElementsByTagName("body")[0].style.cssText = "position:fixed; overflow-y: scroll;"
17 | },[path])
18 |
19 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
20 |
21 | const isTabletOrMobile = useMediaQuery({ query: '(max-width: 888px)' })
22 | return(
23 |
24 | {isTabletOrMobile && path !== '/messages' && account ?
25 | :
26 |
27 |
28 | Messages
29 |
30 |
31 |
32 | {account && conversations && conversations.conversations.length>0 ? conversations.conversations.map(con=>{
33 | return
props.history.push(`/messages/${con._id}`)} className="message-box">
34 |
e.stopPropagation()} to={`/profile/${con.participants[0].username !== account.username ?
35 | con.participants[0].username : con.participants[1].username}`} className="message-avatar">
36 |

38 |
39 | {account &&
40 |
41 |
42 | {con.participants[0].username !== account.username ?
43 |
{con.participants[0].name}@{con.participants[0].username}
:
44 |
{con.participants[1].name}@{con.participants[1].username}
}
45 |
{moment(con.updatedAt).format("MMM D, YYYY")}
46 |
47 |
48 | {con.messages.length>0 && con.messages[con.messages.length - 1].content.slice(0,15)}
49 |
50 |
}
51 |
52 | }):
You have no messages
}
53 |
54 |
55 |
}
56 |
57 | )
58 | }
59 |
60 | export default withRouter(Messages)
--------------------------------------------------------------------------------
/src/components/Messages/style.scss:
--------------------------------------------------------------------------------
1 | .messages-wrapper{
2 | width: 400px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | color: #657786;
5 | min-height: 100vh;
6 | }
7 |
8 | .messages-header-wrapper{
9 | position: sticky;
10 | border-bottom: 1px solid rgb(230, 236, 240);
11 | border-left: 1px solid rgb(230, 236, 240);
12 | background-color: #fff;
13 | z-index: 3;
14 | top: 0px;
15 | display: flex;
16 | align-items: center;
17 | height: 53px;
18 | min-height: 53px;
19 | padding-left: 15px;
20 | padding-right: 15px;
21 | max-width: 400px;
22 | margin: 0 auto;
23 | width: 100%;
24 | font-weight: 800;
25 | font-size: 19px;
26 | color: #14171a;
27 | }
28 |
29 | .recent-messages-wrapper{
30 | display: flex;
31 | flex-direction: column;
32 | overflow: auto;
33 | height: calc(100vh - 96px);
34 | }
35 |
36 | .message-box{
37 | display: flex;
38 | padding: 15px;
39 | transition: 0.2s ease-in-out;
40 | border-bottom: 1px solid rgb(230, 236, 240);
41 | &:hover{
42 | cursor: pointer;
43 | background-color: rgb(245, 248, 250);
44 | }
45 | }
46 |
47 | .message-avatar{
48 | flex-basis: 49px;
49 | min-width: 49px;
50 | margin-right: 10px;
51 | img{
52 | border-radius: 50%;
53 | max-height: 49px;
54 | object-fit: cover;
55 | }
56 | }
57 |
58 | .message-details{
59 | display: flex;
60 | flex-direction: column;
61 | width: 100%;
62 | }
63 |
64 | .message-info{
65 | display: flex;
66 | justify-content: space-between;
67 | white-space: nowrap;
68 | overflow: hidden;
69 | div{
70 | max-width: 210px;
71 | overflow: hidden;
72 | font-weight: 800;
73 | color: #000;
74 | }
75 |
76 | span{
77 | font-weight: 400;
78 | margin-left: 3px;
79 | color: rgb(101, 119, 134);
80 | }
81 | }
--------------------------------------------------------------------------------
/src/components/Nav/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useState, useContext, useRef } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import { Link, withRouter, Redirect } from 'react-router-dom'
4 | import './style.scss'
5 | import { ICON_LOGO, ICON_HOME, ICON_HASH, ICON_BELL, ICON_INBOX
6 | ,ICON_BOOKMARK, ICON_LIST, ICON_USER, ICON_SETTINGS, ICON_HOMEFILL, ICON_HASHFILL,
7 | ICON_BELLFILL, ICON_BOOKMARKFILL, ICON_LISTFILL, ICON_USERFILL, ICON_FEATHER,
8 | ICON_CLOSE,ICON_IMGUPLOAD, ICON_INBOXFILL, ICON_LIGHT, ICON_DARK } from '../../Icons'
9 | import axios from 'axios'
10 | import {API_URL} from '../../config'
11 | import ContentEditable from 'react-contenteditable'
12 | import {
13 | enable as enableDarkMode,
14 | disable as disableDarkMode,
15 | setFetchMethod
16 | } from 'darkreader';
17 |
18 | const Nav = ({history}) => {
19 | const { state, actions } = useContext(StoreContext)
20 |
21 | const { account, session } = state
22 | const [moreMenu, setMoreMenu] = useState(false)
23 | const [theme, setTheme] = useState(true)
24 | const [modalOpen, setModalOpen] = useState(false)
25 | const [styleBody, setStyleBody] = useState(false)
26 | const [tweetText, setTweetText] = useState('')
27 | const [tweetImage, setTweetImage] = useState(null)
28 | const [imageLoaded, setImageLoaded] = useState(false)
29 |
30 | const tweetT = useRef('');
31 |
32 | const isInitialMount = useRef(true);
33 | useEffect(() => {
34 | if (isInitialMount.current){ isInitialMount.current = false }
35 | else {
36 | document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
37 | }
38 | }, [styleBody])
39 |
40 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
41 |
42 | useEffect(()=>{
43 | let ran = false
44 | history.listen((location, action) => {
45 | state.account == null ? actions.verifyToken('get account') : actions.verifyToken()
46 | });
47 | !ran && state.account == null ? actions.verifyToken('get account') : actions.verifyToken()
48 | if(localStorage.getItem('Theme')=='dark'){
49 | setTheme('dark')
50 | setFetchMethod(window.fetch)
51 | enableDarkMode();
52 | }else if(!localStorage.getItem('Theme')){
53 | localStorage.setItem('Theme', 'light')
54 | }
55 | }, [])
56 |
57 | const path = history.location.pathname.slice(0,5)
58 |
59 | const openMore = () => { setMoreMenu(!moreMenu) }
60 |
61 | const handleMenuClick = (e) => { e.stopPropagation() }
62 |
63 |
64 |
65 | const uploadImage = (file) => {
66 | let bodyFormData = new FormData()
67 | bodyFormData.append('image', file)
68 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}`}})
69 | .then(res=>{setTweetImage(res.data.imageUrl)})
70 | .catch(err=>alert('error uploading image'))
71 | }
72 |
73 |
74 | const onchangeImage = () => {
75 | let file = document.getElementById('image').files[0];
76 | uploadImage(file)
77 | }
78 |
79 | const removeImage = () => {
80 | document.getElementById('image').value = "";
81 | setTweetImage(null)
82 | setImageLoaded(false)
83 | }
84 |
85 | const toggleModal = (e, type) => {
86 | if(e){ e.stopPropagation() }
87 | setStyleBody(!styleBody)
88 | setTimeout(()=>{ setModalOpen(!modalOpen) },20)
89 | }
90 |
91 | const handleModalClick = (e) => {
92 | e.stopPropagation()
93 | }
94 |
95 |
96 | const handleChange = evt => {
97 | if(tweetT.current.trim().length <= 280
98 | && tweetT.current.split(/\r\n|\r|\n/).length <= 30){
99 | tweetT.current = evt.target.value;
100 | setTweetText(tweetT.current)
101 | }
102 | };
103 |
104 | const submitTweet = (type) => {
105 |
106 | let hashtags = tweetText.match(/#(\w+)/g)
107 | toggleModal()
108 | if(!tweetText.length){return}
109 | const values = {
110 | description: tweetText,
111 | images: [tweetImage],
112 | hashtags
113 | }
114 | actions.tweet(values)
115 | tweetT.current = ''
116 | setTweetText('')
117 | setTweetImage(null)
118 | }
119 |
120 | const changeTheme = () => {
121 | if(localStorage.getItem('Theme') === 'dark'){
122 | disableDarkMode()
123 | localStorage.setItem('Theme', 'light')
124 | }else if(localStorage.getItem('Theme') === 'light'){
125 | localStorage.setItem('Theme', 'dark')
126 | setFetchMethod(window.fetch)
127 | enableDarkMode();
128 | }
129 | }
130 |
131 | return(
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | {path === '/home' ?
:
}
143 |
Home
144 |
145 |
146 |
147 |
148 | {path === '/expl' ?
:
}
149 |
Explore
150 |
151 |
152 | {session ?
153 | <>
154 |
155 |
156 | {path === '/noti' ?
:
}
157 |
Notifications
158 |
159 |
160 |
161 |
162 | {path === '/mess' ?
:
}
163 |
Messages
164 |
165 |
166 |
167 |
168 | {path === '/book' ?
:
}
169 |
Bookmarks
170 |
171 |
172 |
173 |
174 | {path === '/list' ?
:
}
175 |
Lists
176 |
177 |
178 |
179 |
180 | {path === '/prof' ?
:
}
181 |
Profile
182 |
183 |
184 | > : null}
185 |
214 | {session ?
215 |
216 |
toggleModal()} className="Nav-tweet-link">
217 |
218 | Tweet
219 |
220 |
221 |
222 |
223 |
224 |
: null }
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | {account &&
234 |
toggleModal()} style={{display: modalOpen ? 'block' : 'none'}} className="modal-edit">
235 |
handleModalClick(e)} className="modal-content">
236 |
237 |
238 |
toggleModal()} className="modal-closeIcon-wrap">
239 |
240 |
241 |
242 |
Tweet
243 |
244 |
245 |
246 |
247 |
248 |

249 |
250 |
251 |
document.getElementById('tweetPop').focus()} className="Tweet-input-side">
252 |
253 | tweetT.current.length>279 ? e.keyCode !== 8 && e.preventDefault(): null} id="tweetPop" onPaste={(e)=>e.preventDefault()} style={{minHeight: '120px'}} className={tweetText.length ? 'tweet-input-active' : null} placeholder="What's happening" html={tweetT.current} onChange={handleChange} />
254 |
255 | {tweetImage &&
256 |
![]()
setImageLoaded(true)} className="tweet-upload-image" src={tweetImage} alt="tweet image" />
257 | {imageLoaded &&
x}
258 |
}
259 |
260 |
261 |
262 |
263 | onchangeImage()} />
264 |
265 |
266 |
267 |
= 280 ? 'red' : null }}>
268 | {tweetText.length > 0 && tweetText.length + '/280'}
269 |
270 |
submitTweet('none')} className={tweetText.length ? 'tweet-btn-side tweet-btn-active' : 'tweet-btn-side'}>
271 | Tweet
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
}
280 |
281 | )
282 | }
283 |
284 | export default withRouter(Nav)
--------------------------------------------------------------------------------
/src/components/Nav/style.scss:
--------------------------------------------------------------------------------
1 | .Nav-width{
2 | width: 275px;
3 | position: relative;
4 | }
5 |
6 | .Nav-component{
7 | position: relative;
8 | z-index: 200;
9 | }
10 |
11 | .Nav{
12 | top: 0;
13 | height: 100%;
14 | position: fixed;
15 | display: flex;
16 | align-items: flex-end;
17 | flex-direction: column;
18 | border-right: 1px solid rgb(230, 236, 240);
19 | }
20 |
21 | .Nav-Content{
22 | overflow-y: auto;
23 | display: flex;
24 | flex-direction: column;
25 | width: 275px;
26 | padding-right: 20px;
27 | padding-left: 20px;
28 | // justify-content: space-between;
29 | height: 100%;
30 | }
31 | .Nav-wrapper{
32 | display: flex;
33 | flex-direction: column;
34 | margin-top: 5px;
35 | }
36 | .logo-wrapper{
37 | min-width: 30px;
38 | cursor: pointer;
39 | margin-top: 11.5px;
40 | display: flex;
41 | margin-bottom: 15px;
42 | // align-items: center;
43 | // justify-content: center;
44 | }
45 |
46 |
47 | .Nav-link{
48 | padding: 7px 0;
49 | display: flex;
50 | cursor: pointer;
51 | &:hover{
52 | .Nav-item-hover{
53 | background-color: rgba(29,161,242, 0.1);
54 | color: rgb(29, 161, 242);
55 | svg{fill: rgb(29, 161, 242)}
56 | }
57 | }
58 | }
59 | .Nav-item-hover{
60 | svg{fill: rgb(16, 17, 17)}
61 | display: flex;
62 | align-items: center;
63 | padding: 10px;
64 | justify-content: center;
65 | max-width: 100%;
66 | border-radius: 9999px;
67 | transition-property: background-color, box-shadow;
68 | transition-duration: 0.2s;
69 | }
70 |
71 | .Nav-item-hover svg{
72 | width: 26.25px; height:26.25px;
73 | min-width: 26.25px;
74 | }
75 |
76 | .active-Nav{
77 | svg{
78 | fill: rgb(29, 161, 242);
79 | }
80 | .Nav-item{
81 | color: rgb(29, 161, 242);
82 | }
83 | }
84 |
85 |
86 | .Nav-item{
87 | font-size: 19px;
88 | font-weight: 700;
89 | margin-left: 20px;
90 | margin-right: 20px;
91 | }
92 | .Nav-tweet{
93 | width: 100%;
94 | margin-top: 15px;
95 | margin-bottom: 5px;
96 | display: flex;
97 |
98 | }
99 |
100 | .Nav-tweet-link{
101 | width: 90%;
102 | background-color: rgb(29, 161, 242);
103 | box-shadow: rgba(0, 0, 0, 0.08) 0px 8px 28px;
104 | outline-style: none;
105 | transition-property: background-color, box-shadow;
106 | transition-duration: 0.2s;
107 | min-width: 78.89px;
108 | min-height: 49px;
109 | padding-left: 30px;
110 | padding-right: 30px;
111 | border: 1px solid rgba(0, 0, 0, 0);
112 | cursor: pointer;
113 | display: flex;
114 | justify-content: center;
115 | align-items: center;
116 | border-radius: 9999px;
117 | }
118 |
119 | @media only screen and (max-width: 1286px) {
120 | .Nav-tweet-link{ width: 100%; }
121 | .Nav-tweet{justify-content: center;}
122 | }
123 | .Nav-tweet-btn{
124 | color: #fff;
125 | font-size: 15px;
126 | font-weight: bold;
127 | overflow-wrap: break-word;
128 | text-align: center;
129 | max-width: 100%;
130 | span{
131 | display: flex;
132 | align-items: center;
133 | justify-content: center;
134 | }
135 | content: 'Tweet';
136 | }
137 | .btn-show{
138 | display: none;
139 | }
140 | @media only screen and (max-width: 1282px) {
141 | .Nav-tweet-link{
142 | max-width: 49px;
143 | width: 49px;
144 | padding: 0;
145 | min-width: 49px;
146 | }
147 | .btn-hide{
148 | display: none;
149 | }
150 | .btn-show{
151 | display: block;
152 | }
153 | }
154 | .Nav-tweet-btn span svg{
155 | width: 22.25px; height:22.25px;
156 | min-width: 22.25px;
157 | fill: #fff;
158 | }
159 |
160 | .more-menu-background{
161 | position: fixed;
162 | z-index: 20;
163 | left: 0;
164 | top: 0;
165 | width: 100%;
166 | height: 100%;
167 | overflow: auto;
168 | cursor: auto;
169 | }
170 |
171 | .more-modal-wrapper{
172 | position: relative;
173 | width: 100%;
174 | height: 100%;
175 | }
176 |
177 | .more-menu-content{
178 | min-height: 100px;
179 | max-width: 40vw;
180 | max-height: 50vh;
181 | width: 190px;
182 | min-width: 190px;
183 | border-radius: 14px;
184 | background-color: #fff;
185 | position: absolute;
186 | // top: 46.5%;
187 | // left: 26%;
188 | z-index: 1000;
189 | // transform: translate(-50%, -50%);
190 | overflow: hidden;
191 | box-shadow: rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px;
192 | display: flex;
193 | flex-direction: column;
194 | }
195 |
196 | @media only screen and (min-width: 451px){
197 | .more-menu-content{
198 |
199 | height: 104px;
200 | }
201 | }
202 |
203 | .more-menu-item{
204 | border-bottom: 1px solid rgb(245, 248, 250);
205 | padding: 15px;
206 | display: flex;
207 | align-items: center;
208 | justify-content: space-between;
209 | transition: 0.2 ease-in-out;
210 | cursor: pointer;
211 | &:hover{
212 | background-color: rgb(245, 248, 250);
213 | }
214 | span{
215 | display: flex;
216 | align-items: center;
217 | }
218 |
219 | }
220 |
221 | .more-menu-item svg{
222 | width: 16px;
223 | }
224 |
225 | .more-item{
226 | display: none;
227 | }
228 |
229 |
230 | @media only screen and (max-width: 1282px) {
231 | .Nav-item{
232 | display: none;
233 | }
234 | .Nav-width{
235 | width: 88px;
236 | }
237 | .Nav-Content{
238 | width: 88px;
239 | }
240 | }
--------------------------------------------------------------------------------
/src/components/Notifications/index.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react'
2 | import './style.scss'
3 |
4 | const Notifications = () => {
5 |
6 | useEffect(() => {
7 | document.getElementsByTagName("body")[0].style.cssText = "position:fixed; overflow-y: scroll;"
8 | },[])
9 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
10 | return(
11 |
12 | This is a work in progress
13 |
14 | )
15 | }
16 |
17 | export default Notifications
--------------------------------------------------------------------------------
/src/components/Notifications/style.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ali-hd/Twitter-Clone/f04d5af799519ec22571594e6bed4073ef8be147/src/components/Notifications/style.scss
--------------------------------------------------------------------------------
/src/components/Profile/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useState, useContext, useRef} from 'react'
2 | import './style.scss'
3 | import { ICON_ARROWBACK, ICON_MARKDOWN, ICON_DATE, ICON_CLOSE, ICON_UPLOAD, ICON_NEWMSG } from '../../Icons'
4 | import { withRouter, Link } from 'react-router-dom'
5 | import { StoreContext } from '../../store/store'
6 | import Loader from '../Loader'
7 | import moment from 'moment'
8 | import TweetCard from '../TweetCard'
9 | import {API_URL} from '../../config'
10 | import axios from 'axios'
11 |
12 |
13 | const Profile = (props) => {
14 | const { state, actions } = useContext(StoreContext)
15 | const [activeTab, setActiveTab] = useState('Tweets')
16 | const [editName, setName] = useState('')
17 | const [editBio, setBio] = useState('')
18 | const [editLocation, setLocation] = useState('')
19 | const [modalOpen, setModalOpen] = useState(false)
20 | const [banner, setBanner] = useState('')
21 | const [avatar, setAvatar] = useState('')
22 | const [saved, setSaved] = useState(false)
23 | const [memOpen, setMemOpen] = useState(false)
24 | const [tab, setTab] = useState('Followers')
25 | const [loadingAvatar, setLoadingAvatar] = useState(false)
26 | const [loadingBanner, setLoadingBanner] = useState(false)
27 | const [styleBody, setStyleBody] = useState(false)
28 | const {account, user, session} = state
29 | const userParam = props.match.params.username
30 |
31 | useEffect(() => {
32 | window.scrollTo(0, 0)
33 | actions.getUser(props.match.params.username)
34 | //preventing edit modal from apprearing after clicking a user on memOpen
35 | setMemOpen(false)
36 | setModalOpen(false)
37 | }, [props.match.params.username])
38 |
39 | const isInitialMount = useRef(true);
40 | useEffect(() => {
41 | if (isInitialMount.current){ isInitialMount.current = false }
42 | else {
43 | document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
44 | }
45 | }, [styleBody])
46 |
47 | useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
48 |
49 | const changeTab = (tab) => {
50 | setActiveTab(tab)
51 | }
52 |
53 | const editProfile = () => {
54 | let values = {
55 | name: editName,
56 | description: editBio,
57 | location: editLocation,
58 | profileImg: avatar,
59 | banner: banner
60 | }
61 | actions.updateUser(values)
62 | setSaved(true)
63 | toggleModal()
64 | }
65 |
66 | const toggleModal = (param, type) => {
67 | setStyleBody(!styleBody)
68 | if(param === 'edit'){setSaved(false)}
69 | if(type){setTab(type)}
70 | if(param === 'members'){
71 | setMemOpen(true)
72 | actions.getFollowers(props.match.params.username)
73 | }
74 | if(memOpen){setMemOpen(false)}
75 | setTimeout(()=>{ setModalOpen(!modalOpen) },20)
76 | }
77 |
78 | const handleModalClick = (e) => {
79 | e.stopPropagation()
80 | }
81 |
82 | const followUser = (e,id) => {
83 | if(!session){ actions.alert('Please Sign In'); return }
84 | e.stopPropagation()
85 | actions.followUser(id)
86 | }
87 |
88 | const uploadImage = (file,type) => {
89 | let bodyFormData = new FormData()
90 | bodyFormData.append('image', file)
91 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}`}})
92 | .then(res=>{
93 | type === 'banner' ? setBanner(res.data.imageUrl) : setAvatar(res.data.imageUrl)
94 | type === 'banner' ? setLoadingBanner(false) : setLoadingAvatar(false)
95 | })
96 | .catch(err=>actions.alert('error uploading image'))
97 | }
98 |
99 | const changeBanner = () => {
100 | setLoadingBanner(true)
101 | let file = document.getElementById('banner').files[0];
102 | uploadImage(file, 'banner')
103 | }
104 | const changeAvatar = () => {
105 | setLoadingAvatar(true)
106 | let file = document.getElementById('avatar').files[0];
107 | uploadImage(file, 'avatar')
108 | }
109 |
110 | const goToUser = (id) => {
111 | setModalOpen(false)
112 | props.history.push(`/profile/${id}`)
113 | }
114 |
115 | const startChat = () => {
116 | if(!session){ actions.alert('Please Sign In'); return }
117 | actions.startChat({id:user._id, func: goToMsg})
118 | }
119 |
120 | const goToMsg = () => {
121 | props.history.push(`/messages`)
122 | }
123 |
124 |
125 | return(
126 |
127 | {user ?
128 |
129 |
130 |
131 |
132 |
window.history.back()} className="header-back-wrapper">
133 |
134 |
135 |
136 |
137 |
138 | {account && account.username === userParam ? account.username : user.username}
139 |
140 | {/*
141 | 82 Tweets
142 |
*/}
143 |
144 |
145 |
146 |

0 && saved ? banner : user.banner} alt=""/>
147 |
148 |
149 |
150 |
151 |

0 && saved ? avatar : user.profileImg} alt=""/>
152 |
153 | {account && account.username === userParam? null :
startChat()} className="new-msg">}
154 |
account && account.username === userParam ? toggleModal('edit'): followUser(e,user._id)}
155 | className={account && account.following.includes(user._id) ? 'unfollow-switch profile-edit-button' : 'profile-edit-button'}>
156 | {account && account.username === userParam?
157 | Edit profile :
158 | { account && account.following.includes(user._id) ? 'Following' : 'Follow'}}
159 |
160 |
161 |
162 |
{user.name}
163 |
@{account && account.username === userParam ? account.username : user.username}
164 |
165 | {user.description}
166 |
167 |
168 | {user.location.length>0 &&
169 |
}
170 |
0 ? "profile-location" : ''}> {user.location}
171 |
172 |
Joined {moment(user.createdAt).format("MMMM YYYY")}
173 |
174 |
175 |
176 |
toggleModal('members','Following')}>
177 |
{user.following.length}
178 |
Following
179 |
180 |
toggleModal('members', 'Followers')}>
181 |
{user.followers.length}
182 |
Followers
183 |
184 |
185 |
186 |
187 |
changeTab('Tweets')} className={activeTab ==='Tweets' ? `profile-nav-item activeTab` : `profile-nav-item`}>
188 | Tweets
189 |
190 |
changeTab('Tweets&Replies')} className={activeTab ==='Tweets&Replies' ? `profile-nav-item activeTab` : `profile-nav-item`}>
191 | Tweets & replies
192 |
193 |
changeTab('Media')} className={activeTab ==='Media' ? `profile-nav-item activeTab` : `profile-nav-item`}>
194 | Media
195 |
196 |
changeTab('Likes')} className={activeTab ==='Likes' ? `profile-nav-item activeTab` : `profile-nav-item`}>
197 | Likes
198 |
199 |
200 | {activeTab === 'Tweets' ?
201 | user.tweets.map(t=>{
202 | if(!t.parent)
203 | return
204 | }): activeTab === 'Tweets&Replies' ?
205 | user.tweets.map(t=>{
206 | if(t.parent)
207 | return
209 | }) :
210 | activeTab === 'Likes' ?
211 | user.likes.map(t=>{
212 | return
214 | }): activeTab === 'Media' ?
215 | user.tweets.map(t=>{
216 | if(t.images[0])
217 | return
219 | }): null}
220 |
221 |
toggleModal()} style={{display: modalOpen ? 'block' : 'none'}} className="modal-edit">
222 |
handleModalClick(e)} className="modal-content">
223 |
224 |
225 |
toggleModal()} className="modal-closeIcon-wrap">
226 |
227 |
228 |
229 |
{memOpen ? null : 'Edit Profile'}
230 | {memOpen ? null :
231 |
232 |
233 | Save
234 |
235 |
}
236 |
237 | {memOpen ?
238 |
239 |
setTab('Followers')} className={tab =='Followers' ? `explore-nav-item activeTab` : `explore-nav-item`}>
240 | Followers
241 |
242 |
setTab('Following')} className={tab =='Following' ? `explore-nav-item activeTab` : `explore-nav-item`}>
243 | Following
244 |
245 |
246 |
247 | {tab === 'Followers' ?
248 | state.followers.map(f=>{
249 | return
goToUser(f.username)} key={f._id} className="search-result-wapper">
250 |
251 |

252 |
253 |
254 |
255 |
256 |
{f.name}
257 |
@{f.username}
258 |
259 | {f._id === account && account._id ? null :
260 |
followUser(e,f._id)} className={account && account.following.includes(f._id) ? "follow-btn-wrap unfollow-switch":"follow-btn-wrap"}>
261 | {account && account.following.includes(f._id) ? 'Following' : 'Follow'}
262 |
}
263 |
264 |
265 | {f.description.substring(0,160)}
266 |
267 |
268 |
269 | })
270 | :
271 | state.following.map(f=>{
272 | return
goToUser(f.username)} key={f._id} className="search-result-wapper">
273 |
274 |

275 |
276 |
277 |
278 |
279 |
{f.name}
280 |
@{f.username}
281 |
282 | {f._id === account && account._id ? null :
283 |
followUser(e,f._id)} className={account && account.following.includes(f._id) ? "follow-btn-wrap unfollow-switch":"follow-btn-wrap"}>
284 | {account && account.following.includes(f._id) ? 'Following' : 'Follow'}
285 |
}
286 |
287 |
288 | {f.description.substring(0,160)}
289 |
290 |
291 |
292 | })
293 | }
294 |
295 |
:
296 |
297 |
298 |

0 ? banner : user.banner} alt="modal-banner" />
299 |
300 |
301 | changeBanner()} title=" " id="banner" style={{opacity:'0'}} type="file"/>
302 |
303 |
304 |
305 |
306 |

0 ? avatar : user.profileImg} alt="profile" />
307 |
308 |
309 | changeAvatar()} title=" " id="avatar" style={{opacity:'0'}} type="file"/>
310 |
311 |
312 |
313 |
333 |
}
334 |
335 |
336 |
:
}
337 |
338 | )
339 | }
340 |
341 | export default withRouter(Profile)
--------------------------------------------------------------------------------
/src/components/Profile/style.scss:
--------------------------------------------------------------------------------
1 | .profile-wrapper{
2 | max-width: 600px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | // height: 100vh;
6 | display: flex;
7 | flex-direction: column;
8 | min-height: 2000px;
9 | }
10 |
11 | .profile-header-wrapper{
12 | position: sticky;
13 | border-bottom: 1px solid rgb(230, 236, 240);
14 | border-left: 1px solid rgb(230, 236, 240);
15 | background-color: #fff;
16 | z-index: 8;
17 | top: 0px;
18 | display: flex;
19 | align-items: center;
20 | cursor: pointer;
21 | height: 53px;
22 | min-height: 53px;
23 | padding-left: 15px;
24 | padding-right: 15px;
25 | max-width: 1000px;
26 | margin: 0 auto;
27 | width: 100%;
28 | }
29 |
30 | .profile-header-back{
31 | min-width: 55px;
32 | min-height: 30px;
33 | justify-content: center;
34 | align-items: flex-start;
35 | }
36 |
37 | .header-back-wrapper{
38 | margin-left: -5px;
39 | width: 39px;
40 | height: 39px;
41 | transition: 0.2s ease-in-out;
42 | will-change: background-color;
43 | border: 1px solid rgba(0, 0, 0, 0);
44 | border-radius: 9999px;
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | }
49 | .header-back-wrapper svg{
50 | height: 1.5em;
51 | fill: rgb(29,161,242);
52 | }
53 | .header-back-wrapper:hover{
54 | background-color: rgba(29,161,242,0.1);
55 | }
56 |
57 | .profile-header-content{
58 | display: flex;
59 | flex-direction: column;
60 | }
61 |
62 | .profile-header-name{
63 | font-weight: 800;
64 | font-size: 19px;
65 | }
66 |
67 | .profile-header-tweets{
68 | font-size: 14px;
69 | line-height: calc(19.6875px);
70 | color: rgb(101, 119, 134);
71 | }
72 |
73 | .profile-banner-wrapper{
74 | max-width: 600px;
75 | height: 200px;
76 | position: relative;
77 | img{
78 | width: 100%;
79 | height: 100%;
80 | object-fit: cover;
81 | }
82 | }
83 |
84 | .profile-details-wrapper{
85 | padding: 10px 15px 15px 15px;
86 | }
87 |
88 | .profile-options{
89 | display: flex;
90 | justify-content: flex-end;
91 | align-items: center;
92 | position: relative;
93 | }
94 |
95 | .profile-image-wrapper{
96 | position: absolute;
97 | left: 0px;
98 | bottom: -30px;
99 | width: 134px;
100 | min-width: 49px;
101 | border: 4px solid #fff;
102 | border-radius: 50%;
103 | height: 134px;
104 | z-index: 5;
105 | img{
106 | border-radius: 50%;
107 | width: 100%;
108 | height: 100%;
109 | object-fit: cover;
110 | }
111 | }
112 |
113 | .profile-edit-button{
114 | min-height: 39px;
115 | min-width: 98.8px;
116 | transition: 0.2s ease-in-out;
117 | cursor: pointer;
118 | border: 1px solid rgb(29, 161, 242);
119 | border-radius: 9999px;
120 | display: flex;
121 | justify-content: center;
122 | align-items: center;
123 | margin-left: 7px;
124 | padding-left: 1em;
125 | padding-right: 1em;
126 | span{
127 | text-align: center;
128 | font-weight: 800;
129 | color: rgb(29, 161, 242);
130 | width: 100%
131 | }
132 | &:hover{
133 | background-color: rgba(29, 161, 242,0.1);
134 | }
135 | }
136 |
137 | .unfollow-switch{
138 | background-color: rgb(29, 161, 242);
139 | span{color: #fff !important;}
140 | }
141 |
142 | .unfollow-switch:hover{
143 | background-color: rgb(202,32,85) !important;
144 | border: 1px solid transparent;
145 | span{
146 | color: #fff;
147 | span{display: none;}
148 | &:before{
149 | content: 'Unfollow';
150 | }
151 | }
152 | }
153 |
154 | .profile-details-box{
155 | margin-top: 40px;
156 | }
157 |
158 | .profile-name{
159 | font-weight: 800;
160 | font-size: 19px;
161 | }
162 |
163 | .profile-username{
164 | font-size: 15px;
165 | color: rgb(101, 119, 134);
166 | }
167 |
168 | .profile-bio{
169 | margin-bottom: 10px;
170 | margin-top: 10px;
171 | }
172 |
173 | .profile-info-box{
174 | display: flex;
175 | margin-top: 10px;
176 | }
177 |
178 | .profile-info-box svg{
179 | margin-right: 5px;
180 | fill: rgb(101, 119, 134);
181 | height: 18.75px;
182 | }
183 |
184 | .profile-location, .profile-date{
185 | color: rgb(101, 119, 134);
186 | margin-right: 10px;
187 | }
188 |
189 | .profile-social-box{
190 | display: flex;
191 | margin-top: 7px;
192 | div{
193 | display: flex;
194 | cursor: pointer;
195 | &:hover{
196 | text-decoration: underline;
197 | }
198 | }
199 | }
200 |
201 | .follow-num{
202 | font-weight: bold;
203 | margin-right: 3px;
204 | cursor: pointer;
205 | }
206 |
207 | .follow-text{
208 | color: rgb(101, 119, 134);
209 | margin-right: 20px;
210 | cursor: pointer;
211 | }
212 |
213 | .profile-nav-menu{
214 | display: flex;
215 | justify-content: space-around;
216 | align-items: center;
217 | border-bottom: 1px solid rgb(230, 236, 240);
218 | }
219 |
220 | .profile-nav-item{
221 | white-space: nowrap;
222 | overflow: hidden;
223 | padding: 15px;
224 | width: 100%;
225 | text-align: center;
226 | cursor: pointer;
227 | font-weight: bold;
228 | color: rgb(101, 119, 134);
229 | transition: 0.2s;
230 | will-change: background-color;
231 | border-bottom: 2px solid transparent;
232 | &:hover{
233 | background-color: rgba(29, 161, 242, 0.1);
234 | color: rgb(29, 161, 242);
235 | }
236 | }
237 |
238 | .activeTab{
239 | border-bottom: 2px solid rgb(29, 161, 242);
240 | color: rgb(29, 161, 242);
241 | }
242 |
243 | .new-msg{
244 | border-radius: 999px;
245 | display: flex;
246 | align-items: center;
247 | justify-content: center;
248 | padding: 10px;
249 | transition: 0.2s ease-in-out;
250 | &:hover{
251 | background-color: rgba(29, 161, 242, 0.1);
252 | svg{fill: rgb(29, 161, 242)}
253 | }
254 | cursor: pointer;
255 | }
256 |
257 | .new-msg svg{
258 | width: 25px;
259 | height: 25px;
260 | display: flex;
261 | align-items: center;
262 | fill: #000;
263 | transition: 0.2s ease-in-out;
264 | }
265 |
--------------------------------------------------------------------------------
/src/components/Signup/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import './style.scss'
4 | import { Link, withRouter } from 'react-router-dom'
5 | import { ICON_LOGO } from '../../Icons'
6 |
7 | const SignUpPage = (props) => {
8 | const { actions } = useContext(StoreContext)
9 |
10 | const [name, setName] = useState('')
11 | const [username, setUsername] = useState('')
12 | const [email, setEmail] = useState('')
13 | const [password, setPassword] = useState('')
14 |
15 | const SignUp = (e) => {
16 | e.preventDefault()
17 | if(username.length && password.length && email.length && name.length){
18 | const values = {
19 | name,
20 | username,
21 | email,
22 | password,
23 | func: Redirect
24 | }
25 | actions.signup(values)
26 | }
27 | }
28 |
29 | const Redirect = () => {
30 | props.history.push('/login')
31 | }
32 |
33 | return(
34 |
35 |
36 |
37 | Sign up to Twitter
38 |
39 |
68 |
69 |
70 | Log in to Twitter
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | export default withRouter(SignUpPage)
--------------------------------------------------------------------------------
/src/components/Signup/style.scss:
--------------------------------------------------------------------------------
1 | .signup-wrapper{
2 | max-width: 600px;
3 | padding: 0 15px;
4 | margin: 20px auto 0 auto;
5 | }
6 |
7 | .signup-wrapper svg{
8 | height: 39px;
9 | margin: 0 auto;
10 | display: block;
11 | }
12 |
13 | .signup-header{
14 | margin-top: 30px;
15 | font-size: 23px;
16 | margin-bottom: 10px;
17 | font-weight: bold;
18 | text-align: center;
19 | }
20 |
21 | .signup-form{
22 | width: 100%;
23 | }
24 |
25 | .signup-input{
26 | background-color: inherit;
27 | border: inherit;
28 | }
29 |
30 | .signup-input:focus{
31 | background-color: inherit;
32 | border: inherit;
33 | }
34 |
35 | .signup-input-wrap{
36 | padding: 10px 15px;
37 | }
38 |
39 | .signup-input-content{
40 | border-bottom: 2px solid rgb(64, 67, 70);
41 | background-color: #e3e3e4;
42 | label{
43 | display: block;
44 | padding: 5px 10px 0 10px;
45 | }
46 | input{
47 | width: 100%;
48 | outline: none;
49 | font-size: 19px;
50 | padding: 2px 10px 5px 10px;
51 | }
52 | }
53 |
54 | .signup-btn-wrap{
55 | width: calc(100% - 20px);
56 | min-height: 49px;
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | transition: 0.2s ease-in-out;
61 | margin: 10px;
62 | padding: 0 30px;
63 | border-radius: 9999px;
64 | background-color: rgb(29, 161, 242);
65 | opacity: 0.5;
66 | color: #fff;
67 | font-weight: bold;
68 | outline: none;
69 | border: 1px solid rgba(0,0,0,0);
70 | font-size: 16px;
71 | }
72 |
73 | .signup-option{
74 | margin-top: 20px;
75 | font-size: 15px;
76 | color: rgb(29, 161, 242);
77 | text-align: center;
78 | &:hover{
79 | text-decoration: underline;
80 | cursor: pointer;
81 | }
82 | }
83 |
84 | .button-active{
85 | opacity: 1;
86 | cursor: pointer;
87 | }
--------------------------------------------------------------------------------
/src/components/Tweet/index.js:
--------------------------------------------------------------------------------
1 | import React , { useEffect, useState, useContext, useRef } from 'react'
2 | import { StoreContext } from '../../store/store'
3 | import { withRouter, useHistory , Link } from 'react-router-dom'
4 | import './style.scss'
5 | import moment from 'moment'
6 | import Loader from '../Loader'
7 | import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL, ICON_BOOKMARK,
8 | ICON_DELETE, ICON_BOOKMARKFILL, ICON_IMGUPLOAD, ICON_CLOSE } from '../../Icons'
9 | import axios from 'axios'
10 | import {API_URL} from '../../config'
11 | import ContentEditable from 'react-contenteditable'
12 | import TweetCard from '../TweetCard'
13 |
14 |
15 | const TweetPage = (props) => {
16 | let history = useHistory();
17 |
18 | const { state, actions } = useContext(StoreContext)
19 | const {tweet, account, session} = state
20 |
21 | const [modalOpen, setModalOpen] = useState(false)
22 | const [replyText, setReplyText] = useState('')
23 | const [replyImage, setReplyImg] = useState(null)
24 | const [imageLoaded, setImageLoaded] = useState(false)
25 |
26 | useEffect(()=>{
27 | window.scrollTo(0, 0)
28 | actions.getTweet(props.match.params.id)
29 | }, [props.match.params.id])
30 | var image = new Image()
31 |
32 | let info
33 | const likeTweet = (id) => {
34 | if(!session){ actions.alert('Please Sign In'); return }
35 | info = { dest: "tweet", id }
36 | actions.likeTweet(info)
37 | }
38 | const retweet = (id) => {
39 | if(!session){ actions.alert('Please Sign In'); return }
40 | info = { dest: "tweet", id }
41 | actions.retweet(info)
42 | }
43 | const bookmarkTweet = (id) => {
44 | if(!session){ actions.alert('Please Sign In'); return }
45 | info = { dest: "tweet", id }
46 | actions.bookmarkTweet(info)
47 | }
48 | const deleteTweet = (id) => {
49 | actions.deleteTweet(id)
50 | }
51 |
52 | const uploadImage = (file) => {
53 | let bodyFormData = new FormData()
54 | bodyFormData.append('image', file)
55 | axios.post(`${API_URL}/tweet/upload`, bodyFormData, { headers: { Authorization: `Bearer ${localStorage.getItem('Twittertoken')}`}})
56 | .then(res=>{setReplyImg(res.data.imageUrl)})
57 | .catch(err=>alert('error uploading image'))
58 | }
59 |
60 | const onchangeImage = () => {
61 | let file = document.getElementById('image').files[0];
62 | uploadImage(file)
63 | }
64 |
65 | const removeImage = () => {
66 | document.getElementById('image').value = "";
67 | setReplyImg(null)
68 | setImageLoaded(false)
69 | }
70 |
71 | const toggleModal = (e, type) => {
72 | if(e){ e.stopPropagation() }
73 | // if(param === 'edit'){setSaved(false)}
74 | // if(type === 'parent'){setParent(true)}else{setParent(false)}
75 | setModalOpen(!modalOpen)
76 | }
77 |
78 | const handleModalClick = (e) => {
79 | e.stopPropagation()
80 | }
81 |
82 | const tweetT = useRef('');
83 | const handleChange = evt => {
84 | if(tweetT.current.trim().length <= 280
85 | && tweetT.current.split(/\r\n|\r|\n/).length <= 30){
86 | tweetT.current = evt.target.value;
87 | setReplyText(tweetT.current)
88 | }
89 | };
90 |
91 | const replyTweet = (type) => {
92 | toggleModal()
93 | let hashtags = replyText.match(/#(\w+)/g)
94 | if(!replyText.length){return}
95 | const values = {
96 | description: replyText,
97 | images: [replyImage],
98 | parent: props.match.params.id,
99 | hashtags,
100 | }
101 | actions.tweet(values)
102 | tweetT.current = ''
103 | setReplyText('')
104 | setReplyImg(null)
105 | actions.alert('Tweet sent!')
106 | }
107 |
108 | const goBack = () => {
109 | history.goBack()
110 | }
111 |
112 | return(
113 | <>
114 | {tweet ?
115 |
116 |
117 |
118 |
goBack()} className="header-back-wrapper">
119 |
120 |
121 |
122 |
Tweet
123 |
124 |
125 |
126 |
127 |
128 |

129 |
130 |
131 |
132 |
133 | {tweet.user.name}
134 |
135 |
136 | @{tweet.user.username}
137 |
138 |
139 |
140 |
141 | {tweet.description}
142 |
143 | {tweet.images[0] ?
144 |
: null }
148 |
149 | {moment(tweet.createdAt).format("h:mm A · MMM D, YYYY")}
150 |
151 |
152 |
{tweet.retweets.length}
153 |
Retweets
154 |
{tweet.likes.length}
155 |
Likes
156 |
157 |
158 |
toggleModal()} className="tweet-int-icon">
159 |
160 |
161 |
retweet(tweet._id)} className="tweet-int-icon">
162 |
163 |
164 |
165 |
166 |
likeTweet(tweet._id)} className="tweet-int-icon">
167 |
168 | {account && account.likes.includes(tweet._id) ? : }
170 |
171 |
account && account.username === tweet.user.username ? deleteTweet(tweet._id) : bookmarkTweet(tweet._id)} className="tweet-int-icon">
172 |
173 | {account && account.username === tweet.user.username ?
174 | : account && account.bookmarks.includes(tweet._id) ? :
175 | }
176 |
177 |
178 |
179 |
180 |
181 | {tweet.replies.map(r=>{
182 | return
184 | })}
185 |
186 |
:
}
187 |
188 | {tweet && account ?
189 | toggleModal()} style={{display: modalOpen ? 'block' : 'none'}} className="modal-edit">
190 | {modalOpen ?
191 |
handleModalClick(e)} className="modal-content">
192 |
193 |
194 |
toggleModal()} className="modal-closeIcon-wrap">
195 |
196 |
197 |
198 |
Reply
199 |
200 |
201 |
202 |
203 |
e.stopPropagation()} to={`/profile/${tweet.user.username}`}>
204 |

205 |
206 |
207 |
208 |
209 |
210 |
211 | e.stopPropagation()} to={`/profile/${tweet.user.username}`}>{tweet.user.name}
212 |
213 |
214 | e.stopPropagation()} to={`/profile/${tweet.user.username}`}>{'@'+ tweet.user.username}
215 |
216 | ·
217 |
218 | {/* e.stopPropagation()} to={`/profile/${props.user.username}`}> */}
219 | {/* {moment(parent? props.parent.createdAt : props.createdAt).fromNow(true).replace(' ','').replace('an','1').replace('minutes','m').replace('hour','h').replace('hs','h')} */}
220 | {moment(tweet.createdAt).fromNow()}
221 | {/* */}
222 |
223 |
224 |
225 |
226 | {tweet.description}
227 |
228 |
229 |
230 | Replying to
231 |
232 |
233 | @{tweet.user.username}
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |

242 |
243 |
244 |
document.getElementById('replyBox').focus()} className="Tweet-input-side">
245 |
246 | tweetT.current.length>279 ? e.keyCode !== 8 && e.preventDefault(): null} id="replyBox" onPaste={(e)=>e.preventDefault()} id="replyBox" style={{minHeight: '120px'}} className={replyText.length ? 'tweet-input-active' : null} placeholder="Tweet your reply" html={tweetT.current} onChange={handleChange} />
247 |
248 | {replyImage &&
249 |
![]()
setImageLoaded(true)} className="tweet-upload-image" src={replyImage} alt="tweet" />
250 | {imageLoaded &&
x}
251 |
}
252 |
253 |
254 |
255 |
256 | onchangeImage()} />
257 |
258 |
259 |
260 |
= 280 ? 'red' : null }}>
261 | {replyText.length > 0 && replyText.length + '/280'}
262 |
263 |
replyTweet('parent')} className={replyText.length ? 'tweet-btn-side tweet-btn-active' : 'tweet-btn-side'}>
264 | Reply
265 |
266 |
267 |
268 |
269 |
270 |
271 |
: null}
272 |
:null}
273 | >
274 | )
275 | }
276 |
277 | export default withRouter(TweetPage)
--------------------------------------------------------------------------------
/src/components/Tweet/style.scss:
--------------------------------------------------------------------------------
1 | .tweet-wrapper{
2 | max-width: 600px;
3 | border-right: 1px solid rgb(230, 236, 240);
4 | width: 100%;
5 | height: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | .tweet-header-wrapper{
11 | position: sticky;
12 | border-bottom: 1px solid rgb(230, 236, 240);
13 | border-left: 1px solid rgb(230, 236, 240);
14 | background-color: #fff;
15 | z-index: 3;
16 | top: 0px;
17 | display: flex;
18 | align-items: center;
19 | cursor: pointer;
20 | height: 53px;
21 | min-height: 53px;
22 | padding-left: 15px;
23 | padding-right: 15px;
24 | max-width: 1000px;
25 | margin: 0 auto;
26 | width: 100%;
27 | }
28 |
29 | .profile-header-back{
30 | min-width: 55px;
31 | min-height: 30px;
32 | justify-content: center;
33 | align-items: flex-start;
34 | }
35 |
36 | .header-back-wrapper{
37 | margin-left: -5px;
38 | width: 39px;
39 | height: 39px;
40 | transition: 0.2s ease-in-out;
41 | will-change: background-color;
42 | border: 1px solid rgba(0, 0, 0, 0);
43 | border-radius: 9999px;
44 | display: flex;
45 | justify-content: center;
46 | align-items: center;
47 | }
48 |
49 | .header-back-wrapper svg{
50 | height: 1.5em;
51 | fill: rgb(29,161,242);
52 | }
53 |
54 | .tweet-header-content{
55 | font-weight: 800;
56 | font-size: 19px;
57 | }
58 |
59 | .tweet-body-wrapper{
60 | padding: 0 15px;
61 | border-bottom: 1px solid rgb(230, 236, 240);
62 | // overflow: hidden; causing tweet page info to cuttoff
63 | }
64 |
65 | .tweet-header-content{
66 | margin-top: 10px;
67 | margin-bottom: 10px;
68 | display: flex;
69 | align-items: center;
70 | }
71 |
72 | .tweet-user-pic{
73 | flex-basis: 49px;
74 | margin-right: 10px;
75 | img{
76 | object-fit: cover;
77 | }
78 | }
79 |
80 | .tweet-user-wrap{
81 | display: flex;
82 | flex-direction: column;
83 | justify-content: center
84 | }
85 |
86 | .tweet-user-name{
87 | font-size: 16px;
88 | font-weight: bold;
89 | cursor: pointer;
90 | &:hover{
91 | text-decoration: underline;
92 | }
93 | }
94 |
95 | .tweet-username{
96 | font-size: 15.5px;
97 | font-weight: 400;
98 | color: rgb(101, 119, 134);
99 | }
100 |
101 | .tweet-content{
102 | margin-top: 10px;
103 | font-size: 23px;
104 | margin-bottom: 10px;
105 | word-break: break-word;
106 | }
107 |
108 | .tweet-date{
109 | margin: 15px 0;
110 | font-size: 15px;
111 | color: rgb(101, 119, 134);
112 | }
113 |
114 | .tweet-stats{
115 | display: flex;
116 | padding: 15px 5px;
117 | border-top: 1px solid rgb(230, 236, 240);
118 | border-bottom: 1px solid rgb(230, 236, 240);
119 | }
120 |
121 | .int-num{
122 | font-weight: bold;
123 | margin-right: 5px;
124 | }
125 |
126 | .int-text{
127 | color: rgb(101, 119, 134);
128 | margin-right: 20px;
129 | }
130 |
131 | .tweet-interactions{
132 | display: flex;
133 | justify-content: space-evenly;
134 |
135 | }
136 |
137 | .tweet-int-icon{
138 | min-height: 49px;
139 | width: 100%;
140 | padding: 0 5px;
141 | display: flex;
142 | align-items: center;
143 | justify-content: center;
144 | }
145 |
146 | .card-icon svg{
147 | height: 22.5px;
148 | width: 22.5px;
149 | fill: rgb(101, 119, 134);
150 | }
151 | //////////////////////@exten
152 |
153 | .tweet-replies-wrapper{
154 | padding: 10px 15px 0 15px;
155 | transition: 0.2s ease-in-out;
156 | display: flex;
157 | border-bottom: 1px solid rgb(230, 236, 240);
158 | cursor: pointer;
159 | &:hover{
160 | background-color: rgb(245, 248, 250);
161 | }
162 | }
163 |
164 |
165 | .reply-tweet-username{
166 | font-size: 15.5px;
167 | margin-right: 5px;
168 | color: rgb(101, 119, 134);
169 | }
170 |
171 | .main-tweet-user{
172 | color: rgb(27, 149, 224);
173 | &:hover{ text-decoration: underline;}
174 | }
175 |
176 | .card-icon{
177 | display: flex;
178 | justify-content: center;
179 | align-items: center;
180 | padding: 7.4px;
181 | border-radius: 50%;
182 | transition: 0.2s ease-in-out;
183 | will-change: background-color;
184 | cursor: pointer;
185 | }
186 |
187 |
188 | .reply-icon:hover{
189 | background-color: rgba(29, 161, 242,0.1);
190 | svg{ fill: rgb(29, 161, 242) !important; }
191 | }
192 | .retweet-icon:hover{
193 | background-color: rgba(23, 191, 99,0.1);
194 | svg{ fill: rgb(23, 191, 99) !important; }
195 | }
196 | .heart-icon:hover{
197 |
198 | background-color: rgba(224, 36, 94,0.1);
199 | svg{ fill: rgb(224, 36, 94) !important; }
200 | }
201 | .share-icon:hover{
202 | background-color: rgba(29, 161, 242,0.1);
203 | svg{ fill: rgb(29, 161, 242) !important; }
204 | }
205 | .delete-icon:hover{
206 | background-color: rgba(212, 11, 11, 0.1);
207 | svg{ fill: red !important; }
208 | }
209 |
210 | .reply-int svg{
211 | display: flex;
212 | align-items: center;
213 | height: 18.75px;
214 | width: 18.75px;
215 | fill: rgb(101, 119, 134);
216 | }
217 |
218 | .card-icon-value{
219 | margin-left: 3px;
220 | font-size: 13px;
221 | }
222 |
223 | .retweet-int:hover{
224 | color: rgb(23, 191, 99);
225 | }
226 | .heart-int:hover{
227 | color: rgb(224, 36, 94);
228 | }
229 |
230 |
231 |
232 | ////////////@extend
233 | .tweet-image-wrapper{
234 | overflow: hidden;
235 | max-height: 730px;
236 |
237 | div{
238 | border-radius: 14px;
239 | background-position: center center;
240 | background-repeat: no-repeat;
241 | width: 100%;
242 | height: 100%;
243 | top: 0px;
244 | left: 0px;
245 | right: 0px;
246 | bottom: 0px;
247 | background-size: cover;
248 | }
249 | }
--------------------------------------------------------------------------------
/src/components/TweetCard/style.scss:
--------------------------------------------------------------------------------
1 | .Tweet-card-wrapper{
2 | border-bottom: 1px solid rgb(230, 236, 240);
3 | display: flex;
4 | transition: 0.2s ease-in-out;
5 | will-change: background-color;
6 | cursor: pointer;
7 | padding: 10px 15px;
8 | &:hover{
9 | background-color: rgb(245,248,250);
10 | }
11 | }
12 |
13 | .card-userPic-wrapper{
14 | flex-basis: 49px;
15 | margin-right: 10px;
16 | display: flex;
17 | flex-direction: column;
18 | img{
19 | object-fit: cover;
20 | }
21 | }
22 |
23 | .card-content-wrapper{
24 | // overflow: hidden; removed it caused hover icons to cuttoff
25 | max-width: calc(100% - 60px);
26 | flex-basis: calc(100% - 49px);
27 | }
28 |
29 |
30 | .card-content-header{
31 | margin-bottom: 2px;
32 | display: flex;
33 | justify-content: space-between;
34 | }
35 |
36 |
37 | .card-header-user:hover{
38 | text-decoration: underline;
39 | }
40 |
41 |
42 | .card-header-date{
43 | &:hover{
44 | text-decoration: underline;
45 | }
46 | }
47 |
48 | .card-header-user{
49 | font-weight: bold;
50 | }
51 |
52 | .card-header-username{
53 | margin-left: 5px;
54 | color: rgb(101, 119, 134)
55 | }
56 |
57 | .card-header-dot{
58 | padding: 0 5px;
59 | color: rgb(101, 119, 134)
60 | }
61 |
62 | .card-header-date{
63 | color: rgb(101, 119, 134)
64 |
65 | }
66 |
67 | .card-header-more{
68 |
69 | }
70 |
71 | .card-content-images{
72 | margin-top: 10px;
73 | border: 1px solid rgb(204, 214, 221);
74 | border-radius: 14px;
75 | // display: flex;
76 | // flex-direction: column;
77 |
78 | }
79 |
80 | .card-image-link{
81 | cursor: pointer;
82 | display: block;
83 | max-height: 253px;
84 | border-radius: 14px;
85 |
86 | img{
87 | max-height:253px;
88 | border-radius: 14px;
89 | width: 100%;
90 | height: 100%;
91 | object-fit: cover;
92 | }
93 | }
94 |
95 | .card-buttons-wrapper{
96 | margin-left: -5px;
97 | margin-top: 5px;
98 | max-width: 425px;
99 | display: flex;
100 | justify-content: space-between;
101 | align-items: center;
102 | margin-bottom: -5px;
103 | }
104 |
105 | .card-button-wrap{
106 | display: flex;
107 | justify-content: flex-start;
108 | align-items: center;
109 | color: rgb(134, 120, 101);
110 | &:hover{
111 | .reply-icon{
112 | .card-button-wrap{color: rgb(212, 11, 11) !important}
113 | background-color: rgba(29, 161, 242,0.1);
114 | svg{ fill: rgb(29, 161, 242) !important; }
115 | }
116 | .retweet-icon{
117 | background-color: rgba(23, 191, 99,0.1);
118 | svg{ fill: rgb(23, 191, 99) !important; }
119 | }
120 | .heart-icon{
121 |
122 | background-color: rgba(224, 36, 94,0.1);
123 | svg{ fill: rgb(224, 36, 94) !important; }
124 | }
125 | .share-icon{
126 | background-color: rgba(29, 161, 242,0.1);
127 | svg{ fill: rgb(29, 161, 242) !important; }
128 | }
129 | .delete-icon{
130 | background-color: rgba(212, 11, 11, 0.1);
131 | svg{ fill: rgb(212, 11, 11) !important; }
132 | }
133 | }
134 | }
135 |
136 | .reply-wrap:hover{
137 | color: rgb(29, 161, 242);
138 | }
139 | .retweet-wrap:hover{
140 | color: rgb(23, 191, 99);
141 | }
142 | .heart-wrap:hover{
143 | color: rgb(224, 36, 94);
144 | }
145 |
146 | .card-icon{
147 | display: flex;
148 | justify-content: center;
149 | align-items: center;
150 | padding: 7.4px;
151 | border-radius: 50%;
152 | transition: 0.2s ease-in-out;
153 | will-change: background-color;
154 | }
155 |
156 | .card-icon svg{
157 | width: 18.75px;
158 | height: 18.75px;
159 | }
160 |
161 | .card-icon-value{
162 | margin-left: 3px;
163 | font-size: 13px;
164 |
165 | }
166 |
167 |
168 | /////////reply modal
169 | .reply-content-wrapper{
170 | display: flex;
171 | padding: 10px 15px;
172 | }
173 |
174 |
175 | .reply-tweet-username{
176 | font-size: 15.5px;
177 | margin-right: 5px;
178 | color: rgb(101, 119, 134);
179 | }
180 |
181 | .main-tweet-user{
182 | color: rgb(27, 149, 224);
183 | &:hover{ text-decoration: underline;}
184 | }
185 |
186 | .reply-to-user{
187 | margin-top: 15px;
188 | }
189 |
190 | // .reply-thread{
191 | // border-right: 2px solid #ccd6dd;
192 | // text-align: center;
193 | // height: 100%;
194 | // width: 100%;
195 | // }
196 |
197 |
198 | .replyTo-wrapper{
199 | margin-bottom: 2px;
200 | }
201 |
202 | .user-retweet-icon{
203 | display: flex;
204 | justify-content: flex-end;
205 | margin-bottom: 5px;
206 | }
207 |
208 | .user-retweet-icon svg{
209 | width: 13px;
210 | height: 18.75px;
211 | fill: rgb(101, 119, 134);
212 | }
213 |
214 | .user-retweeted{
215 | color: rgb(101, 119, 134);
216 | font-size: 13px;
217 | margin-bottom: 5px;
218 | &:hover{
219 | text-decoration: underline;
220 | }
221 | }
222 |
223 | .tweet-reply-thread{
224 | margin-top: 2px;
225 | width: 2px;
226 | background-color: rgb(204, 214, 221);
227 | margin: 0 auto;
228 | height: 100%;
229 | margin-top: -5px;
230 | margin-bottom: -20px;
231 | }
232 |
233 | .user-replied{
234 | color: rgb(101, 119, 134);
235 | font-size: 13px;
236 | margin-bottom: 5px;
237 | &:hover{
238 | text-decoration: underline;
239 | }
240 | }
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | // export const API_URL = 'http://127.0.0.1:5000';
2 | export const API_URL = 'https://twitter-c-api.herokuapp.com';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | /* for scroll bar shift effect when hidding */
9 |
10 | }
11 |
12 | @media only screen and (min-width: 450px) {
13 | body{
14 | width: calc(100vw - 17px);
15 | }
16 | }
17 |
18 | code {
19 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
20 | monospace;
21 | }
22 |
23 | * {
24 | box-sizing: border-box;
25 | margin: 0;
26 | padding: 0;
27 | font-family: 'Assistant', sans-serif;
28 | /* scroll-behavior: smooth; */
29 | word-break: break-word;
30 | }
31 | a{
32 | text-decoration: none;
33 | color: inherit;
34 | }
35 |
36 |
37 | /* *{
38 | background-color: #1a1919 !important;
39 | fill: aliceblue;
40 | color: aliceblue !important;
41 | } */
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | //remove because it causes double renders on reducer
8 | ReactDOM.render( , document.getElementById('root')
9 | );
10 |
11 | // If you want your app to work offline and load faster, you can change
12 | // unregister() to register() below. Note this comes with some pitfalls.
13 | // Learn more about service workers: https://bit.ly/CRA-PWA
14 | serviceWorker.unregister();
15 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/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/store/actions.js:
--------------------------------------------------------------------------------
1 | import types from './typeActions'
2 | import jwt_decode from 'jwt-decode'
3 |
4 | export const useActions = (state, dispatch) => ({
5 | login: data => {
6 | dispatch({type: types.SET_STATE, payload: {loading: true}})
7 | dispatch({type: types.LOGIN, payload: data})
8 | },
9 | signup: data => {
10 | dispatch({type: types.SET_STATE, payload: {loading: true}})
11 | dispatch({type: types.REGISTER, payload: data})
12 | },
13 | tweet: data => {
14 | dispatch({type: types.SET_STATE, payload: {loading: true}})
15 | dispatch({type: types.TWEET, payload: data})
16 | },
17 | likeTweet: data => {
18 | dispatch({type: types.LIKE_TWEET, payload: data})
19 | },
20 | getTweets: data => {
21 | dispatch({type: types.SET_STATE, payload: {loading: true}})
22 | dispatch({type: types.GET_TWEETS, payload: data})
23 | },
24 | bookmarkTweet: data => {
25 | dispatch({type: types.BOOKMARK, payload: data})
26 | },
27 | getTweet: data => {
28 | dispatch({type: types.SET_STATE, payload: {loading: true}})
29 | dispatch({type: types.GET_TWEET, payload: data})
30 | },
31 | verifyToken: data => {
32 | if(localStorage.getItem('Twittertoken')){
33 | const jwt = jwt_decode(localStorage.getItem('Twittertoken'))
34 | const current_time = new Date().getTime() / 1000;
35 | if(current_time > jwt.exp){
36 | dispatch({type: types.SET_STATE, payload: {session: false}})
37 | localStorage.removeItem("Twittertoken")
38 | }else{
39 | if(data === 'get account'){ dispatch({type: types.GET_ACCOUNT}) }
40 | dispatch({type: types.SET_STATE, payload: {session: true, decoded: jwt}})
41 | }
42 | }else{
43 | dispatch({type: types.SET_STATE, payload: {session: false}})
44 | }
45 | },
46 | getUser: data => {
47 | dispatch({type: types.SET_STATE, payload: {loading: true}})
48 | dispatch({type: types.GET_USER, payload: data})
49 | },
50 | getBookmarks: data => {
51 | dispatch({type: types.GET_BOOKMARKS})
52 | },
53 | updateUser: data => {
54 | dispatch({type: types.SET_STATE, payload: {loading: true}})
55 | dispatch({type: types.UPDATE_USER, payload: data})
56 | },
57 | retweet: data => {
58 | dispatch({type: types.RETWEET, payload: data})
59 | },
60 | deleteTweet: data => {
61 | dispatch({type: types.DELETE_TWEET, payload: data})
62 | },
63 | followUser: data => {
64 | dispatch({type: types.FOLLOW_USER, payload: data})
65 | },
66 | editList: data => {
67 | dispatch({type: types.EDIT_LIST, payload: data})
68 | },
69 | createList: data => {
70 | dispatch({type: types.CREATE_LIST, payload: data})
71 | },
72 | deleteList: data => {
73 | dispatch({type: types.DELETE_LIST, payload: data})
74 | },
75 | getLists: data => {
76 | dispatch({type: types.GET_LISTS, payload: data})
77 | },
78 | logout: () => {
79 | localStorage.removeItem("Twittertoken")
80 | window.location.reload()
81 | },
82 | getList: data => {
83 | dispatch({type: types.GET_LIST, payload: data})
84 | },
85 | getTrend: data => {
86 | dispatch({type: types.GET_TREND, payload: data})
87 | },
88 | search: data => {
89 | dispatch({type: types.SEARCH, payload: data})
90 | },
91 | getTrendTweets: data => {
92 | dispatch({type: types.TREND_TWEETS, payload: data})
93 | },
94 | addToList: data => {
95 | dispatch({type: types.ADD_TO_LIST, payload: data})
96 | },
97 | getFollowers: data => {
98 | dispatch({type: types.GET_FOLLOWERS, payload: data})
99 | },
100 | getFollowing: data => {
101 | dispatch({type: types.GET_FOLLOWING, payload: data})
102 | },
103 | searchUsers: data => {
104 | dispatch({type: types.SEARCH_USERS, payload: data})
105 | },
106 | whoToFollow: data => {
107 | dispatch({type: types.WHO_TO_FOLLOW, payload: data})
108 | },
109 | alert: data => {
110 | dispatch({type: types.SET_STATE, payload: {top: '16px', msg: data}})
111 | setTimeout(() => { dispatch({type: types.SET_STATE, payload: {top: '-100px'}}) }, 2700)
112 | },
113 | getConversations: data => {
114 | dispatch({type: types.GET_CONVERSATIONS, payload: data})
115 | },
116 | startChat: data => {
117 | dispatch({type: types.SET_STATE, payload: {startingChat: true}})
118 | dispatch({type: types.START_CHAT, payload: data})
119 | },
120 | getSingleConversation: data =>{
121 | dispatch({type: types.GET_SINGLE_CONVERSATION, payload: data})
122 | }
123 | })
124 |
--------------------------------------------------------------------------------
/src/store/middleware.js:
--------------------------------------------------------------------------------
1 | import types from './typeActions'
2 | import axios from 'axios'
3 | import {API_URL} from '../config'
4 |
5 | export const token = () => {
6 | if(localStorage.getItem('Twittertoken')){
7 | return localStorage.getItem('Twittertoken')
8 | }
9 | return null
10 | }
11 |
12 | export const applyMiddleware = dispatch => action => {
13 | let headers = { headers: { Authorization: `Bearer ${token()}` } }
14 | switch (action.type){
15 | case types.LOGIN:
16 | return axios.post(`${API_URL}/auth/login`, action.payload)
17 | .then(res=>dispatch({ type: types.LOGIN, payload: res.data, rememberMe: action.payload.remember }))
18 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
19 |
20 | case types.REGISTER:
21 | return axios.post(`${API_URL}/auth/register`, action.payload)
22 | .then(res=>dispatch({ type: types.REGISTER, payload: res.data, data: action.payload }))
23 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
24 |
25 | case types.TWEET:
26 | return axios.post(`${API_URL}/tweet/create`, action.payload, headers)
27 | .then(res=>dispatch({ type: types.TWEET, payload: res.data, data: action.payload }))
28 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
29 |
30 | case types.LIKE_TWEET:
31 | return axios.post(`${API_URL}/tweet/${action.payload.id}/like`, action.payload, headers)
32 | .then(res=>dispatch({ type: types.LIKE_TWEET, payload: res.data, data: action.payload }))
33 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
34 |
35 | case types.GET_TWEETS:
36 | return axios.get(`${API_URL}/tweet`, action.payload)
37 | .then(res=>dispatch({ type: types.GET_TWEETS, payload: res.data }))
38 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
39 |
40 | case types.GET_TWEET:
41 | return axios.get(`${API_URL}/tweet/${action.payload}`, action.payload)
42 | .then(res=>dispatch({ type: types.GET_TWEET, payload: res.data }))
43 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
44 |
45 | case types.GET_ACCOUNT:
46 | return axios.get(`${API_URL}/auth/user`, headers)
47 | .then(res=>dispatch({ type: types.GET_ACCOUNT, payload: res.data }))
48 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
49 |
50 | case types.BOOKMARK:
51 | return axios.post(`${API_URL}/tweet/${action.payload.id}/bookmark`, action.payload, headers)
52 | .then(res=>dispatch({ type: types.BOOKMARK, payload: res.data, data: action.payload }))
53 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
54 |
55 | case types.GET_USER:
56 | return axios.get(`${API_URL}/user/${action.payload}/tweets`)
57 | .then(res=>dispatch({ type: types.GET_USER, payload: res.data }))
58 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
59 |
60 | case types.GET_BOOKMARKS:
61 | return axios.get(`${API_URL}/user/i/bookmarks`, headers)
62 | .then(res=>dispatch({ type: types.GET_BOOKMARKS, payload: res.data }))
63 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
64 |
65 | case types.UPDATE_USER:
66 | return axios.put(`${API_URL}/user/i`, action.payload, headers)
67 | .then(res=>dispatch({ type: types.UPDATE_USER, payload: res.data, data: action.payload }))
68 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
69 |
70 | case types.RETWEET:
71 | return axios.post(`${API_URL}/tweet/${action.payload.id}/retweet`, action.payload, headers)
72 | .then(res=>dispatch({ type: types.RETWEET, payload: res.data, data: action.payload }))
73 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
74 |
75 | case types.DELETE_TWEET:
76 | return axios.delete(`${API_URL}/tweet/${action.payload}/delete`, headers)
77 | .then(res=>dispatch({ type: types.DELETE_TWEET, payload: res.data, data: action.payload }))
78 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
79 |
80 | case types.FOLLOW_USER:
81 | return axios.post(`${API_URL}/user/${action.payload}/follow`, action.payload, headers)
82 | .then(res=>dispatch({ type: types.FOLLOW_USER, payload: res.data, data: action.payload }))
83 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
84 |
85 | case types.EDIT_LIST:
86 | return axios.put(`${API_URL}/lists/${action.payload.id}/edit`, action.payload, headers)
87 | .then(res=>dispatch({ type: types.EDIT_LIST, payload: res.data, data: action.payload }))
88 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
89 |
90 | case types.CREATE_LIST:
91 | return axios.post(`${API_URL}/lists/create`, action.payload, headers)
92 | .then(res=>dispatch({ type: types.CREATE_LIST, payload: res.data, data: action.payload }))
93 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
94 |
95 | case types.DELETE_LIST:
96 | return axios.delete(`${API_URL}/lists/${action.payload}/delete`, headers)
97 | .then(res=>dispatch({ type: types.DELETE_LIST, payload: res.data, data: action.payload }))
98 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
99 |
100 | case types.GET_LISTS:
101 | return axios.get(`${API_URL}/user/i/lists`, headers)
102 | .then(res=>dispatch({ type: types.GET_LISTS, payload: res.data, data: action.payload }))
103 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
104 |
105 | case types.GET_LIST:
106 | return axios.get(`${API_URL}/lists/${action.payload}`, headers )
107 | .then(res=>dispatch({ type: types.GET_LIST, payload: res.data }))
108 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
109 |
110 | case types.GET_TREND:
111 | return axios.get(`${API_URL}/trend`)
112 | .then(res=>dispatch({ type: types.GET_TREND, payload: res.data }))
113 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
114 |
115 | case types.SEARCH:
116 | return axios.post(`${API_URL}/trend`, action.payload)
117 | .then(res=>dispatch({ type: types.SEARCH, payload: res.data }))
118 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
119 |
120 | case types.SEARCH_USERS:
121 | return axios.post(`${API_URL}/user`, action.payload)
122 | .then(res=>dispatch({ type: types.SEARCH_USERS, payload: res.data }))
123 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
124 |
125 | case types.TREND_TWEETS:
126 | return axios.get(`${API_URL}/trend/${action.payload}`)
127 | .then(res=>dispatch({ type: types.TREND_TWEETS, payload: res.data }))
128 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
129 |
130 | case types.ADD_TO_LIST:
131 | return axios.post(`${API_URL}/lists/${action.payload.username}/${action.payload.id}`, action.payload, headers)
132 | .then(res=>dispatch({ type: types.ADD_TO_LIST, payload: res.data, data: action.payload }))
133 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
134 |
135 | case types.GET_FOLLOWERS:
136 | return axios.get(`${API_URL}/user/${action.payload}/followers`, headers)
137 | .then(res=>dispatch({ type: types.GET_FOLLOWERS, payload: res.data, data: action.payload }))
138 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
139 |
140 | // case types.GET_FOLLOWING:
141 | // return axios.get(`${API_URL}/lists/i/following`, action.payload, headers)
142 | // .then(res=>dispatch({ type: types.GET_FOLLOWING, payload: res.data, data: action.payload }))
143 | // .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
144 |
145 | case types.WHO_TO_FOLLOW:
146 | return axios.get(`${API_URL}/user/i/suggestions`, headers)
147 | .then(res=>dispatch({ type: types.WHO_TO_FOLLOW, payload: res.data, data: action.payload }))
148 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
149 |
150 |
151 | case types.GET_CONVERSATIONS:
152 | return axios.get(`${API_URL}/chat/conversations`, headers)
153 | .then(res=>dispatch({ type: types.GET_CONVERSATIONS, payload: res.data }))
154 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
155 |
156 | case types.START_CHAT:
157 | return axios.post(`${API_URL}/chat/conversation`, action.payload, headers)
158 | .then(res=>dispatch({ type: types.START_CHAT, payload: res.data, data: action.payload }))
159 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
160 |
161 | case types.GET_SINGLE_CONVERSATION:
162 | return axios.get(`${API_URL}/chat/conversation?id=${action.payload.id}`, headers)
163 | .then(res=>dispatch({ type: types.GET_SINGLE_CONVERSATION, payload: res.data, data: action.payload }))
164 | .catch(err=>dispatch({ type: types.ERROR, payload: err.response.data }))
165 |
166 | default: dispatch(action)
167 | }
168 | }
--------------------------------------------------------------------------------
/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import type from './typeActions'
2 |
3 | const initialState = {
4 | session: true,
5 | loggedin: false,
6 | tweets: [],
7 | tweet: null,
8 | account: null,
9 | user: null,
10 | bookmarks: [],
11 | recent_tweets: [],
12 | lists: [],
13 | list: null,
14 | trends: [],
15 | result: [],
16 | tagTweets: [],
17 | followers: [],
18 | following: [],
19 | resultUsers: [],
20 | suggestions: [],
21 | top: '-100px',
22 | msg: '',
23 | conversations: null,
24 | conversation: null,
25 | error: false
26 | }
27 |
28 | const reducer = (state = initialState, action) => {
29 | switch (action.type) {
30 | case type.SET_STATE:
31 | return {...state, ...action.payload }
32 |
33 | case type.ERROR:
34 | // message.error(action.payload.msg? action.payload.msg : action.payload == 'Unauthorized' ? 'You need to sign in' : 'error');
35 | return {...state, loading: false, error: true, msg: action.payload.msg}
36 |
37 | case type.LOGIN:
38 | localStorage.setItem("Twittertoken", action.payload.token)
39 | return {...state, ...action.payload, loggedin: true, loading: false, error: false}
40 |
41 | case type.REGISTER:
42 | setTimeout(()=>{action.data.func()},250)
43 | return {...state, ...action.payload, loading: false, error: false}
44 |
45 | case type.TWEET:
46 | let recentT = state.tweets
47 | let s_tweet = state.tweet
48 | recentT.unshift(action.payload.tweet)
49 | if(s_tweet && s_tweet._id === action.data.parent){
50 | s_tweet.replies.unshift(action.payload.tweet)
51 | }
52 | return {...state, loading: false, error: false}
53 |
54 | case type.LIKE_TWEET:
55 | let account_likes = state.account
56 | let tweet_likes = state.tweets
57 | let user_likes = state.user
58 | let Stweet_likes = state.tweet
59 | if(action.payload.msg === "liked"){
60 |
61 | if(Stweet_likes){
62 | Stweet_likes.likes.push(action.data.id)
63 | }
64 |
65 | account_likes.likes.push(action.data.id)
66 | tweet_likes.length && tweet_likes.find(x=>x._id === action.data.id).likes.push(account_likes._id)
67 |
68 | if(action.data.dest === 'profile'){
69 | user_likes.tweets.find(x=>x._id === action.data.id).likes.push(action.data.id)
70 | user_likes.likes = user_likes.tweets.filter(x=>x._id === action.data.id).concat(user_likes.likes)
71 | }
72 |
73 | }else if(action.payload.msg === "unliked"){
74 |
75 | if(Stweet_likes){
76 | Stweet_likes.likes = Stweet_likes.likes.filter((x)=>{
77 | return x !== action.data.id
78 | });
79 | }
80 |
81 | tweet_likes.length && tweet_likes.find(x=>x._id === action.data.id).likes.pop()
82 | let likeIndex = account_likes.likes.indexOf(action.data.id)
83 | likeIndex > -1 && account_likes.likes.splice(likeIndex, 1)
84 |
85 | if(action.data.dest === 'profile'){
86 | user_likes.tweets.find(x=>x._id === action.data.id).likes.pop()
87 | user_likes.likes = user_likes.likes.filter((x)=>{
88 | return x._id !== action.data.id
89 | });
90 | }
91 | }
92 | return {...state, ...{account:account_likes}, ...{tweets:tweet_likes}, ...{user: user_likes}, ...{tweet: Stweet_likes}}
93 |
94 | case type.GET_TWEETS:
95 | return {...state, ...action.payload, loading: false, error: false}
96 |
97 | case type.GET_TWEET:
98 | return {...state, ...action.payload, loading: false, error: false}
99 |
100 | case type.GET_ACCOUNT:
101 | return {...state, ...action.payload}
102 |
103 | case type.BOOKMARK:
104 | let account_bookmarks = state.account
105 | if(action.payload.msg === "bookmarked"){
106 | account_bookmarks.bookmarks.push(action.data.id)
107 | }else if(action.payload.msg === "removed from bookmarks"){
108 | let bookIndex = account_bookmarks.bookmarks.indexOf(action.data.id)
109 | bookIndex > -1 && account_bookmarks.bookmarks.splice(bookIndex, 1)}
110 | return {...state, ...{account:account_bookmarks}}
111 |
112 | case type.GET_USER:
113 | return {...state, ...action.payload}
114 |
115 | case type.GET_BOOKMARKS:
116 | return {...state, ...action.payload}
117 |
118 | case type.UPDATE_USER:
119 | Object.keys(action.data).forEach(key => action.data[key] === '' || action.data[key] === undefined ? delete action.data[key] : null)
120 | let updateUser = {...state.user, ...action.data}
121 | return {...state, ...{user:updateUser}}
122 |
123 | case type.RETWEET:
124 | let user_retweets = state.user
125 | let acc_retweets = state.account
126 | let t_retweets = state.tweets
127 | let Stweet_retweets = state.tweet
128 | if(action.payload.msg === "retweeted"){
129 | if(Stweet_retweets){ Stweet_retweets.retweets.push(action.data.id) }
130 | acc_retweets.retweets.push(action.data.id)
131 | for(let i = 0; i < t_retweets.length; i++){
132 | if(t_retweets[i]._id === action.data.id){
133 | t_retweets[i].retweets.push(state.account._id)
134 | }
135 | }
136 | }else if(action.payload.msg === "undo retweet"){
137 | if(Stweet_retweets){
138 | Stweet_retweets.retweets = Stweet_retweets.retweets.filter((x)=>{
139 | return x !== action.data.id
140 | });
141 | }
142 | let accRe_Index = acc_retweets.retweets.indexOf(action.data.id)
143 | accRe_Index > -1 && acc_retweets.retweets.splice(accRe_Index, 1)
144 | if(user_retweets){
145 | user_retweets.tweets = user_retweets.tweets.filter((x)=>{
146 | return x._id !== action.data.id})
147 | }
148 | for(let i = 0; i < t_retweets.length; i++){
149 | if(t_retweets[i]._id === action.data.id){
150 | t_retweets[i].retweets = t_retweets[i].retweets.filter((x)=>{
151 | return x !== state.account._id})
152 | }
153 | }
154 | }
155 | return {...state, ...{user:user_retweets}, ...{account: acc_retweets}, ...{tweets: t_retweets}, ...{tweet: Stweet_retweets}}
156 |
157 | case type.DELETE_TWEET:
158 | let userTweetsD = state.user
159 | let homeTweetsD = state.tweets
160 | let singleTweet = state.tweet
161 | if(userTweetsD){
162 | userTweetsD.tweets = userTweetsD && userTweetsD.tweets.filter((x=>{
163 | return x._id !== action.data }))
164 | }
165 | if(singleTweet && action.data === singleTweet._id){
166 | window.location.replace('/home')
167 | singleTweet = null
168 | }
169 | homeTweetsD = homeTweetsD.filter((x)=>{
170 | return x._id !== action.data
171 | })
172 | return {...state, ...{user: userTweetsD}, ...{tweets: homeTweetsD}, ...{tweet: singleTweet}}
173 |
174 | case type.FOLLOW_USER:
175 | let accountF = state.account
176 | let user_followers = state.followers
177 | if(action.payload.msg === 'follow'){
178 | accountF.following.push(action.data)
179 | }else if(action.payload.msg === 'unfollow'){
180 | accountF.following = accountF.following.filter(f=>{
181 | return f !== action.data })
182 | user_followers = user_followers.filter(f=>{
183 | return f._id !== action.data })
184 | }
185 | return {...state, ...{account: accountF}, ...{followers: user_followers}}
186 |
187 | case type.GET_LIST:
188 | return {...state, ...action.payload}
189 |
190 | case type.EDIT_LIST:
191 | ////
192 | return state
193 |
194 | case type.CREATE_LIST:
195 | let add_list = state.lists
196 | add_list.unshift(action.payload.list)
197 | return {...state, ...{lists: add_list}}
198 |
199 | case type.DELETE_LIST:
200 | ////
201 | return state
202 |
203 | case type.GET_LISTS:
204 | return {...state, ...action.payload}
205 |
206 | case type.GET_TREND:
207 | return {...state, ...action.payload}
208 |
209 | case type.SEARCH:
210 | return {...state, ...action.payload}
211 |
212 | case type.TREND_TWEETS:
213 | let t_tweets = action.payload.tagTweets.tweets
214 | return {...state, ...{tagTweets: t_tweets}}
215 |
216 | case type.ADD_TO_LIST:
217 | let added_list = state.list
218 | if(action.payload.msg === 'user removed'){
219 | added_list.users = added_list.users.filter(x=>{ return x._id !== action.data.userId })
220 | }else{
221 | added_list.users.push({username: action.data.username , _id: action.data.userId, name: action.data.name, profileImg: action.data.profileImg})
222 | }
223 | return {...state, ...{list: added_list}}
224 |
225 | case type.GET_FOLLOWERS:
226 | return {...state, ...action.payload}
227 |
228 | case type.GET_FOLLOWING:
229 | return {...state, ...action.payload}
230 |
231 | case type.SEARCH_USERS:
232 | return {...state, ...action.payload}
233 |
234 | case type.WHO_TO_FOLLOW:
235 | return {...state, ...action.payload}
236 |
237 | case type.GET_CONVERSATIONS:
238 | return {...state, ...action.payload}
239 | case type.START_CHAT:
240 | setTimeout(()=>{action.data.func()},250)
241 | return {...state, ...action.payload}
242 | case type.GET_SINGLE_CONVERSATION:
243 | setTimeout(()=>{action.data.func(action.payload.conversation.messages)},250)
244 | return {...state, ...action.payload}
245 |
246 | default:
247 | return state
248 | }
249 | }
250 |
251 | export { initialState, reducer }
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useReducer } from 'react'
2 | import { reducer, initialState } from './reducers'
3 | import { useActions } from './actions'
4 | import { applyMiddleware } from './middleware'
5 |
6 |
7 | const StoreContext = createContext()
8 | const StoreProvider = ({ children }) => {
9 | const [state, dispatch] = useReducer(reducer, initialState)
10 | const actions = useActions(state, applyMiddleware(dispatch))
11 | return (
12 |
13 | {children}
14 |
15 | )
16 | }
17 |
18 | export { StoreContext, StoreProvider}
--------------------------------------------------------------------------------
/src/store/typeActions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | SET_STATE: 'SET_STATE',
3 | LOGIN: 'LOGIN',
4 | REGISTER: 'REGISTER',
5 | TWEET: 'TWEET',
6 | GET_TWEETS: 'GET_TWEETS',
7 | LIKE_TWEET: 'LIKE_TWEET',
8 | BOOKMARK: 'BOOKMARK',
9 | GET_TWEET: 'GET_TWEET',
10 | GET_ACCOUNT: 'GET_ACCOUNT',
11 | GET_USER: 'GET_USER',
12 | GET_BOOKMARKS: 'GET_BOOKMARKS',
13 | UPDATE_USER: 'UPDATE_USER',
14 | RETWEET: 'RETWEET',
15 | DELETE_TWEET: 'DELETE_TWEET',
16 | FOLLOW_USER: 'FOLLOW_USER',
17 | EDIT_LIST: 'EDIT_LIST',
18 | CREATE_LIST: 'CREATE_LIST',
19 | DELETE_LIST: 'DELETE_LIST',
20 | GET_LISTS: 'GET_LISTS',
21 | LOG_OUT: 'LOG_OUT',
22 | GET_LIST: 'GET_LIST',
23 | GET_TREND: 'GET_TREND',
24 | SEARCH: 'SEARCH',
25 | TREND_TWEETS: 'TREND_TWEETS',
26 | ADD_TO_LIST: 'ADD_TO_LIST',
27 | GET_FOLLOWERS: 'GET_FOLLOWERS',
28 | GET_FOLLOWING: 'GET_FOLLOWING',
29 | SEARCH_USERS: 'SEARCH_USERS',
30 | WHO_TO_FOLLOW: 'WHO_TO_FOLLOW',
31 | GET_CONVERSATIONS: 'GET_CONVERSATIONS',
32 | START_CHAT: 'START_CHAT',
33 | GET_SINGLE_CONVERSATION: 'GET_SINGLE_CONVERSATION'
34 | }
--------------------------------------------------------------------------------