├── .gitignore ├── Readme.md ├── client ├── .prettierrc ├── README.md ├── public │ └── index.html └── src │ ├── application.tsx │ ├── assets │ └── css │ │ └── dots.css │ ├── components │ ├── AuthRoute │ │ └── index.tsx │ ├── BlogPreview │ │ └── index.tsx │ ├── CenterPiece │ │ └── index.tsx │ ├── ErrorText │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── LoadingComponent │ │ └── index.tsx │ ├── Navigation │ │ └── index.tsx │ └── SuccessText │ │ └── index.tsx │ ├── config │ ├── firebase.ts │ ├── logging.ts │ └── routes.ts │ ├── contexts │ └── user.ts │ ├── index.tsx │ ├── interfaces │ ├── blog.ts │ ├── route.ts │ └── user.ts │ ├── modules │ └── Auth │ │ └── index.ts │ ├── pages │ ├── blog.tsx │ ├── edit.tsx │ ├── home.tsx │ └── login.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts └── server ├── .prettierrc └── src ├── config └── logging.ts ├── controllers ├── blog.ts └── user.ts ├── interfaces ├── blog.ts └── user.ts ├── middleware └── extractFirebaseInfo.ts ├── models ├── blog.ts └── user.ts ├── routes ├── blog.ts └── user.ts └── server.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/*/node_modules 5 | **/*/.pnp 6 | **/*.pnp.js 7 | 8 | # testing 9 | **/*/coverage 10 | 11 | # production 12 | **/*/build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | **/*/config.ts 26 | *.json -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeythelantern/MERN-Stack-Typescript-Blog/ef73b9118cd7123adeb4e0a8fd4c73c748cc90e2/Readme.md -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 200, 4 | "proseWrap": "always", 5 | "tabWidth": 4, 6 | "useTabs": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "semi": true 11 | } -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 38 | 39 | Blog Station 40 | 41 | 42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /client/src/application.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer, useState } from 'react'; 2 | import { Route, RouteComponentProps, Switch } from 'react-router'; 3 | import AuthRoute from './components/AuthRoute'; 4 | import LoadingComponent from './components/LoadingComponent'; 5 | import logging from './config/logging'; 6 | import routes from './config/routes'; 7 | import { initialUserState, UserContextProvider, userReducer } from './contexts/user'; 8 | import { Validate } from './modules/Auth'; 9 | 10 | export interface IApplicationProps { } 11 | 12 | const Application: React.FunctionComponent = props => { 13 | const [userState, userDispatch] = useReducer(userReducer, initialUserState); 14 | const [authStage, setAuthStage] = useState('Checking localstorage ...'); 15 | const [loading, setLoading] = useState(true); 16 | 17 | useEffect(() => { 18 | setTimeout(() => { 19 | CheckLocalStorageForCredentials(); 20 | }, 1000); 21 | 22 | // eslint-disable-next-line 23 | }, []); 24 | 25 | const CheckLocalStorageForCredentials = () => { 26 | setAuthStage('Checking credentials ...'); 27 | 28 | const fire_token = localStorage.getItem('fire_token'); 29 | 30 | if (fire_token === null) 31 | { 32 | userDispatch({ type: 'logout', payload: initialUserState }); 33 | setAuthStage('No credentials found'); 34 | setTimeout(() => { 35 | setLoading(false); 36 | }, 500); 37 | } 38 | else 39 | { 40 | return Validate(fire_token, (error, user) => { 41 | if (error) 42 | { 43 | logging.error(error); 44 | userDispatch({ type: 'logout', payload: initialUserState }); 45 | setLoading(false); 46 | 47 | } 48 | else if (user) 49 | { 50 | userDispatch({ type: 'login', payload: { user, fire_token } }); 51 | setLoading(false); 52 | } 53 | }) 54 | } 55 | } 56 | 57 | const userContextValues = { 58 | userState, 59 | userDispatch 60 | }; 61 | 62 | if (loading) 63 | { 64 | return {authStage}; 65 | } 66 | 67 | return ( 68 | 69 | 70 | {routes.map((route, index) => { 71 | if (route.auth) 72 | { 73 | return ( 74 | } 79 | /> 80 | ); 81 | } 82 | 83 | return ( 84 | } 89 | /> 90 | ); 91 | })} 92 | 93 | 94 | ); 95 | } 96 | 97 | export default Application; -------------------------------------------------------------------------------- /client/src/assets/css/dots.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /** 3 | * 4 | * three-dots.css v0.1.0 5 | * 6 | * https://nzbin.github.io/three-dots/ 7 | * 8 | * Copyright (c) 2018 nzbin 9 | * 10 | * Released under the MIT license 11 | * 12 | */ 13 | /** 14 | * ============================================== 15 | * Dot Elastic 16 | * ============================================== 17 | */ 18 | .dot-elastic { 19 | position: relative; 20 | width: 10px; 21 | height: 10px; 22 | border-radius: 5px; 23 | background-color: #9880ff; 24 | color: #9880ff; 25 | animation: dotElastic 1s infinite linear; 26 | } 27 | 28 | .dot-elastic::before, .dot-elastic::after { 29 | content: ''; 30 | display: inline-block; 31 | position: absolute; 32 | top: 0; 33 | } 34 | 35 | .dot-elastic::before { 36 | left: -15px; 37 | width: 10px; 38 | height: 10px; 39 | border-radius: 5px; 40 | background-color: #9880ff; 41 | color: #9880ff; 42 | animation: dotElasticBefore 1s infinite linear; 43 | } 44 | 45 | .dot-elastic::after { 46 | left: 15px; 47 | width: 10px; 48 | height: 10px; 49 | border-radius: 5px; 50 | background-color: #9880ff; 51 | color: #9880ff; 52 | animation: dotElasticAfter 1s infinite linear; 53 | } 54 | 55 | @keyframes dotElasticBefore { 56 | 0% { 57 | transform: scale(1, 1); 58 | } 59 | 25% { 60 | transform: scale(1, 1.5); 61 | } 62 | 50% { 63 | transform: scale(1, 0.67); 64 | } 65 | 75% { 66 | transform: scale(1, 1); 67 | } 68 | 100% { 69 | transform: scale(1, 1); 70 | } 71 | } 72 | 73 | @keyframes dotElastic { 74 | 0% { 75 | transform: scale(1, 1); 76 | } 77 | 25% { 78 | transform: scale(1, 1); 79 | } 80 | 50% { 81 | transform: scale(1, 1.5); 82 | } 83 | 75% { 84 | transform: scale(1, 1); 85 | } 86 | 100% { 87 | transform: scale(1, 1); 88 | } 89 | } 90 | 91 | @keyframes dotElasticAfter { 92 | 0% { 93 | transform: scale(1, 1); 94 | } 95 | 25% { 96 | transform: scale(1, 1); 97 | } 98 | 50% { 99 | transform: scale(1, 0.67); 100 | } 101 | 75% { 102 | transform: scale(1, 1.5); 103 | } 104 | 100% { 105 | transform: scale(1, 1); 106 | } 107 | } 108 | 109 | /** 110 | * ============================================== 111 | * Dot Pulse 112 | * ============================================== 113 | */ 114 | .dot-pulse { 115 | position: relative; 116 | left: -9999px; 117 | width: 10px; 118 | height: 10px; 119 | border-radius: 5px; 120 | background-color: #9880ff; 121 | color: #9880ff; 122 | box-shadow: 9999px 0 0 -5px #9880ff; 123 | animation: dotPulse 1.5s infinite linear; 124 | animation-delay: .25s; 125 | } 126 | 127 | .dot-pulse::before, .dot-pulse::after { 128 | content: ''; 129 | display: inline-block; 130 | position: absolute; 131 | top: 0; 132 | width: 10px; 133 | height: 10px; 134 | border-radius: 5px; 135 | background-color: #9880ff; 136 | color: #9880ff; 137 | } 138 | 139 | .dot-pulse::before { 140 | box-shadow: 9984px 0 0 -5px #9880ff; 141 | animation: dotPulseBefore 1.5s infinite linear; 142 | animation-delay: 0s; 143 | } 144 | 145 | .dot-pulse::after { 146 | box-shadow: 10014px 0 0 -5px #9880ff; 147 | animation: dotPulseAfter 1.5s infinite linear; 148 | animation-delay: .5s; 149 | } 150 | 151 | @keyframes dotPulseBefore { 152 | 0% { 153 | box-shadow: 9984px 0 0 -5px #9880ff; 154 | } 155 | 30% { 156 | box-shadow: 9984px 0 0 2px #9880ff; 157 | } 158 | 60%, 159 | 100% { 160 | box-shadow: 9984px 0 0 -5px #9880ff; 161 | } 162 | } 163 | 164 | @keyframes dotPulse { 165 | 0% { 166 | box-shadow: 9999px 0 0 -5px #9880ff; 167 | } 168 | 30% { 169 | box-shadow: 9999px 0 0 2px #9880ff; 170 | } 171 | 60%, 172 | 100% { 173 | box-shadow: 9999px 0 0 -5px #9880ff; 174 | } 175 | } 176 | 177 | @keyframes dotPulseAfter { 178 | 0% { 179 | box-shadow: 10014px 0 0 -5px #9880ff; 180 | } 181 | 30% { 182 | box-shadow: 10014px 0 0 2px #9880ff; 183 | } 184 | 60%, 185 | 100% { 186 | box-shadow: 10014px 0 0 -5px #9880ff; 187 | } 188 | } 189 | 190 | /** 191 | * ============================================== 192 | * Dot Flashing 193 | * ============================================== 194 | */ 195 | .dot-flashing { 196 | position: relative; 197 | width: 10px; 198 | height: 10px; 199 | border-radius: 5px; 200 | background-color: #9880ff; 201 | color: #9880ff; 202 | animation: dotFlashing 1s infinite linear alternate; 203 | animation-delay: .5s; 204 | } 205 | 206 | .dot-flashing::before, .dot-flashing::after { 207 | content: ''; 208 | display: inline-block; 209 | position: absolute; 210 | top: 0; 211 | } 212 | 213 | .dot-flashing::before { 214 | left: -15px; 215 | width: 10px; 216 | height: 10px; 217 | border-radius: 5px; 218 | background-color: #9880ff; 219 | color: #9880ff; 220 | animation: dotFlashing 1s infinite alternate; 221 | animation-delay: 0s; 222 | } 223 | 224 | .dot-flashing::after { 225 | left: 15px; 226 | width: 10px; 227 | height: 10px; 228 | border-radius: 5px; 229 | background-color: #9880ff; 230 | color: #9880ff; 231 | animation: dotFlashing 1s infinite alternate; 232 | animation-delay: 1s; 233 | } 234 | 235 | @keyframes dotFlashing { 236 | 0% { 237 | background-color: #9880ff; 238 | } 239 | 50%, 240 | 100% { 241 | background-color: #ebe6ff; 242 | } 243 | } 244 | 245 | /** 246 | * ============================================== 247 | * Dot Collision 248 | * ============================================== 249 | */ 250 | .dot-collision { 251 | position: relative; 252 | width: 10px; 253 | height: 10px; 254 | border-radius: 5px; 255 | background-color: #9880ff; 256 | color: #9880ff; 257 | } 258 | 259 | .dot-collision::before, .dot-collision::after { 260 | content: ''; 261 | display: inline-block; 262 | position: absolute; 263 | top: 0; 264 | } 265 | 266 | .dot-collision::before { 267 | left: -10px; 268 | width: 10px; 269 | height: 10px; 270 | border-radius: 5px; 271 | background-color: #9880ff; 272 | color: #9880ff; 273 | animation: dotCollisionBefore 2s infinite ease-in; 274 | } 275 | 276 | .dot-collision::after { 277 | left: 10px; 278 | width: 10px; 279 | height: 10px; 280 | border-radius: 5px; 281 | background-color: #9880ff; 282 | color: #9880ff; 283 | animation: dotCollisionAfter 2s infinite ease-in; 284 | animation-delay: 1s; 285 | } 286 | 287 | @keyframes dotCollisionBefore { 288 | 0%, 289 | 50%, 290 | 75%, 291 | 100% { 292 | transform: translateX(0); 293 | } 294 | 25% { 295 | transform: translateX(-15px); 296 | } 297 | } 298 | 299 | @keyframes dotCollisionAfter { 300 | 0%, 301 | 50%, 302 | 75%, 303 | 100% { 304 | transform: translateX(0); 305 | } 306 | 25% { 307 | transform: translateX(15px); 308 | } 309 | } 310 | 311 | /** 312 | * ============================================== 313 | * Dot Revolution 314 | * ============================================== 315 | */ 316 | .dot-revolution { 317 | position: relative; 318 | width: 10px; 319 | height: 10px; 320 | border-radius: 5px; 321 | background-color: #9880ff; 322 | color: #9880ff; 323 | } 324 | 325 | .dot-revolution::before, .dot-revolution::after { 326 | content: ''; 327 | display: inline-block; 328 | position: absolute; 329 | } 330 | 331 | .dot-revolution::before { 332 | left: 0; 333 | top: -15px; 334 | width: 10px; 335 | height: 10px; 336 | border-radius: 5px; 337 | background-color: #9880ff; 338 | color: #9880ff; 339 | transform-origin: 5px 20px; 340 | animation: dotRevolution 1.4s linear infinite; 341 | } 342 | 343 | .dot-revolution::after { 344 | left: 0; 345 | top: -30px; 346 | width: 10px; 347 | height: 10px; 348 | border-radius: 5px; 349 | background-color: #9880ff; 350 | color: #9880ff; 351 | transform-origin: 5px 35px; 352 | animation: dotRevolution 1s linear infinite; 353 | } 354 | 355 | @keyframes dotRevolution { 356 | 0% { 357 | transform: rotateZ(0deg) translate3d(0, 0, 0); 358 | } 359 | 100% { 360 | transform: rotateZ(360deg) translate3d(0, 0, 0); 361 | } 362 | } 363 | 364 | /** 365 | * ============================================== 366 | * Dot Carousel 367 | * ============================================== 368 | */ 369 | .dot-carousel { 370 | position: relative; 371 | left: -9999px; 372 | width: 10px; 373 | height: 10px; 374 | border-radius: 5px; 375 | background-color: #9880ff; 376 | color: #9880ff; 377 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 378 | animation: dotCarousel 1.5s infinite linear; 379 | } 380 | 381 | @keyframes dotCarousel { 382 | 0% { 383 | box-shadow: 9984px 0 0 -1px #9880ff, 9999px 0 0 1px #9880ff, 10014px 0 0 -1px #9880ff; 384 | } 385 | 50% { 386 | box-shadow: 10014px 0 0 -1px #9880ff, 9984px 0 0 -1px #9880ff, 9999px 0 0 1px #9880ff; 387 | } 388 | 100% { 389 | box-shadow: 9999px 0 0 1px #9880ff, 10014px 0 0 -1px #9880ff, 9984px 0 0 -1px #9880ff; 390 | } 391 | } 392 | 393 | /** 394 | * ============================================== 395 | * Dot Typing 396 | * ============================================== 397 | */ 398 | .dot-typing { 399 | position: relative; 400 | left: -9999px; 401 | width: 10px; 402 | height: 10px; 403 | border-radius: 5px; 404 | background-color: #9880ff; 405 | color: #9880ff; 406 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 407 | animation: dotTyping 1.5s infinite linear; 408 | } 409 | 410 | @keyframes dotTyping { 411 | 0% { 412 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 413 | } 414 | 16.667% { 415 | box-shadow: 9984px -10px 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 416 | } 417 | 33.333% { 418 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 419 | } 420 | 50% { 421 | box-shadow: 9984px 0 0 0 #9880ff, 9999px -10px 0 0 #9880ff, 10014px 0 0 0 #9880ff; 422 | } 423 | 66.667% { 424 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 425 | } 426 | 83.333% { 427 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px -10px 0 0 #9880ff; 428 | } 429 | 100% { 430 | box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff; 431 | } 432 | } 433 | 434 | /** 435 | * ============================================== 436 | * Dot Windmill 437 | * ============================================== 438 | */ 439 | .dot-windmill { 440 | position: relative; 441 | top: -10px; 442 | width: 10px; 443 | height: 10px; 444 | border-radius: 5px; 445 | background-color: #9880ff; 446 | color: #9880ff; 447 | transform-origin: 5px 15px; 448 | animation: dotWindmill 2s infinite linear; 449 | } 450 | 451 | .dot-windmill::before, .dot-windmill::after { 452 | content: ''; 453 | display: inline-block; 454 | position: absolute; 455 | } 456 | 457 | .dot-windmill::before { 458 | left: -8.66px; 459 | top: 15px; 460 | width: 10px; 461 | height: 10px; 462 | border-radius: 5px; 463 | background-color: #9880ff; 464 | color: #9880ff; 465 | } 466 | 467 | .dot-windmill::after { 468 | left: 8.66px; 469 | top: 15px; 470 | width: 10px; 471 | height: 10px; 472 | border-radius: 5px; 473 | background-color: #9880ff; 474 | color: #9880ff; 475 | } 476 | 477 | @keyframes dotWindmill { 478 | 0% { 479 | transform: rotateZ(0deg) translate3d(0, 0, 0); 480 | } 481 | 100% { 482 | transform: rotateZ(720deg) translate3d(0, 0, 0); 483 | } 484 | } 485 | 486 | /** 487 | * ============================================== 488 | * Dot Bricks 489 | * ============================================== 490 | */ 491 | .dot-bricks { 492 | position: relative; 493 | top: 8px; 494 | left: -9999px; 495 | width: 10px; 496 | height: 10px; 497 | border-radius: 5px; 498 | background-color: #9880ff; 499 | color: #9880ff; 500 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff; 501 | animation: dotBricks 2s infinite ease; 502 | } 503 | 504 | @keyframes dotBricks { 505 | 0% { 506 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff; 507 | } 508 | 8.333% { 509 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff; 510 | } 511 | 16.667% { 512 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 10007px 0 0 0 #9880ff; 513 | } 514 | 25% { 515 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff; 516 | } 517 | 33.333% { 518 | box-shadow: 10007px 0 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff; 519 | } 520 | 41.667% { 521 | box-shadow: 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff; 522 | } 523 | 50% { 524 | box-shadow: 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff; 525 | } 526 | 58.333% { 527 | box-shadow: 9991px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff; 528 | } 529 | 66.666% { 530 | box-shadow: 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff, 9991px -16px 0 0 #9880ff; 531 | } 532 | 75% { 533 | box-shadow: 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff; 534 | } 535 | 83.333% { 536 | box-shadow: 9991px -16px 0 0 #9880ff, 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff; 537 | } 538 | 91.667% { 539 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff; 540 | } 541 | 100% { 542 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff; 543 | } 544 | } 545 | 546 | /** 547 | * ============================================== 548 | * Dot Floating 549 | * ============================================== 550 | */ 551 | .dot-floating { 552 | position: relative; 553 | width: 10px; 554 | height: 10px; 555 | border-radius: 5px; 556 | background-color: #9880ff; 557 | color: #9880ff; 558 | animation: dotFloating 3s infinite cubic-bezier(0.15, 0.6, 0.9, 0.1); 559 | } 560 | 561 | .dot-floating::before, .dot-floating::after { 562 | content: ''; 563 | display: inline-block; 564 | position: absolute; 565 | top: 0; 566 | } 567 | 568 | .dot-floating::before { 569 | left: -12px; 570 | width: 10px; 571 | height: 10px; 572 | border-radius: 5px; 573 | background-color: #9880ff; 574 | color: #9880ff; 575 | animation: dotFloatingBefore 3s infinite ease-in-out; 576 | } 577 | 578 | .dot-floating::after { 579 | left: -24px; 580 | width: 10px; 581 | height: 10px; 582 | border-radius: 5px; 583 | background-color: #9880ff; 584 | color: #9880ff; 585 | animation: dotFloatingAfter 3s infinite cubic-bezier(0.4, 0, 1, 1); 586 | } 587 | 588 | @keyframes dotFloating { 589 | 0% { 590 | left: calc(-50% - 5px); 591 | } 592 | 75% { 593 | left: calc(50% + 105px); 594 | } 595 | 100% { 596 | left: calc(50% + 105px); 597 | } 598 | } 599 | 600 | @keyframes dotFloatingBefore { 601 | 0% { 602 | left: -50px; 603 | } 604 | 50% { 605 | left: -12px; 606 | } 607 | 75% { 608 | left: -50px; 609 | } 610 | 100% { 611 | left: -50px; 612 | } 613 | } 614 | 615 | @keyframes dotFloatingAfter { 616 | 0% { 617 | left: -100px; 618 | } 619 | 50% { 620 | left: -24px; 621 | } 622 | 75% { 623 | left: -100px; 624 | } 625 | 100% { 626 | left: -100px; 627 | } 628 | } 629 | 630 | /** 631 | * ============================================== 632 | * Dot Fire 633 | * ============================================== 634 | */ 635 | .dot-fire { 636 | position: relative; 637 | left: -9999px; 638 | width: 10px; 639 | height: 10px; 640 | border-radius: 5px; 641 | background-color: #9880ff; 642 | color: #9880ff; 643 | box-shadow: 9999px 22.5px 0 -5px #9880ff; 644 | animation: dotFire 1.5s infinite linear; 645 | animation-delay: -.85s; 646 | } 647 | 648 | .dot-fire::before, .dot-fire::after { 649 | content: ''; 650 | display: inline-block; 651 | position: absolute; 652 | top: 0; 653 | width: 10px; 654 | height: 10px; 655 | border-radius: 5px; 656 | background-color: #9880ff; 657 | color: #9880ff; 658 | } 659 | 660 | .dot-fire::before { 661 | box-shadow: 9999px 22.5px 0 -5px #9880ff; 662 | animation: dotFire 1.5s infinite linear; 663 | animation-delay: -1.85s; 664 | } 665 | 666 | .dot-fire::after { 667 | box-shadow: 9999px 22.5px 0 -5px #9880ff; 668 | animation: dotFire 1.5s infinite linear; 669 | animation-delay: -2.85s; 670 | } 671 | 672 | @keyframes dotFire { 673 | 1% { 674 | box-shadow: 9999px 22.5px 0 -5px #9880ff; 675 | } 676 | 50% { 677 | box-shadow: 9999px -5.625px 0 2px #9880ff; 678 | } 679 | 100% { 680 | box-shadow: 9999px -22.5px 0 -5px #9880ff; 681 | } 682 | } 683 | 684 | /** 685 | * ============================================== 686 | * Dot Spin 687 | * ============================================== 688 | */ 689 | .dot-spin { 690 | position: relative; 691 | width: 10px; 692 | height: 10px; 693 | border-radius: 5px; 694 | background-color: transparent; 695 | color: transparent; 696 | box-shadow: 0 -18px 0 0 #9880ff, 12.72984px -12.72984px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.72984px 12.72984px 0 0 rgba(152, 128, 255, 0), 0 18px 0 0 rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 0 rgba(152, 128, 255, 0), -18px 0 0 0 rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 0 rgba(152, 128, 255, 0); 697 | animation: dotSpin 1.5s infinite linear; 698 | } 699 | 700 | @keyframes dotSpin { 701 | 0%, 702 | 100% { 703 | box-shadow: 0 -18px 0 0 #9880ff, 12.72984px -12.72984px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0); 704 | } 705 | 12.5% { 706 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.72984px -12.72984px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.72984px 12.72984px 0 0 #9880ff, 0 18px 0 -5px rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0); 707 | } 708 | 25% { 709 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 0 #9880ff, 12.72984px 12.72984px 0 0 #9880ff, 0 18px 0 0 #9880ff, -12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0); 710 | } 711 | 37.5% { 712 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.72984px 12.72984px 0 0 #9880ff, 0 18px 0 0 #9880ff, -12.72984px 12.72984px 0 0 #9880ff, -18px 0 0 -5px rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0); 713 | } 714 | 50% { 715 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 0 #9880ff, -12.72984px 12.72984px 0 0 #9880ff, -18px 0 0 0 #9880ff, -12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0); 716 | } 717 | 62.5% { 718 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 0 #9880ff, -18px 0 0 0 #9880ff, -12.72984px -12.72984px 0 0 #9880ff; 719 | } 720 | 75% { 721 | box-shadow: 0 -18px 0 0 #9880ff, 12.72984px -12.72984px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 0 #9880ff, -12.72984px -12.72984px 0 0 #9880ff; 722 | } 723 | 87.5% { 724 | box-shadow: 0 -18px 0 0 #9880ff, 12.72984px -12.72984px 0 0 #9880ff, 18px 0 0 -5px rgba(152, 128, 255, 0), 12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.72984px 12.72984px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.72984px -12.72984px 0 0 #9880ff; 725 | } 726 | } 727 | 728 | /** 729 | * ============================================== 730 | * Dot Falling 731 | * ============================================== 732 | */ 733 | .dot-falling { 734 | position: relative; 735 | left: -9999px; 736 | width: 10px; 737 | height: 10px; 738 | border-radius: 5px; 739 | background-color: #9880ff; 740 | color: #9880ff; 741 | box-shadow: 9999px 0 0 0 #9880ff; 742 | animation: dotFalling 1s infinite linear; 743 | animation-delay: .1s; 744 | } 745 | 746 | .dot-falling::before, .dot-falling::after { 747 | content: ''; 748 | display: inline-block; 749 | position: absolute; 750 | top: 0; 751 | } 752 | 753 | .dot-falling::before { 754 | width: 10px; 755 | height: 10px; 756 | border-radius: 5px; 757 | background-color: #9880ff; 758 | color: #9880ff; 759 | animation: dotFallingBefore 1s infinite linear; 760 | animation-delay: 0s; 761 | } 762 | 763 | .dot-falling::after { 764 | width: 10px; 765 | height: 10px; 766 | border-radius: 5px; 767 | background-color: #9880ff; 768 | color: #9880ff; 769 | animation: dotFallingAfter 1s infinite linear; 770 | animation-delay: .2s; 771 | } 772 | 773 | @keyframes dotFalling { 774 | 0% { 775 | box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0); 776 | } 777 | 25%, 778 | 50%, 779 | 75% { 780 | box-shadow: 9999px 0 0 0 #9880ff; 781 | } 782 | 100% { 783 | box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0); 784 | } 785 | } 786 | 787 | @keyframes dotFallingBefore { 788 | 0% { 789 | box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0); 790 | } 791 | 25%, 792 | 50%, 793 | 75% { 794 | box-shadow: 9984px 0 0 0 #9880ff; 795 | } 796 | 100% { 797 | box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0); 798 | } 799 | } 800 | 801 | @keyframes dotFallingAfter { 802 | 0% { 803 | box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0); 804 | } 805 | 25%, 806 | 50%, 807 | 75% { 808 | box-shadow: 10014px 0 0 0 #9880ff; 809 | } 810 | 100% { 811 | box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0); 812 | } 813 | } 814 | 815 | /** 816 | * ============================================== 817 | * Dot Stretching 818 | * ============================================== 819 | */ 820 | .dot-stretching { 821 | position: relative; 822 | width: 10px; 823 | height: 10px; 824 | border-radius: 5px; 825 | background-color: #9880ff; 826 | color: #9880ff; 827 | transform: scale(1.25, 1.25); 828 | animation: dotStretching 2s infinite ease-in; 829 | } 830 | 831 | .dot-stretching::before, .dot-stretching::after { 832 | content: ''; 833 | display: inline-block; 834 | position: absolute; 835 | top: 0; 836 | } 837 | 838 | .dot-stretching::before { 839 | width: 10px; 840 | height: 10px; 841 | border-radius: 5px; 842 | background-color: #9880ff; 843 | color: #9880ff; 844 | animation: dotStretchingBefore 2s infinite ease-in; 845 | } 846 | 847 | .dot-stretching::after { 848 | width: 10px; 849 | height: 10px; 850 | border-radius: 5px; 851 | background-color: #9880ff; 852 | color: #9880ff; 853 | animation: dotStretchingAfter 2s infinite ease-in; 854 | } 855 | 856 | @keyframes dotStretching { 857 | 0% { 858 | transform: scale(1.25, 1.25); 859 | } 860 | 50%, 861 | 60% { 862 | transform: scale(0.8, 0.8); 863 | } 864 | 100% { 865 | transform: scale(1.25, 1.25); 866 | } 867 | } 868 | 869 | @keyframes dotStretchingBefore { 870 | 0% { 871 | transform: translate(0) scale(0.7, 0.7); 872 | } 873 | 50%, 874 | 60% { 875 | transform: translate(-20px) scale(1, 1); 876 | } 877 | 100% { 878 | transform: translate(0) scale(0.7, 0.7); 879 | } 880 | } 881 | 882 | @keyframes dotStretchingAfter { 883 | 0% { 884 | transform: translate(0) scale(0.7, 0.7); 885 | } 886 | 50%, 887 | 60% { 888 | transform: translate(20px) scale(1, 1); 889 | } 890 | 100% { 891 | transform: translate(0) scale(0.7, 0.7); 892 | } 893 | } 894 | 895 | /** 896 | * ============================================== 897 | * Experiment-Gooey Effect 898 | * Dot Gathering 899 | * ============================================== 900 | */ 901 | .dot-gathering { 902 | position: relative; 903 | width: 12px; 904 | height: 12px; 905 | border-radius: 6px; 906 | background-color: black; 907 | color: transparent; 908 | margin: -1px 0; 909 | filter: blur(2px); 910 | } 911 | 912 | .dot-gathering::before, .dot-gathering::after { 913 | content: ''; 914 | display: inline-block; 915 | position: absolute; 916 | top: 0; 917 | left: -50px; 918 | width: 12px; 919 | height: 12px; 920 | border-radius: 6px; 921 | background-color: black; 922 | color: transparent; 923 | opacity: 0; 924 | filter: blur(2px); 925 | animation: dotGathering 2s infinite ease-in; 926 | } 927 | 928 | .dot-gathering::after { 929 | animation-delay: .5s; 930 | } 931 | 932 | @keyframes dotGathering { 933 | 0% { 934 | opacity: 0; 935 | transform: translateX(0); 936 | } 937 | 35%, 938 | 60% { 939 | opacity: 1; 940 | transform: translateX(50px); 941 | } 942 | 100% { 943 | opacity: 0; 944 | transform: translateX(100px); 945 | } 946 | } 947 | 948 | /** 949 | * ============================================== 950 | * Experiment-Gooey Effect 951 | * Dot Hourglass 952 | * ============================================== 953 | */ 954 | .dot-hourglass { 955 | position: relative; 956 | top: -15px; 957 | width: 12px; 958 | height: 12px; 959 | border-radius: 6px; 960 | background-color: black; 961 | color: transparent; 962 | margin: -1px 0; 963 | filter: blur(2px); 964 | transform-origin: 5px 20px; 965 | animation: dotHourglass 2.4s infinite ease-in-out; 966 | animation-delay: .6s; 967 | } 968 | 969 | .dot-hourglass::before, .dot-hourglass::after { 970 | content: ''; 971 | display: inline-block; 972 | position: absolute; 973 | top: 0; 974 | left: 0; 975 | width: 12px; 976 | height: 12px; 977 | border-radius: 6px; 978 | background-color: black; 979 | color: transparent; 980 | filter: blur(2px); 981 | } 982 | 983 | .dot-hourglass::before { 984 | top: 30px; 985 | } 986 | 987 | .dot-hourglass::after { 988 | animation: dotHourglassAfter 2.4s infinite cubic-bezier(0.65, 0.05, 0.36, 1); 989 | } 990 | 991 | @keyframes dotHourglass { 992 | 0% { 993 | transform: rotateZ(0deg); 994 | } 995 | 25% { 996 | transform: rotateZ(180deg); 997 | } 998 | 50% { 999 | transform: rotateZ(180deg); 1000 | } 1001 | 75% { 1002 | transform: rotateZ(360deg); 1003 | } 1004 | 100% { 1005 | transform: rotateZ(360deg); 1006 | } 1007 | } 1008 | 1009 | @keyframes dotHourglassAfter { 1010 | 0% { 1011 | transform: translateY(0); 1012 | } 1013 | 25% { 1014 | transform: translateY(30px); 1015 | } 1016 | 50% { 1017 | transform: translateY(30px); 1018 | } 1019 | 75% { 1020 | transform: translateY(0); 1021 | } 1022 | 100% { 1023 | transform: translateY(0); 1024 | } 1025 | } 1026 | 1027 | /** 1028 | * ============================================== 1029 | * Experiment-Gooey Effect 1030 | * Dot Overtaking 1031 | * ============================================== 1032 | */ 1033 | .dot-overtaking { 1034 | position: relative; 1035 | width: 12px; 1036 | height: 12px; 1037 | border-radius: 6px; 1038 | background-color: transparent; 1039 | color: black; 1040 | margin: -1px 0; 1041 | box-shadow: 0 -20px 0 0; 1042 | filter: blur(2px); 1043 | animation: dotOvertaking 2s infinite cubic-bezier(0.2, 0.6, 0.8, 0.2); 1044 | } 1045 | 1046 | .dot-overtaking::before, .dot-overtaking::after { 1047 | content: ''; 1048 | display: inline-block; 1049 | position: absolute; 1050 | top: 0; 1051 | left: 0; 1052 | width: 12px; 1053 | height: 12px; 1054 | border-radius: 6px; 1055 | background-color: transparent; 1056 | color: black; 1057 | box-shadow: 0 -20px 0 0; 1058 | filter: blur(2px); 1059 | } 1060 | 1061 | .dot-overtaking::before { 1062 | animation: dotOvertaking 2s infinite cubic-bezier(0.2, 0.6, 0.8, 0.2); 1063 | animation-delay: .3s; 1064 | } 1065 | 1066 | .dot-overtaking::after { 1067 | animation: dotOvertaking 1.5s infinite cubic-bezier(0.2, 0.6, 0.8, 0.2); 1068 | animation-delay: .6s; 1069 | } 1070 | 1071 | @keyframes dotOvertaking { 1072 | 0% { 1073 | transform: rotateZ(0deg); 1074 | } 1075 | 100% { 1076 | transform: rotateZ(360deg); 1077 | } 1078 | } 1079 | 1080 | /** 1081 | * ============================================== 1082 | * Experiment-Gooey Effect 1083 | * Dot Shuttle 1084 | * ============================================== 1085 | */ 1086 | .dot-shuttle { 1087 | position: relative; 1088 | left: -15px; 1089 | width: 12px; 1090 | height: 12px; 1091 | border-radius: 6px; 1092 | background-color: black; 1093 | color: transparent; 1094 | margin: -1px 0; 1095 | filter: blur(2px); 1096 | } 1097 | 1098 | .dot-shuttle::before, .dot-shuttle::after { 1099 | content: ''; 1100 | display: inline-block; 1101 | position: absolute; 1102 | top: 0; 1103 | width: 12px; 1104 | height: 12px; 1105 | border-radius: 6px; 1106 | background-color: black; 1107 | color: transparent; 1108 | filter: blur(2px); 1109 | } 1110 | 1111 | .dot-shuttle::before { 1112 | left: 15px; 1113 | animation: dotShuttle 2s infinite ease-out; 1114 | } 1115 | 1116 | .dot-shuttle::after { 1117 | left: 30px; 1118 | } 1119 | 1120 | @keyframes dotShuttle { 1121 | 0%, 1122 | 50%, 1123 | 100% { 1124 | transform: translateX(0); 1125 | } 1126 | 25% { 1127 | transform: translateX(-45px); 1128 | } 1129 | 75% { 1130 | transform: translateX(45px); 1131 | } 1132 | } 1133 | 1134 | /** 1135 | * ============================================== 1136 | * Experiment-Emoji 1137 | * Dot Bouncing 1138 | * ============================================== 1139 | */ 1140 | .dot-bouncing { 1141 | position: relative; 1142 | height: 10px; 1143 | font-size: 10px; 1144 | } 1145 | 1146 | .dot-bouncing::before { 1147 | content: '⚽🏀🏐'; 1148 | display: inline-block; 1149 | position: relative; 1150 | animation: dotBouncing 1s infinite; 1151 | } 1152 | 1153 | @keyframes dotBouncing { 1154 | 0% { 1155 | top: -20px; 1156 | animation-timing-function: ease-in; 1157 | } 1158 | 34% { 1159 | transform: scale(1, 1); 1160 | } 1161 | 35% { 1162 | top: 20px; 1163 | animation-timing-function: ease-out; 1164 | transform: scale(1.5, 0.5); 1165 | } 1166 | 45% { 1167 | transform: scale(1, 1); 1168 | } 1169 | 90% { 1170 | top: -20px; 1171 | } 1172 | 100% { 1173 | top: -20px; 1174 | } 1175 | } 1176 | 1177 | /** 1178 | * ============================================== 1179 | * Experiment-Emoji 1180 | * Dot Rolling 1181 | * ============================================== 1182 | */ 1183 | .dot-rolling { 1184 | position: relative; 1185 | height: 10px; 1186 | font-size: 10px; 1187 | } 1188 | 1189 | .dot-rolling::before { 1190 | content: '⚽'; 1191 | display: inline-block; 1192 | position: relative; 1193 | transform: translateX(-25px); 1194 | animation: dotRolling 3s infinite; 1195 | } 1196 | 1197 | @keyframes dotRolling { 1198 | 0% { 1199 | content: '⚽'; 1200 | transform: translateX(-25px) rotateZ(0deg); 1201 | } 1202 | 16.667% { 1203 | content: '⚽'; 1204 | transform: translateX(25px) rotateZ(720deg); 1205 | } 1206 | 33.333% { 1207 | content: '⚽'; 1208 | transform: translateX(-25px) rotateZ(0deg); 1209 | } 1210 | 34.333% { 1211 | content: '🏀'; 1212 | transform: translateX(-25px) rotateZ(0deg); 1213 | } 1214 | 50% { 1215 | content: '🏀'; 1216 | transform: translateX(25px) rotateZ(720deg); 1217 | } 1218 | 66.667% { 1219 | content: '🏀'; 1220 | transform: translateX(-25px) rotateZ(0deg); 1221 | } 1222 | 67.667% { 1223 | content: '🏐'; 1224 | transform: translateX(-25px) rotateZ(0deg); 1225 | } 1226 | 83.333% { 1227 | content: '🏐'; 1228 | transform: translateX(25px) rotateZ(720deg); 1229 | } 1230 | 100% { 1231 | content: '🏐'; 1232 | transform: translateX(-25px) rotateZ(0deg); 1233 | } 1234 | } 1235 | 1236 | /*# sourceMappingURL=three-dots.css.map */ -------------------------------------------------------------------------------- /client/src/components/AuthRoute/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import logging from '../../config/logging'; 4 | import UserContext from '../../contexts/user'; 5 | 6 | export interface IAuthRouteProps {} 7 | 8 | const AuthRoute: React.FunctionComponent = props => { 9 | const { children } = props; 10 | 11 | const userContext = useContext(UserContext); 12 | 13 | if (userContext.userState.user._id === '') 14 | { 15 | logging.info('Unauthorized, redirecting.'); 16 | return 17 | } 18 | else 19 | { 20 | return <>{children} 21 | } 22 | } 23 | 24 | export default AuthRoute; -------------------------------------------------------------------------------- /client/src/components/BlogPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Card, CardBody } from 'reactstrap'; 4 | 5 | export interface IBlogPreviewProps { 6 | _id: string; 7 | title: string; 8 | headline: string; 9 | author: string; 10 | createdAt: string; 11 | updatedAt: string; 12 | } 13 | 14 | const BlogPreview: React.FunctionComponent = props => { 15 | const { _id, author, children, createdAt, updatedAt, headline, title } = props; 16 | 17 | return ( 18 | 19 | 20 | 25 |

