├── .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 |
) 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 |
239 |
240 |
241 | 242 | setName(e.target.value)} type="text" name="name" className="edit-input"/> 243 |
244 |
245 |
246 |
247 | 248 | setDescription(e.target.value)} type="text" name="description" className="edit-input"/> 249 |
250 |
251 |
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 && list-banner} 128 |
129 | 130 | changeBanner()} title=" " id="banner" style={{opacity:'0'}} type="file"/> 131 |
132 |
133 |
134 |
135 |
136 | 137 | setName(e.target.value)} type="text" name="name" className="edit-input"/> 138 |
139 |
140 |
141 |
142 | 143 | setDescription(e.target.value)} type="text" name="description" className="edit-input"/> 144 |
145 |
146 |
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 | 9 | 10 | 17 | 18 | 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 |
Login(e)} className="login-form"> 33 |
34 |
35 | 36 | setUsername(e.target.value)} type="text" name="username" className="login-input"/> 37 |
38 |
39 |
40 |
41 | 42 | setPassword(e.target.value)} type="password" name="password" className="login-input"/> 43 |
44 |
45 | 48 |
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 | 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 |
314 |
315 |
316 | 317 | setName(e.target.value)} type="text" name="name" className="edit-input"/> 318 |
319 |
320 |
321 |
322 | 323 | setBio(e.target.value)} type="text" name="bio" className="edit-input"/> 324 |
325 |
326 |
327 |
328 | 329 | setLocation(e.target.value)} type="text" name="location" className="edit-input"/> 330 |
331 |
332 |
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 |
SignUp(e)} className="signup-form"> 40 |
41 |
42 | 43 | setName(e.target.value)} name="name" type="text" className="signup-input"/> 44 |
45 |
46 |
47 |
48 | 49 | setUsername(e.target.value)} name="username" type="text" className="signup-input"/> 50 |
51 |
52 |
53 |
54 | 55 | setEmail(e.target.value)} name="email" type="email" className="signup-input"/> 56 |
57 |
58 |
59 |
60 | 61 | setPassword(e.target.value)} name="password" type="password" className="signup-input"/> 62 |
63 |
64 | 67 |
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 |
145 |
147 |
: 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | } --------------------------------------------------------------------------------