├── .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 | You need to enable JavaScript to run this app, nerd.
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 |
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 |
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 |
24 |
25 | Post a Blog
26 |
27 | |
28 | logout()}>
29 | Logout
30 |
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 | deleteBlog()}>Delete Permanently
127 | setModal(false)}>Cancel
128 |
129 |
130 |
137 |
138 | {user._id === (blog.author as IUser)._id &&
139 |
140 | Edit
141 | setModal(true)}> Delete
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 |
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 | signInWithSocialMedia(Providers.google)}
103 | style={{ backgroundColor:'#ea4335', borderColor: '#ea4335'}}
104 | >
105 | Sign {isLogin ? 'in' : 'up'} with Google
106 |
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}`));
--------------------------------------------------------------------------------