{title}

26 |

{headline}


27 | 28 | {createdAt !== updatedAt ? 29 |

Updated by {author} at {new Date(updatedAt).toLocaleString()}

30 | : 31 |

Posted by {author} at {new Date(createdAt).toLocaleString()}

32 | } 33 | {children} 34 |
35 |
36 | ); 37 | } 38 | 39 | export default BlogPreview; -------------------------------------------------------------------------------- /client/src/components/CenterPiece/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'reactstrap'; 3 | 4 | export interface ICenterPieceProps {} 5 | 6 | const CenterPiece: React.FunctionComponent = props => { 7 | const { children } = props; 8 | 9 | return ( 10 | 11 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | 27 | export default CenterPiece; -------------------------------------------------------------------------------- /client/src/components/ErrorText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface IErrorTextProps { 4 | error: string; 5 | } 6 | 7 | const ErrorText: React.FunctionComponent = props => { 8 | const { error } = props; 9 | 10 | if (error === '') return null; 11 | 12 | return {error}; 13 | } 14 | 15 | export default ErrorText; -------------------------------------------------------------------------------- /client/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Col, Container, Row } from 'reactstrap'; 3 | 4 | export interface IHeaderProps { 5 | height?: string; 6 | image?: string; 7 | title: string; 8 | headline: string; 9 | } 10 | 11 | const Header: React.FunctionComponent = props => { 12 | const { children, height, image, headline, title } = props; 13 | 14 | let headerStyle = { 15 | background: 'linear-gradient(rgba(36, 20, 38, 0.5), rgba(36, 39, 38, 0.5)), url(' + image + ') no-repeat center center', 16 | WebkitBackgroundSize: 'cover', 17 | MozBackgroundSize: 'cover', 18 | OBackgroundSize: 'cover', 19 | backgroundSize: 'cover', 20 | backgroundRepeat: 'no-repeat', 21 | backgroundPosition: 'center', 22 | width: '100%', 23 | height: height 24 | }; 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 |

{title}

32 |

{headline}

33 | {children} 34 | 35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | Header.defaultProps = { 42 | height: '100%', 43 | image: 'https://images.unsplash.com/photo-1488998427799-e3362cec87c3?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80' 44 | } 45 | 46 | export default Header; -------------------------------------------------------------------------------- /client/src/components/LoadingComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardBody } from 'reactstrap'; 3 | import CenterPiece from '../CenterPiece'; 4 | 5 | export interface ILoadingProps { 6 | dotType?: string; 7 | } 8 | 9 | export const Loading: React.FunctionComponent = props => { 10 | const { children, dotType } = props; 11 | 12 | return ( 13 |
14 |
15 |
16 |
17 | {children} 18 |
19 | ) 20 | } 21 | 22 | Loading.defaultProps = { 23 | dotType: 'dot-bricks' 24 | } 25 | 26 | export interface ILoadingComponentProps { 27 | card?: boolean; 28 | dotType?: string; 29 | } 30 | 31 | const LoadingComponent: React.FunctionComponent = props => { 32 | const { card, children, dotType } = props; 33 | 34 | if (card) 35 | { 36 | return ( 37 | 38 | 39 | 40 | 41 | {children} 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 |
51 |
52 |
53 |
54 | {children} 55 |
56 | ); 57 | } 58 | 59 | LoadingComponent.defaultProps = { 60 | card: true, 61 | dotType: 'dot-bricks' 62 | } 63 | 64 | export default LoadingComponent; -------------------------------------------------------------------------------- /client/src/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Navbar, NavbarBrand, Nav, NavbarText, Container, Button } from 'reactstrap'; 4 | import UserContext from '../../contexts/user'; 5 | 6 | export interface INavigationProps { } 7 | 8 | const Navigation: React.FunctionComponent = props => { 9 | const userContext = useContext(UserContext); 10 | const { user } = userContext.userState; 11 | 12 | const logout = () => { 13 | userContext.userDispatch({ type: 'logout', payload: userContext.userState }); 14 | } 15 | 16 | return ( 17 | 18 | 19 | 📝 20 | 21 | {user._id !== '' ? 22 |
23 | 27 | | 28 | 31 |
32 | 33 | : 34 |
35 | Login 36 | | 37 | Signup 38 |
39 | } 40 |
41 |
42 | ); 43 | } 44 | 45 | export default Navigation; -------------------------------------------------------------------------------- /client/src/components/SuccessText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface ISuccessTextProps { 4 | success: string; 5 | } 6 | 7 | const SuccessText: React.FunctionComponent = props => { 8 | const { success } = props; 9 | 10 | if (success === '') return null; 11 | 12 | return {success}; 13 | } 14 | 15 | export default SuccessText; -------------------------------------------------------------------------------- /client/src/config/firebase.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import 'firebase/firestore'; 4 | import config from '../config/config'; 5 | 6 | const Firebase = firebase.initializeApp(config.firebase); 7 | 8 | export const Providers = { 9 | google: new firebase.auth.GoogleAuthProvider() 10 | }; 11 | 12 | export const auth = firebase.auth(); 13 | export default Firebase; 14 | -------------------------------------------------------------------------------- /client/src/config/logging.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_NAMESPACE = 'Client'; 2 | 3 | const info = (message: any, namespace?: string) => { 4 | if (typeof message === 'string') { 5 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [INFO] ${message}`); 6 | } else { 7 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [INFO]`, message); 8 | } 9 | }; 10 | 11 | const warn = (message: any, namespace?: string) => { 12 | if (typeof message === 'string') { 13 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [WARN] ${message}`); 14 | } else { 15 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [WARN]`, message); 16 | } 17 | }; 18 | 19 | const error = (message: any, namespace?: string) => { 20 | if (typeof message === 'string') { 21 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [ERROR] ${message}`); 22 | } else { 23 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [ERROR]`, message); 24 | } 25 | }; 26 | 27 | const getDate = () => { 28 | return new Date().toISOString(); 29 | }; 30 | 31 | const logging = { info, warn, error }; 32 | 33 | export default logging; -------------------------------------------------------------------------------- /client/src/config/routes.ts: -------------------------------------------------------------------------------- 1 | import IRoute from '../interfaces/route'; 2 | import HomePage from '../pages/home'; 3 | import LoginPage from '../pages/login'; 4 | import EditPage from '../pages/edit'; 5 | import BlogPage from '../pages/blog'; 6 | 7 | const authRoutes: IRoute[] = [ 8 | { 9 | name: 'Login', 10 | path: '/login', 11 | exact: true, 12 | component: LoginPage, 13 | auth: false 14 | }, 15 | { 16 | name: 'Sign Up', 17 | path: '/register', 18 | exact: true, 19 | component: LoginPage, 20 | auth: false 21 | } 22 | ]; 23 | 24 | const blogRoutes: IRoute[] = [ 25 | { 26 | name: 'Create', 27 | path: '/edit', 28 | exact: true, 29 | component: EditPage, 30 | auth: true 31 | }, 32 | { 33 | name: 'Edit', 34 | path: '/edit/:blogID', 35 | exact: true, 36 | component: EditPage, 37 | auth: true 38 | }, 39 | { 40 | name: 'Blog', 41 | path: '/blogs/:blogID', 42 | exact: true, 43 | component: BlogPage, 44 | auth: false 45 | } 46 | ]; 47 | 48 | const mainRoutes: IRoute[] = [ 49 | { 50 | name: 'Home', 51 | path: '/', 52 | exact: true, 53 | component: HomePage, 54 | auth: false 55 | } 56 | ]; 57 | 58 | const routes: IRoute[] = [...authRoutes, ...blogRoutes, ...mainRoutes]; 59 | 60 | export default routes; 61 | -------------------------------------------------------------------------------- /client/src/contexts/user.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import IUser, { DEFAULT_FIRE_TOKEN, DEFAULT_USER } from '../interfaces/user'; 3 | 4 | export interface IUserState { 5 | user: IUser; 6 | fire_token: string; 7 | } 8 | 9 | export interface IUserActions { 10 | type: 'login' | 'logout' | 'authenticate'; 11 | payload: { 12 | user: IUser; 13 | fire_token: string; 14 | }; 15 | } 16 | 17 | export const initialUserState: IUserState = { 18 | user: DEFAULT_USER, 19 | fire_token: DEFAULT_FIRE_TOKEN 20 | }; 21 | 22 | export const userReducer = (state: IUserState, action: IUserActions) => { 23 | let user = action.payload.user; 24 | let fire_token = action.payload.fire_token; 25 | 26 | switch (action.type) { 27 | case 'login': 28 | localStorage.setItem('fire_token', fire_token); 29 | 30 | return { user, fire_token }; 31 | case 'logout': 32 | localStorage.removeItem('fire_token'); 33 | 34 | return initialUserState; 35 | default: 36 | return state; 37 | } 38 | }; 39 | 40 | export interface IUserContextProps { 41 | userState: IUserState; 42 | userDispatch: React.Dispatch; 43 | } 44 | 45 | const UserContext = createContext({ 46 | userState: initialUserState, 47 | userDispatch: () => {} 48 | }); 49 | 50 | export const UserContextConsumer = UserContext.Consumer; 51 | export const UserContextProvider = UserContext.Provider; 52 | export default UserContext; 53 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import Application from './application'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import './assets/css/dots.css' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /client/src/interfaces/blog.ts: -------------------------------------------------------------------------------- 1 | import IUser from './user'; 2 | 3 | export default interface IBlog { 4 | _id: string; 5 | title: string; 6 | author: string | IUser; 7 | content: string; 8 | headline: string; 9 | picture?: string; 10 | createdAt: string; 11 | updatedAt: string; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/interfaces/route.ts: -------------------------------------------------------------------------------- 1 | export default interface IRoute { 2 | path: string; 3 | name: string; 4 | exact: boolean; 5 | auth: boolean; 6 | component: any; 7 | props?: any; 8 | } -------------------------------------------------------------------------------- /client/src/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | export default interface IUser { 2 | _id: string; 3 | uid: string; 4 | name: string; 5 | } 6 | 7 | export const DEFAULT_USER: IUser = { 8 | _id: '', 9 | uid: '', 10 | name: '' 11 | }; 12 | 13 | export const DEFAULT_FIRE_TOKEN = ''; 14 | -------------------------------------------------------------------------------- /client/src/modules/Auth/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import firebase from 'firebase'; 3 | import { auth } from '../../config/firebase'; 4 | import config from '../../config/config'; 5 | import logging from '../../config/logging'; 6 | import IUser from '../../interfaces/user'; 7 | 8 | const NAMESPACE = 'Auth'; 9 | 10 | export const Authenticate = async (uid: string, name: string, fire_token: string, callback: (error: string | null, user: IUser | null) => void) => { 11 | try { 12 | let response = await axios({ 13 | method: 'POST', 14 | url: `${config.server.url}/users/login`, 15 | data: { 16 | uid, 17 | name 18 | }, 19 | headers: { Authorization: `Bearer ${fire_token}` } 20 | }); 21 | 22 | if (response.status === 200 || response.status === 201 || response.status === 304) { 23 | logging.info('Successfully authenticated.', NAMESPACE); 24 | callback(null, response.data.user); 25 | } else { 26 | logging.warn('Unable to authenticate.', NAMESPACE); 27 | callback('Unable to authenticate.', null); 28 | } 29 | } catch (error) { 30 | logging.error(error, NAMESPACE); 31 | callback('Unable to authenticate.', null); 32 | } 33 | }; 34 | 35 | export const Validate = async (fire_token: string, callback: (error: string | null, user: IUser | null) => void) => { 36 | try { 37 | let response = await axios({ 38 | method: 'GET', 39 | url: `${config.server.url}/users/validate`, 40 | headers: { Authorization: `Bearer ${fire_token}` } 41 | }); 42 | 43 | if (response.status === 200 || response.status === 304) { 44 | logging.info('Successfully validated.', NAMESPACE); 45 | callback(null, response.data.user); 46 | } else { 47 | logging.warn(response, NAMESPACE); 48 | callback('Unable to validate.', null); 49 | } 50 | } catch (error) { 51 | logging.error(error, NAMESPACE); 52 | callback('Unable to validate.', null); 53 | } 54 | }; 55 | 56 | export const SignInWithSocialMedia = (provider: firebase.auth.AuthProvider) => 57 | new Promise((resolve, reject) => { 58 | auth.signInWithPopup(provider) 59 | .then((result) => resolve(result)) 60 | .catch((error) => reject(error)); 61 | }); 62 | -------------------------------------------------------------------------------- /client/src/pages/blog.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useContext, useEffect, useState } from 'react'; 3 | import { Redirect, RouteComponentProps, useHistory, withRouter } from 'react-router'; 4 | import { Link } from 'react-router-dom'; 5 | import { Button, Container, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; 6 | import ErrorText from '../components/ErrorText'; 7 | import Header from '../components/Header'; 8 | import LoadingComponent, { Loading } from '../components/LoadingComponent'; 9 | import Navigation from '../components/Navigation'; 10 | import config from '../config/config'; 11 | import UserContext from '../contexts/user'; 12 | import IBlog from '../interfaces/blog'; 13 | import IUser from '../interfaces/user'; 14 | 15 | const BlogPage: React.FunctionComponent> = props => { 16 | const [_id, setId] = useState(''); 17 | const [blog, setBlog] = useState(null); 18 | const [loading, setLoading] = useState(true); 19 | const [error, setError] = useState(''); 20 | 21 | const [modal, setModal] = useState(false); 22 | const [deleting, setDeleting] = useState(false); 23 | 24 | const { user } = useContext(UserContext).userState; 25 | const history = useHistory(); 26 | 27 | useEffect(() => { 28 | let _blogId = props.match.params.blogID; 29 | 30 | if (_blogId) 31 | { 32 | setId(_blogId); 33 | } 34 | else 35 | { 36 | history.push('/'); 37 | } 38 | 39 | // eslint-disable-next-line 40 | }, []); 41 | 42 | useEffect(() => { 43 | if (_id !== '') 44 | getBlog(); 45 | 46 | // eslint-disable-next-line 47 | }, [_id]) 48 | 49 | const getBlog = async () => { 50 | try 51 | { 52 | const response = await axios({ 53 | method: 'GET', 54 | url: `${config.server.url}/blogs/read/${_id}`, 55 | }); 56 | 57 | if (response.status === (200 || 304)) 58 | { 59 | setBlog(response.data.blog); 60 | } 61 | else 62 | { 63 | setError(`Unable to retrieve blog ${_id}`); 64 | } 65 | } 66 | catch (error) 67 | { 68 | setError(error.message); 69 | } 70 | finally 71 | { 72 | setTimeout(() => { 73 | setLoading(false); 74 | }, 500); 75 | } 76 | } 77 | 78 | const deleteBlog = async () => { 79 | setDeleting(true); 80 | 81 | try 82 | { 83 | const response = await axios({ 84 | method: 'DELETE', 85 | url: `${config.server.url}/blogs/${_id}`, 86 | }); 87 | 88 | if (response.status === 201) 89 | { 90 | setTimeout(() => { 91 | history.push('/'); 92 | }, 1000); 93 | } 94 | else 95 | { 96 | setError(`Unable to retrieve blog ${_id}`); 97 | setDeleting(false); 98 | } 99 | } 100 | catch (error) 101 | { 102 | setError(error.message); 103 | setDeleting(false); 104 | } 105 | } 106 | 107 | 108 | if (loading) return Loading Blog ...; 109 | 110 | if (blog) 111 | { 112 | return ( 113 | 114 | 115 | 116 | Delete 117 | 118 | {deleting ? 119 | 120 | : 121 | "Are you sure you want to delete this blog?" 122 | } 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
135 |

Posted by {(blog.author as IUser).name} on {new Date(blog.createdAt).toLocaleString()}

136 |
137 | 138 | {user._id === (blog.author as IUser)._id && 139 | 140 | 141 | 142 |
143 |
144 | } 145 | 146 |
147 | 148 | 149 | ) 150 | } 151 | else 152 | { 153 | return ; 154 | } 155 | } 156 | 157 | export default withRouter(BlogPage); -------------------------------------------------------------------------------- /client/src/pages/edit.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { RouteComponentProps, withRouter } from 'react-router'; 3 | import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap'; 4 | import axios from 'axios'; 5 | import ErrorText from '../components/ErrorText'; 6 | import Header from '../components/Header'; 7 | import LoadingComponent from '../components/LoadingComponent'; 8 | import Navigation from '../components/Navigation'; 9 | import config from '../config/config'; 10 | import logging from '../config/logging'; 11 | import UserContext from '../contexts/user'; 12 | import { EditorState, ContentState, convertToRaw } from 'draft-js'; 13 | import { Editor } from "react-draft-wysiwyg"; 14 | import draftToHtml from 'draftjs-to-html'; 15 | import htmlToDraft from 'html-to-draftjs'; 16 | import SuccessText from '../components/SuccessText'; 17 | import { Link } from 'react-router-dom'; 18 | import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; 19 | 20 | const EditPage: React.FunctionComponent> = props => { 21 | const [_id, setId] = useState(''); 22 | const [title, setTitle] = useState(''); 23 | const [picture, setPicture] = useState(''); 24 | const [content, setContent] = useState(''); 25 | const [headline, setHeadline] = useState(''); 26 | const [editorState, setEditorState] = useState(EditorState.createEmpty()); 27 | 28 | const [saving, setSaving] = useState(false); 29 | const [loading, setLoading] = useState(true); 30 | const [success, setSuccess] = useState(''); 31 | const [error, setError] = useState(''); 32 | 33 | const { user } = useContext(UserContext).userState; 34 | 35 | useEffect(() => { 36 | let blogID = props.match.params.blogID; 37 | 38 | if (blogID) 39 | { 40 | setId(blogID); 41 | getBlog(blogID); 42 | } 43 | else 44 | { 45 | setLoading(false); 46 | } 47 | 48 | // eslint-disable-next-line 49 | }, []); 50 | 51 | const getBlog = async (id: string) => { 52 | try 53 | { 54 | const response = await axios({ 55 | method: 'GET', 56 | url: `${config.server.url}/blogs/read/${id}`, 57 | }); 58 | 59 | if (response.status === (200 || 304)) 60 | { 61 | if (user._id !== response.data.blog.author._id) 62 | { 63 | logging.warn(`This blog is owned by someone else.`); 64 | setId(''); 65 | } 66 | else 67 | { 68 | setTitle(response.data.blog.title); 69 | setContent(response.data.blog.content); 70 | setHeadline(response.data.blog.headline); 71 | setPicture(response.data.blog.picture || ''); 72 | 73 | /** Convert html string to draft JS */ 74 | const contentBlock = htmlToDraft(response.data.blog.content); 75 | const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); 76 | const editorState = EditorState.createWithContent(contentState); 77 | 78 | setEditorState(editorState); 79 | } 80 | } 81 | else 82 | { 83 | setError(`Unable to retrieve blog ${_id}`); 84 | } 85 | } 86 | catch (error) 87 | { 88 | setError(error.message); 89 | } 90 | finally 91 | { 92 | setLoading(false); 93 | } 94 | } 95 | 96 | const createBlog = async () => { 97 | if (title === '' || headline === '' || content === '') 98 | { 99 | setError('Please fill out all fields.'); 100 | setSuccess(''); 101 | return null; 102 | } 103 | 104 | setError(''); 105 | setSuccess(''); 106 | setSaving(true); 107 | 108 | try 109 | { 110 | const response = await axios({ 111 | method: 'POST', 112 | url: `${config.server.url}/blogs/create`, 113 | data: { 114 | title, 115 | picture, 116 | headline, 117 | content, 118 | author: user._id 119 | } 120 | }); 121 | 122 | if (response.status === 201) 123 | { 124 | setId(response.data.blog._id); 125 | setSuccess('Blog posted. You can continue to edit on this page.'); 126 | } 127 | else 128 | { 129 | setError(`Unable to save blog.`); 130 | } 131 | } 132 | catch (error) 133 | { 134 | setError(error.message); 135 | } 136 | finally 137 | { 138 | setSaving(false); 139 | } 140 | } 141 | 142 | const editBlog = async () => { 143 | if (title === '' || headline === '' || content === '') 144 | { 145 | setError('Please fill out all fields.'); 146 | setSuccess(''); 147 | return null; 148 | } 149 | 150 | setError(''); 151 | setSuccess(''); 152 | setSaving(true); 153 | 154 | try 155 | { 156 | const response = await axios({ 157 | method: 'PATCH', 158 | url: `${config.server.url}/blogs/update/${_id}`, 159 | data: { 160 | title, 161 | picture, 162 | headline, 163 | content 164 | } 165 | }); 166 | 167 | if (response.status === 201) 168 | { 169 | setSuccess('Blog updated.'); 170 | } 171 | else 172 | { 173 | setError(`Unable to save blog.`); 174 | } 175 | } 176 | catch (error) 177 | { 178 | setError(error.message); 179 | } 180 | finally 181 | { 182 | setSaving(false); 183 | } 184 | } 185 | 186 | if (loading) return ; 187 | 188 | return ( 189 | 190 | 191 |
196 | 197 | 198 |
199 | 200 | 201 | { 209 | setTitle(event.target.value); 210 | }} 211 | /> 212 | 213 | 214 | 215 | { 223 | setPicture(event.target.value); 224 | }} 225 | /> 226 | 227 | 228 | 229 | { 237 | setHeadline(event.target.value); 238 | }} 239 | /> 240 | 241 | 242 | 243 | { 248 | setEditorState(newState); 249 | setContent(draftToHtml(convertToRaw(newState.getCurrentContent()))); 250 | }} 251 | toolbar={{ 252 | options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'history', 'embedded', 'emoji', 'image'], 253 | inline: { inDropdown: true }, 254 | list: { inDropdown: true }, 255 | textAlign: { inDropdown: true }, 256 | link: { inDropdown: true }, 257 | history: { inDropdown: true }, 258 | }} 259 | /> 260 | 261 | 262 | 263 | 264 | 265 | 286 | {_id !== '' && 287 | 290 | } 291 | 292 | 293 | 294 |
295 |
300 |
301 | 302 | 303 | 304 | 305 | 306 | ) 307 | } 308 | 309 | export default withRouter(EditPage); -------------------------------------------------------------------------------- /client/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { Container } from 'reactstrap'; 5 | import BlogPreview from '../components/BlogPreview'; 6 | import ErrorText from '../components/ErrorText'; 7 | import Header from '../components/Header'; 8 | import LoadingComponent from '../components/LoadingComponent'; 9 | import Navigation from '../components/Navigation'; 10 | import config from '../config/config'; 11 | import IBlog from '../interfaces/blog'; 12 | import IUser from '../interfaces/user'; 13 | 14 | const HomePage: React.FunctionComponent<{}> = props => { 15 | const [blogs, setBlogs] = useState([]); 16 | const [loading, setLoading] = useState(true) 17 | const [error, setError] = useState(''); 18 | 19 | useEffect(() => { 20 | getAllBlogs(); 21 | }, []); 22 | 23 | const getAllBlogs = async () => { 24 | try 25 | { 26 | const response = await axios({ 27 | method: 'GET', 28 | url: `${config.server.url}/blogs`, 29 | }); 30 | 31 | if (response.status === (200 || 304)) 32 | { 33 | let blogs = response.data.blogs as IBlog[]; 34 | blogs.sort((x,y) => y.updatedAt.localeCompare(x.updatedAt)); 35 | 36 | setBlogs(blogs); 37 | } 38 | else 39 | { 40 | setError('Unable to retrieve blogs'); 41 | } 42 | } 43 | catch (error) 44 | { 45 | setError(error.message); 46 | } 47 | finally 48 | { 49 | setTimeout(() => { 50 | setLoading(false) 51 | }, 500) 52 | } 53 | } 54 | 55 | if (loading) 56 | { 57 | return Loading blogs... 58 | } 59 | 60 | return ( 61 | 62 | 63 |
67 | 68 | {blogs.length === 0 &&

There are no blogs yet. You should post one 😊.

} 69 | {blogs.map((blog, index) => { 70 | return ( 71 |
72 | 80 |
81 |
82 | ); 83 | })} 84 | 85 |
86 | 87 | ) 88 | } 89 | 90 | export default HomePage; -------------------------------------------------------------------------------- /client/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { Button, Card, CardBody, CardHeader } from 'reactstrap'; 4 | import ErrorText from '../components/ErrorText'; 5 | import { Providers } from '../config/firebase'; 6 | import logging from '../config/logging'; 7 | import firebase from 'firebase'; 8 | import { Authenticate, SignInWithSocialMedia } from '../modules/Auth'; 9 | import CenterPiece from '../components/CenterPiece'; 10 | import LoadingComponent from '../components/LoadingComponent'; 11 | import UserContext from '../contexts/user'; 12 | 13 | const LoginPage: React.FunctionComponent<{}> = props => { 14 | const [authenticating, setAuthenticating] = useState(false); 15 | const [error, setError] = useState(''); 16 | 17 | const userContext = useContext(UserContext) 18 | const history = useHistory(); 19 | const isLogin = window.location.pathname.includes('login'); 20 | 21 | const signInWithSocialMedia = (provider: firebase.auth.AuthProvider) => { 22 | if (error !== '') setError(''); 23 | 24 | setAuthenticating(true); 25 | 26 | SignInWithSocialMedia(provider) 27 | .then(async (result) => { 28 | logging.info(result); 29 | 30 | let user = result.user; 31 | 32 | if (user) 33 | { 34 | let uid = user.uid; 35 | let name = user.displayName; 36 | 37 | if (name) 38 | { 39 | try 40 | { 41 | let fire_token = await user.getIdToken(); 42 | 43 | Authenticate(uid, name, fire_token, (error, _user) => { 44 | if (error) 45 | { 46 | setError(error); 47 | setAuthenticating(false); 48 | } 49 | else if (_user) 50 | { 51 | userContext.userDispatch({ type: 'login', payload: { user: _user, fire_token } }) 52 | history.push('/'); 53 | } 54 | }); 55 | } 56 | catch (error) 57 | { 58 | setError('Invalid token.'); 59 | logging.error(error); 60 | setAuthenticating(false); 61 | } 62 | } 63 | else 64 | { 65 | /** 66 | * We can set these manually with a new form 67 | * For example, the Twitter provider sometimes 68 | * does not provide a username as some users sign 69 | * up with a phone number. Here you could ask 70 | * them to provide a name that would be displayed 71 | * on this website. 72 | * */ 73 | setError('The identify provider is missing a display name.') 74 | setAuthenticating(false); 75 | } 76 | 77 | } 78 | else 79 | { 80 | setError('The social media provider does not have enough information. Please try a different provider.') 81 | setAuthenticating(false); 82 | } 83 | }) 84 | .catch(error => { 85 | logging.error(error); 86 | setAuthenticating(false); 87 | setError(error.message); 88 | }); 89 | } 90 | 91 | return ( 92 | 93 | 94 | 95 | {isLogin ? 'Login' : 'Sign Up'} 96 | 97 | 98 | 99 | 107 | {authenticating && } 108 | 109 | 110 | 111 | ); 112 | } 113 | 114 | export default LoginPage; -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /client/src/setupTests.ts: -------------------------------------------------------------------------------- 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'; 6 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 200, 4 | "proseWrap": "always", 5 | "tabWidth": 4, 6 | "useTabs": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "semi": true 11 | } -------------------------------------------------------------------------------- /server/src/config/logging.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_NAMESPACE = 'Server'; 2 | 3 | const info = (message: any, namespace?: string) => { 4 | if (typeof message === 'string') { 5 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [INFO] ${message}`); 6 | } else { 7 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [INFO]`, message); 8 | } 9 | }; 10 | 11 | const warn = (message: any, namespace?: string) => { 12 | if (typeof message === 'string') { 13 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [WARN] ${message}`); 14 | } else { 15 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [WARN]`, message); 16 | } 17 | }; 18 | 19 | const error = (message: any, namespace?: string) => { 20 | if (typeof message === 'string') { 21 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [ERROR] ${message}`); 22 | } else { 23 | console.log(`[${getDate()}] [${namespace || DEFAULT_NAMESPACE}] [ERROR]`, message); 24 | } 25 | }; 26 | 27 | const getDate = () => { 28 | return new Date().toISOString(); 29 | }; 30 | 31 | const logging = { info, warn, error }; 32 | 33 | export default logging; -------------------------------------------------------------------------------- /server/src/controllers/blog.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import logging from '../config/logging'; 3 | import Blog from '../models/blog'; 4 | import mongoose from 'mongoose'; 5 | 6 | const create = (req: Request, res: Response, next: NextFunction) => { 7 | logging.info('Attempting to create blog ...'); 8 | 9 | let { author, title, content, headline, picture } = req.body; 10 | 11 | const blog = new Blog({ 12 | _id: new mongoose.Types.ObjectId(), 13 | author, 14 | title, 15 | content, 16 | headline, 17 | picture 18 | }); 19 | 20 | return blog 21 | .save() 22 | .then((newBlog) => { 23 | logging.info(`New blog created`); 24 | 25 | return res.status(201).json({ blog: newBlog }); 26 | }) 27 | .catch((error) => { 28 | logging.error(error.message); 29 | 30 | return res.status(500).json({ 31 | message: error.message 32 | }); 33 | }); 34 | }; 35 | 36 | const read = (req: Request, res: Response, next: NextFunction) => { 37 | const _id = req.params.blogID; 38 | logging.info(`Incoming read for blog with id ${_id}`); 39 | 40 | Blog.findById(_id) 41 | .populate('author') 42 | .exec() 43 | .then((blog) => { 44 | if (blog) { 45 | return res.status(200).json({ blog }); 46 | } else { 47 | return res.status(404).json({ 48 | error: 'Blog not found.' 49 | }); 50 | } 51 | }) 52 | .catch((error) => { 53 | logging.error(error.message); 54 | 55 | return res.status(500).json({ 56 | error: error.message 57 | }); 58 | }); 59 | }; 60 | 61 | const readAll = (req: Request, res: Response, next: NextFunction) => { 62 | logging.info('Returning all blogs '); 63 | 64 | Blog.find() 65 | .populate('author') 66 | .exec() 67 | .then((blogs) => { 68 | return res.status(200).json({ 69 | count: blogs.length, 70 | blogs: blogs 71 | }); 72 | }) 73 | .catch((error) => { 74 | logging.error(error.message); 75 | 76 | return res.status(500).json({ 77 | message: error.message 78 | }); 79 | }); 80 | }; 81 | 82 | const query = (req: Request, res: Response, next: NextFunction) => { 83 | logging.info('Query route called'); 84 | 85 | Blog.find(req.body) 86 | .populate('author') 87 | .exec() 88 | .then((blogs) => { 89 | return res.status(200).json({ 90 | count: blogs.length, 91 | blogs: blogs 92 | }); 93 | }) 94 | .catch((error) => { 95 | logging.error(error.message); 96 | 97 | return res.status(500).json({ 98 | message: error.message 99 | }); 100 | }); 101 | }; 102 | 103 | const update = (req: Request, res: Response, next: NextFunction) => { 104 | logging.info('Update route called'); 105 | 106 | const _id = req.params.blogID; 107 | 108 | Blog.findById(_id) 109 | .exec() 110 | .then((blog) => { 111 | if (blog) { 112 | blog.set(req.body); 113 | blog.save() 114 | .then((savedBlog) => { 115 | logging.info(`Blog with id ${_id} updated`); 116 | 117 | return res.status(201).json({ 118 | blog: savedBlog 119 | }); 120 | }) 121 | .catch((error) => { 122 | logging.error(error.message); 123 | 124 | return res.status(500).json({ 125 | message: error.message 126 | }); 127 | }); 128 | } else { 129 | return res.status(401).json({ 130 | message: 'NOT FOUND' 131 | }); 132 | } 133 | }) 134 | .catch((error) => { 135 | logging.error(error.message); 136 | 137 | return res.status(500).json({ 138 | message: error.message 139 | }); 140 | }); 141 | }; 142 | 143 | const deleteBlog = (req: Request, res: Response, next: NextFunction) => { 144 | logging.warn('Delete route called'); 145 | 146 | const _id = req.params.blogID; 147 | 148 | Blog.findByIdAndDelete(_id) 149 | .exec() 150 | .then(() => { 151 | return res.status(201).json({ 152 | message: 'Blog deleted' 153 | }); 154 | }) 155 | .catch((error) => { 156 | logging.error(error.message); 157 | 158 | return res.status(500).json({ 159 | message: error.message 160 | }); 161 | }); 162 | }; 163 | 164 | export default { 165 | create, 166 | read, 167 | readAll, 168 | query, 169 | update, 170 | deleteBlog 171 | }; 172 | -------------------------------------------------------------------------------- /server/src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import logging from '../config/logging'; 3 | import User from '../models/user'; 4 | import mongoose from 'mongoose'; 5 | 6 | const validate = (req: Request, res: Response, next: NextFunction) => { 7 | logging.info('Token validated, ensuring user.'); 8 | 9 | let firebase = res.locals.firebase; 10 | 11 | return User.findOne({ uid: firebase.uid }) 12 | .then((user) => { 13 | if (user) { 14 | return res.status(200).json({ user }); 15 | } else { 16 | return res.status(401).json({ 17 | message: 'Token(s) invalid, user not found' 18 | }); 19 | } 20 | }) 21 | .catch((error) => { 22 | return res.status(500).json({ 23 | message: error.message, 24 | error 25 | }); 26 | }); 27 | }; 28 | 29 | const create = (req: Request, res: Response, next: NextFunction) => { 30 | logging.info('Attempting to register user ...'); 31 | 32 | let { uid, name } = req.body; 33 | let fire_token = res.locals.fire_token; 34 | 35 | const user = new User({ 36 | _id: new mongoose.Types.ObjectId(), 37 | uid, 38 | name 39 | }); 40 | 41 | return user 42 | .save() 43 | .then((newUser) => { 44 | logging.info(`New user ${uid} created`); 45 | 46 | return res.status(200).json({ user: newUser, fire_token }); 47 | }) 48 | .catch((error) => { 49 | logging.error(error.message); 50 | 51 | return res.status(500).json({ 52 | message: error.message 53 | }); 54 | }); 55 | }; 56 | 57 | const login = (req: Request, res: Response, next: NextFunction) => { 58 | logging.info('Verifying user'); 59 | 60 | let { uid } = req.body; 61 | let fire_token = res.locals.fire_token; 62 | 63 | return User.findOne({ uid }) 64 | .then((user) => { 65 | if (user) { 66 | logging.info(`User ${uid} found, attempting to sign token and return user ...`); 67 | return res.status(200).json({ user, fire_token }); 68 | } else { 69 | logging.warn(`User ${uid} not in the DB, attempting to register ...`); 70 | return create(req, res, next); 71 | } 72 | }) 73 | .catch((error) => { 74 | logging.error(error.message); 75 | return res.status(500).json({ 76 | message: error.message 77 | }); 78 | }); 79 | }; 80 | 81 | const read = (req: Request, res: Response, next: NextFunction) => { 82 | const _id = req.params.userID; 83 | logging.info(`Incoming read for user with id ${_id}`); 84 | 85 | User.findById(_id) 86 | .exec() 87 | .then((user) => { 88 | if (user) { 89 | return res.status(200).json({ 90 | user: user 91 | }); 92 | } else { 93 | return res.status(404).json({ 94 | error: 'User not found.' 95 | }); 96 | } 97 | }) 98 | .catch((error) => { 99 | logging.error(error.message); 100 | 101 | return res.status(500).json({ 102 | error: error.message 103 | }); 104 | }); 105 | }; 106 | 107 | const readAll = (req: Request, res: Response, next: NextFunction) => { 108 | logging.info('Readall route called'); 109 | 110 | User.find() 111 | .exec() 112 | .then((users) => { 113 | return res.status(200).json({ 114 | count: users.length, 115 | users: users 116 | }); 117 | }) 118 | .catch((error) => { 119 | logging.error(error.message); 120 | 121 | return res.status(500).json({ 122 | message: error.message 123 | }); 124 | }); 125 | }; 126 | 127 | export default { 128 | validate, 129 | create, 130 | login, 131 | read, 132 | readAll 133 | }; 134 | -------------------------------------------------------------------------------- /server/src/interfaces/blog.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | import IUser from './user'; 3 | 4 | export default interface IBlog extends Document { 5 | title: string; 6 | author: IUser; 7 | content: string; 8 | headline: string; 9 | picture?: string; 10 | } 11 | -------------------------------------------------------------------------------- /server/src/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export default interface IUser extends Document { 4 | uid: string; 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /server/src/middleware/extractFirebaseInfo.ts: -------------------------------------------------------------------------------- 1 | import logging from '../config/logging'; 2 | import firebaseAdmin from 'firebase-admin'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | const extractFirebaseInfo = (req: Request, res: Response, next: NextFunction) => { 6 | logging.info('Validating firebase token'); 7 | 8 | let token = req.headers.authorization?.split(' ')[1]; 9 | 10 | if (token) { 11 | firebaseAdmin 12 | .auth() 13 | .verifyIdToken(token) 14 | .then((result) => { 15 | if (result) { 16 | res.locals.firebase = result; 17 | res.locals.fire_token = token; 18 | next(); 19 | } else { 20 | logging.warn('Token invalid, Unauthorized'); 21 | 22 | return res.status(401).json({ 23 | message: 'Unauthorized' 24 | }); 25 | } 26 | }) 27 | .catch((error) => { 28 | logging.error(error); 29 | 30 | return res.status(401).json({ 31 | error, 32 | message: 'Unauthorized' 33 | }); 34 | }); 35 | } else { 36 | return res.status(401).json({ 37 | message: 'Unauthorized' 38 | }); 39 | } 40 | }; 41 | 42 | export default extractFirebaseInfo; 43 | -------------------------------------------------------------------------------- /server/src/models/blog.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import IBlog from '../interfaces/blog'; 3 | 4 | const BlogSchema: Schema = new Schema( 5 | { 6 | title: { type: String, unique: true }, 7 | author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 8 | content: { type: String, unique: true }, 9 | headline: { type: String, unique: true }, 10 | picture: { type: String } 11 | }, 12 | { 13 | timestamps: true 14 | } 15 | ); 16 | 17 | export default mongoose.model('Blog', BlogSchema); 18 | -------------------------------------------------------------------------------- /server/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import IUser from '../interfaces/user'; 3 | 4 | const UserSchema: Schema = new Schema({ 5 | uid: { type: String, unique: true }, 6 | name: { type: String } 7 | }); 8 | 9 | export default mongoose.model('User', UserSchema); 10 | -------------------------------------------------------------------------------- /server/src/routes/blog.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import controller from '../controllers/blog'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', controller.readAll); 7 | router.get('/read/:blogID', controller.read); 8 | router.post('/create', controller.create); 9 | router.post('/query', controller.query); 10 | router.patch('/update/:blogID', controller.update); 11 | router.delete('/:blogID', controller.deleteBlog); 12 | 13 | export = router; 14 | -------------------------------------------------------------------------------- /server/src/routes/user.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import controller from '../controllers/user'; 3 | import extractFirebaseInfo from '../middleware/extractFirebaseInfo'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/validate', extractFirebaseInfo, controller.validate); 8 | router.get('/:userID', controller.read); 9 | router.post('/create', controller.create); 10 | router.post('/login', controller.login); 11 | router.get('/', controller.readAll); 12 | 13 | export = router; -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import express from 'express'; 3 | import logging from './config/logging'; 4 | import config from './config/config'; 5 | import mongoose from 'mongoose'; 6 | import firebaseAdmin from 'firebase-admin'; 7 | 8 | import userRoutes from './routes/user'; 9 | import blogRoutes from './routes/blog'; 10 | 11 | const router = express(); 12 | 13 | /** Server Handling */ 14 | const httpServer = http.createServer(router); 15 | 16 | /** Connect to Firebase */ 17 | let serviceAccount = require('./config/serviceAccountKey.json'); 18 | 19 | firebaseAdmin.initializeApp({ 20 | credential: firebaseAdmin.credential.cert(serviceAccount) 21 | }); 22 | 23 | /** Connect to Mongo */ 24 | mongoose 25 | .connect(config.mongo.url, config.mongo.options) 26 | .then((result) => { 27 | logging.info('Mongo Connected'); 28 | }) 29 | .catch((error) => { 30 | logging.error(error); 31 | }); 32 | 33 | /** Log the request */ 34 | router.use((req, res, next) => { 35 | logging.info(`METHOD: [${req.method}] - URL: [${req.url}] - IP: [${req.socket.remoteAddress}]`); 36 | 37 | res.on('finish', () => { 38 | logging.info(`METHOD: [${req.method}] - URL: [${req.url}] - STATUS: [${res.statusCode}] - IP: [${req.socket.remoteAddress}]`); 39 | }); 40 | 41 | next(); 42 | }); 43 | 44 | /** Parse the body of the request */ 45 | router.use(express.urlencoded({ extended: true })); 46 | router.use(express.json()); 47 | 48 | /** Rules of our API */ 49 | router.use((req, res, next) => { 50 | res.header('Access-Control-Allow-Origin', '*'); 51 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 52 | 53 | if (req.method == 'OPTIONS') { 54 | res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET'); 55 | return res.status(200).json({}); 56 | } 57 | 58 | next(); 59 | }); 60 | 61 | /** Routes */ 62 | router.use('/users', userRoutes); 63 | router.use('/blogs', blogRoutes); 64 | 65 | /** Error handling */ 66 | router.use((req, res, next) => { 67 | const error = new Error('Not found'); 68 | 69 | res.status(404).json({ 70 | message: error.message 71 | }); 72 | }); 73 | 74 | /** Listen */ 75 | httpServer.listen(config.server.port, () => logging.info(`Server is running ${config.server.host}:${config.server.port}`)); --------------------------------------------------------------------------------