();
33 | const { addToast } = useToasts();
34 | const [isLoadingAppState, setIsLoadingAppState] = useState(true);
35 | const [ranInitialUserCheck, setRanInitialUserCheck] = useState(false);
36 |
37 | // Sometimes your app with need to run some actions on page refresh.
38 | // Right now we're going to load the base notes in the dashboard instead
39 | // const loadProgramData = async () => {};
40 |
41 | // Check for a possible user...
42 | const checkUserStatus = async () => {
43 | try {
44 | const accessToken = localStorage.getItem("accessToken");
45 | const refreshToken = localStorage.getItem("refreshToken");
46 |
47 | if (accessToken != null && refreshToken != null) {
48 | const decoded: JwtDecodeData = jwtDecode(accessToken);
49 | dispatch(setUser(decoded.data));
50 | }
51 | return Promise.resolve();
52 | } catch (error) {
53 | addToast(
54 | "Hmm, there was an error retrieving your data. Please refresh the page and try again.",
55 | {
56 | appearance: "error",
57 | },
58 | );
59 | } finally {
60 | setIsLoadingAppState(false);
61 | }
62 | };
63 |
64 | // Two important things here, attach our axios interceptor and check for user
65 | useEffect(() => {
66 | runAxiosAuthInterceptor();
67 | checkUserStatus();
68 | }, []); // eslint-disable-line react-hooks/exhaustive-deps
69 |
70 | useEffect(() => {
71 | // If we haven't checked for the user yet, and the app is done loading,
72 | // then check for the user and run loadProgramData if needed.
73 | if (!ranInitialUserCheck && !isLoadingAppState && user.id) {
74 | // loadProgramData();
75 | setRanInitialUserCheck(true);
76 | }
77 | }, [user]); // eslint-disable-line react-hooks/exhaustive-deps
78 |
79 | // Array of paths where we don't want to show the nav and footer
80 | const metaPaths = ["/maintenance"];
81 |
82 | // The logic here is that if we are loading the initial app state and
83 | // haven't checked for a user yet, let's just show a blank page. Otherwise,
84 | // show the home page, but take care to only allow traveling to user links
85 | // when a user is logged in.
86 | return isLoadingAppState && !ranInitialUserCheck ? (
87 |
88 | ) : (
89 |
90 |
91 |
92 | {!metaPaths.includes(window.location.pathname) &&
}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | {user.id ? : }
110 |
111 |
112 | {user.id ? : }
113 |
114 |
115 | {user.id ? : }
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | {!metaPaths.includes(window.location.pathname) &&
}
132 |
133 |
134 |
135 | );
136 | };
137 |
--------------------------------------------------------------------------------
/src/__tests__/Home.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import { Provider } from "react-redux";
4 | import { ToastProvider } from "react-toast-notifications";
5 | import { configureStore } from "@/store";
6 | import { App } from "@/App";
7 |
8 | test("Loads App", () => {
9 | const { getByText } = render(
10 |
11 |
12 |
13 |
14 | ,
15 | );
16 |
17 | expect(getByText(/Home/i)).toBeInTheDocument();
18 | });
19 |
--------------------------------------------------------------------------------
/src/__tests__/User.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import { Provider } from "react-redux";
4 | import { ToastProvider } from "react-toast-notifications";
5 | import { configureStore } from "@/store";
6 | import { Login } from "@/components/user/components/Login";
7 | import { Signup } from "@/components/user/components/Signup";
8 | import { Forgot } from "@/components/user/components/Forgot";
9 | import { Reset } from "@/components/user/components/Reset";
10 | import { MemoryRouter } from "react-router-dom";
11 |
12 | test("Loads Login", () => {
13 | const { getByLabelText } = render(
14 |
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | );
22 |
23 | expect(getByLabelText(/Username/i)).toBeInTheDocument();
24 | expect(getByLabelText(/Password/i)).toBeInTheDocument();
25 | });
26 |
27 | test("Loads Signup", () => {
28 | const { getByLabelText } = render(
29 |
30 |
31 |
32 |
33 |
34 |
35 | ,
36 | );
37 |
38 | expect(getByLabelText(/First Name/i)).toBeInTheDocument();
39 | expect(getByLabelText(/Last Name/i)).toBeInTheDocument();
40 | expect(getByLabelText(/Username/i)).toBeInTheDocument();
41 | expect(getByLabelText(/Email/i)).toBeInTheDocument();
42 | expect(getByLabelText(/Confirm Password/i)).toBeInTheDocument();
43 | });
44 |
45 | test("Loads Forgot", () => {
46 | const { getByLabelText } = render(
47 |
48 |
49 |
50 |
51 |
52 |
53 | ,
54 | );
55 |
56 | expect(getByLabelText(/Email/i)).toBeInTheDocument();
57 | });
58 |
59 | test("Loads Reset", () => {
60 | const { getByLabelText } = render(
61 |
62 |
63 |
64 |
65 |
66 |
67 | ,
68 | );
69 |
70 | expect(getByLabelText(/Confirm Password/i)).toBeInTheDocument();
71 | });
72 |
--------------------------------------------------------------------------------
/src/assets/css/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/css/.gitkeep
--------------------------------------------------------------------------------
/src/assets/css/app.scss:
--------------------------------------------------------------------------------
1 | // Sanitize
2 | @import "~sanitize.css";
3 | @import "~sanitize.css/forms";
4 | @import "~sanitize.css/typography";
5 |
6 | // Functions
7 | @import "functions/color";
8 |
9 | // Variables - Has to be early
10 | @import "components/variables";
11 |
12 | // Bootstrap
13 | @import "~bootstrap/scss/bootstrap";
14 |
15 | // Components
16 | @import "components/bootstrap_addition";
17 | @import "components/pattern_backgrounds";
18 | @import "components/modal";
19 | @import "components/fonts";
20 | @import "components/form";
21 | @import "components/buttons";
22 | @import "components/hr";
23 | @import "components/animations";
24 | @import "components/blockquote";
25 | @import "components/scrollbox";
26 | @import "components/user";
27 | @import "components/main";
28 | @import "components/vendor";
29 | @import "components/program";
30 |
--------------------------------------------------------------------------------
/src/assets/css/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/css/components/.gitkeep
--------------------------------------------------------------------------------
/src/assets/css/components/_animations.scss:
--------------------------------------------------------------------------------
1 | // shake
2 | @keyframes shake {
3 | 0%,
4 | 100% {
5 | -webkit-transform: translateY(0);
6 | transform: translateY(0);
7 | }
8 |
9 | 20% {
10 | -webkit-transform: translateY(4px);
11 | transform: translateY(4px);
12 | }
13 |
14 | 40% {
15 | -webkit-transform: translateY(-4px);
16 | transform: translateY(-4px);
17 | }
18 |
19 | 60% {
20 | -webkit-transform: translateY(2px);
21 | transform: translateY(2px);
22 | }
23 |
24 | 80% {
25 | -webkit-transform: translateY(-2px);
26 | transform: translateY(-2px);
27 | }
28 | }
29 |
30 | // slide-fade
31 | // Note the enter and and leave-to look better like this,
32 | // and for some reason `duration` is needed on the transition component
33 | .slide-fade-enter-active,
34 | .slide-fade-leave-active {
35 | transition: transform 0.2s ease, opacity 0.2s ease;
36 | }
37 | .slide-fade-enter {
38 | opacity: 0;
39 | transform: translateY(-5px);
40 | }
41 | .slide-fade-leave-to {
42 | opacity: 0;
43 | transform: translateY(5px);
44 | }
45 |
46 | .quick-fade-enter-active,
47 | .quick-fade-leave-active {
48 | transition: opacity 0.2s cubic-bezier(0, 0, 0.58, 1);
49 | }
50 | .quick-fade-enter {
51 | opacity: 0;
52 | }
53 | .quick-fade-leave-to {
54 | opacity: 0;
55 | }
56 |
57 | // Pulsating effect
58 | .pulsating {
59 | animation: pulse 1.35s infinite cubic-bezier(0.66, 0, 0, 1);
60 | }
61 | .pulsating:hover {
62 | animation: none;
63 | }
64 | @keyframes pulse {
65 | 0% {
66 | box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.45);
67 | }
68 | 100% {
69 | box-shadow: 0 0 0 25px rgba(0, 0, 0, 0);
70 | }
71 | }
72 |
73 | // Spin
74 | .spin {
75 | animation: spin 2s infinite linear;
76 | }
77 | @keyframes spin {
78 | 0% {
79 | transform: rotate(0deg);
80 | }
81 | 100% {
82 | transform: rotate(359deg);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/assets/css/components/_blockquote.scss:
--------------------------------------------------------------------------------
1 | $blockquote-background: #ededed;
2 | $blockquote-grey: #555;
3 |
4 | blockquote {
5 | background: $blockquote-background;
6 | border-left: 8px solid color(blue);
7 | color: $blockquote-grey;
8 | font-family: var(--font-family-base);
9 | font-size: 1.1rem;
10 | font-style: italic;
11 | line-height: 1.4;
12 | padding: 1.2rem 30px 1.2rem 35px;
13 | position: relative;
14 | width: 100%;
15 | }
16 |
17 | @media (min-width: 992px) {
18 | blockquote {
19 | width: 100%;
20 | }
21 | }
22 |
23 | blockquote:after {
24 | content: "";
25 | }
26 |
--------------------------------------------------------------------------------
/src/assets/css/components/_bootstrap_addition.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable color-no-hex, declaration-no-important */
2 |
3 | // Button mixin - have to hold this here because bootstrap has changed the way
4 | // to set the button text color of your choice. It's been changed to
5 | // automatically detect button text color by contrast - but it is actually
6 | // getting in the way.
7 | // https://github.com/twbs/bootstrap/blame/v4-dev/scss/mixins/_buttons.scss.
8 | //
9 | // Also, I'm using important here because react-bootstrap is forcing me to
10 | // use a variant prop, which defaults to btn-primary. Will follow up on this
11 | // github issue in the future to see if we can avoid this:
12 | // https://github.com/react-bootstrap/react-bootstrap/issues/5047
13 | @mixin button-variant-custom(
14 | $color,
15 | $background,
16 | $border,
17 | $hover-color: darken($color, 5%),
18 | $hover-background: darken($background, 5%),
19 | $hover-border: darken($border, 5%),
20 | $active-color: $hover-color,
21 | $active-background: darken($background, 10%),
22 | $active-border: darken($border, 10%)
23 | ) {
24 | background-color: $background !important;
25 | border-color: $border !important;
26 | color: $color !important;
27 | @include box-shadow($btn-box-shadow);
28 |
29 | &:hover {
30 | background-color: $hover-background !important;
31 | border-color: $hover-border !important;
32 | color: $hover-color !important;
33 | }
34 |
35 | &:focus,
36 | &.focus {
37 | // Avoid using mixin so we can pass custom focus shadow properly
38 | @if $enable-shadows {
39 | box-shadow: $btn-box-shadow, 0 0 0 3px rgba($border, 0.5);
40 | } @else {
41 | box-shadow: 0 0 0 3px rgba($border, 0.5);
42 | }
43 | }
44 |
45 | // Disabled comes first so active can properly restyle
46 | &.disabled,
47 | &:disabled {
48 | background-color: $background !important;
49 | border-color: $border !important;
50 | }
51 |
52 | &:active,
53 | &.active,
54 | .show > &.dropdown-toggle {
55 | background-color: $active-background !important;
56 | background-image: none !important; // Remove the gradient for the pressed/active state
57 | border-color: $active-border !important;
58 | color: $active-color !important;
59 | @include box-shadow($btn-active-box-shadow);
60 | }
61 | }
62 |
63 | // Buttons
64 | .btn-white {
65 | @include button-variant-custom($font-color, $white, $white);
66 | }
67 |
68 | .btn-grey {
69 | @include button-variant-custom($font-color, $gray-500, darken($gray-500, 5%));
70 | }
71 |
72 | .btn-white-black-text {
73 | @include button-variant-custom(lighten($black, 14%), $white, $white);
74 | }
75 |
76 | .btn-white-blue-text {
77 | @include button-variant-custom($blue, $white, $white);
78 | }
79 |
80 | .btn-white-text {
81 | @include button-variant-custom($white, transparent, transparent, rgba(0, 0, 0, 0.1));
82 | }
83 |
84 | .btn-red {
85 | @include button-variant-custom($white, $red, darken($red, 2%));
86 | }
87 |
88 | .btn-dark-red {
89 | @include button-variant-custom($white, darken($red, 10%), darken($red, 10%));
90 | }
91 |
92 | .btn-orange {
93 | @include button-variant-custom($white, $orange, darken($orange, 2%));
94 | }
95 |
96 | .btn-dark-orange {
97 | @include button-variant-custom($white, darken($orange, 10%), darken($orange, 10%));
98 | }
99 |
100 | .btn-purple {
101 | @include button-variant-custom($white, $purple, darken($purple, 2%));
102 | }
103 |
104 | .btn-yellow {
105 | @include button-variant-custom($black, $yellow, darken($yellow, 2%));
106 | }
107 |
108 | .btn-green {
109 | @include button-variant-custom($white, $green, darken($green, 2%));
110 | }
111 |
112 | .btn-dark-green {
113 | @include button-variant-custom($white, darken($green, 10%), darken($green, 10%));
114 | }
115 |
116 | .btn-blue {
117 | @include button-variant-custom($white, $blue, darken($blue, 2%));
118 | }
119 |
120 | .btn-dark-blue {
121 | @include button-variant-custom($white, darken($blue, 10%), darken($blue, 10%));
122 | }
123 |
124 | .btn-custom-navbar-dropdown-white {
125 | @include button-variant-custom($white, transparent, transparent);
126 | }
127 |
128 | // Button outlines - btn-outline
129 | .btn-outline-blue {
130 | @include button-variant-custom(
131 | $blue,
132 | rgba($blue, 0.1),
133 | rgba($blue, 0.4),
134 | $blue,
135 | rgba($blue, 0.1),
136 | $blue,
137 | $blue,
138 | rgba($blue, 0.2),
139 | $blue
140 | );
141 | }
142 |
143 | .btn-outline-red {
144 | @include button-variant-custom(
145 | $red,
146 | rgba($red, 0.1),
147 | rgba($red, 0.4),
148 | $red,
149 | rgba($red, 0.1),
150 | $red,
151 | $red,
152 | rgba($red, 0.2),
153 | $red
154 | );
155 | }
156 |
157 | .btn-outline-green {
158 | @include button-variant-custom(
159 | $green,
160 | rgba($green, 0.1),
161 | rgba($green, 0.4),
162 | $green,
163 | rgba($green, 0.1),
164 | $green,
165 | $green,
166 | rgba($green, 0.2),
167 | $green
168 | );
169 | }
170 |
171 | .btn-outline-orange {
172 | @include button-variant-custom(
173 | $orange,
174 | rgba($orange, 0.1),
175 | rgba($orange, 0.4),
176 | $orange,
177 | rgba($orange, 0.1),
178 | $orange,
179 | $orange,
180 | rgba($orange, 0.2),
181 | $orange
182 | );
183 | }
184 |
185 | .btn-outline-yellow {
186 | @include button-variant-custom(
187 | $yellow,
188 | rgba($yellow, 0.1),
189 | rgba($yellow, 0.4),
190 | $yellow,
191 | rgba($yellow, 0.1),
192 | $yellow,
193 | $yellow,
194 | rgba($yellow, 0.2),
195 | $yellow
196 | );
197 | }
198 |
199 | .btn-outline-grey {
200 | @include button-variant-custom(
201 | rgba($black, 0.8),
202 | rgba($black, 0.06),
203 | rgba($black, 0.4),
204 | rgba($black, 0.8),
205 | rgba($black, 0.06),
206 | rgba($black, 0.8),
207 | rgba($black, 0.8),
208 | rgba($black, 0.1),
209 | $black
210 | );
211 | }
212 |
213 | // Badges
214 | .badge {
215 | border-radius: var(--border-radius);
216 | font-family: var(--font-family-base);
217 | font-size: 80%;
218 | font-weight: 400;
219 | letter-spacing: 0.75px;
220 | padding: 6px 6px;
221 |
222 | @media (min-width: 992px) {
223 | font-size: 90%;
224 | padding: 6px 12px;
225 | }
226 | }
227 |
228 | .badge-block {
229 | display: block;
230 | line-height: 1.6;
231 | -ms-overflow-style: -ms-autohiding-scrollbar;
232 | text-align: left;
233 | white-space: normal;
234 |
235 | &::-webkit-scrollbar {
236 | display: none;
237 | }
238 | }
239 |
240 | .badge-blue {
241 | @include badge-variant(rgba($blue, 0.1));
242 | border: 1px solid color(blue-400);
243 | color: colors(blue) !important;
244 | }
245 |
246 | .badge-red {
247 | @include badge-variant(rgba($red, 0.1));
248 | border: 1px solid color(red-400);
249 | color: colors(red) !important;
250 | }
251 |
252 | .badge-green {
253 | @include badge-variant(rgba($green, 0.1));
254 | border: 1px solid color(green-400);
255 | color: colors(green) !important;
256 | }
257 |
258 | .badge-orange {
259 | @include badge-variant(rgba($green, 0.1));
260 | border: 1px solid color(orange-400);
261 | color: colors(orange) !important;
262 | }
263 |
264 | .badge-light-grey {
265 | @include badge-variant($gray-300);
266 | border: 1px solid color(gray-700);
267 | color: colors(gray-500);
268 | }
269 |
270 | .badge-grey {
271 | @include badge-variant($gray-500);
272 | border: 1px solid color(gray-700);
273 | color: colors(gray-500);
274 | }
275 |
276 | .badge-yellow {
277 | @include badge-variant(rgba($yellow, 0.1));
278 | border: 1px solid color(yellow-500);
279 | color: $muted !important;
280 | }
281 |
282 | // Because badges can be a tags
283 | a {
284 | &.badge {
285 | &.badge-blue {
286 | &:hover {
287 | border-color: color(blue);
288 | }
289 | }
290 |
291 | &.badge-red {
292 | &:hover {
293 | border-color: color(red);
294 | }
295 | }
296 |
297 | &.badge-green {
298 | &:hover {
299 | border-color: color(green);
300 | }
301 | }
302 |
303 | &.badge-orange {
304 | &:hover {
305 | border-color: color(orange);
306 | }
307 | }
308 |
309 | &.badge-light-grey {
310 | &:hover {
311 | border-color: color(gray-300)
312 | }
313 | }
314 |
315 | &.badge-grey {
316 | &:hover {
317 | border-color: color(gray-500);
318 | }
319 | }
320 |
321 | &.badge-yellow {
322 | &:hover {
323 | border-color: color(yellow);
324 | }
325 | }
326 | }
327 | }
328 |
329 | //Font size
330 | .text-large {
331 | font-size: 1.2rem;
332 | font-weight: 400;
333 | }
334 |
335 | .text-extra-large {
336 | font-size: 1.4rem;
337 | font-weight: 400;
338 | }
339 |
340 | // Button size
341 | .btn-xs {
342 | @include button-size(5px, 5px, 0.8rem, 1, 4px);
343 | }
344 |
345 | // Text helpers
346 | .text-black {
347 | color: colors(black) !important;
348 | }
349 |
350 | .text-white {
351 | color: colors(white) !important;
352 | }
353 |
354 | .text-blue {
355 | color: colors(blue) !important;
356 | }
357 |
358 | .text-red {
359 | color: colors(red) !important;
360 | }
361 |
362 | .text-green {
363 | color: colors(green) !important;
364 | }
365 |
366 | .text-orange {
367 | color: colors(orange) !important;
368 | }
369 |
370 | .text-purple {
371 | color: colors(purple) !important;
372 | }
373 |
374 | .text-yellow {
375 | color: colors(yellow) !important;
376 | }
377 |
378 | .text-grey {
379 | color: colors(gray-500) !important;
380 | }
381 |
382 | .muted {
383 | color: colors(muted) !important;
384 | }
385 |
386 | .list-group-item-off-white {
387 | background: color(off-white);
388 | }
389 |
390 | //For additional heading display
391 | $display5-weight: 300 !default;
392 | $display5-size: 2.5rem !default;
393 | .display-5 {
394 | font-size: $display5-size;
395 | font-weight: $display5-weight;
396 | line-height: $display-line-height;
397 | }
398 |
399 | // input-group button margin-bottom
400 | .input-group {
401 | .btn {
402 | margin-bottom: 0;
403 | }
404 | }
405 |
406 | // Add responsive sizing helpers
407 | // https://github.com/twbs/bootstrap/issues/21943
408 | @media (min-width: 576px) {
409 | .w-sm-25 {
410 | width: 25% !important;
411 | }
412 | .w-sm-50 {
413 | width: 50% !important;
414 | }
415 | .w-sm-75 {
416 | width: 75% !important;
417 | }
418 | .w-sm-100 {
419 | width: 100% !important;
420 | }
421 | .w-sm-auto {
422 | width: auto !important;
423 | }
424 | .h-sm-25 {
425 | height: 25% !important;
426 | }
427 | .h-sm-50 {
428 | height: 50% !important;
429 | }
430 | .h-sm-75 {
431 | height: 75% !important;
432 | }
433 | .h-sm-100 {
434 | height: 100% !important;
435 | }
436 | .h-sm-auto {
437 | height: auto !important;
438 | }
439 | }
440 |
441 | @media (min-width: 768px) {
442 | .w-md-25 {
443 | width: 25% !important;
444 | }
445 | .w-md-50 {
446 | width: 50% !important;
447 | }
448 | .w-md-75 {
449 | width: 75% !important;
450 | }
451 | .w-md-100 {
452 | width: 100% !important;
453 | }
454 | .w-md-auto {
455 | width: auto !important;
456 | }
457 | .h-md-25 {
458 | height: 25% !important;
459 | }
460 | .h-md-50 {
461 | height: 50% !important;
462 | }
463 | .h-md-75 {
464 | height: 75% !important;
465 | }
466 | .h-md-100 {
467 | height: 100% !important;
468 | }
469 | .h-md-auto {
470 | height: auto !important;
471 | }
472 | }
473 |
474 | @media (min-width: 992px) {
475 | .w-lg-25 {
476 | width: 25% !important;
477 | }
478 | .w-lg-50 {
479 | width: 50% !important;
480 | }
481 | .w-lg-75 {
482 | width: 75% !important;
483 | }
484 | .w-lg-100 {
485 | width: 100% !important;
486 | }
487 | .w-lg-auto {
488 | width: auto !important;
489 | }
490 | .h-lg-25 {
491 | height: 25% !important;
492 | }
493 | .h-lg-50 {
494 | height: 50% !important;
495 | }
496 | .h-lg-75 {
497 | height: 75% !important;
498 | }
499 | .h-lg-100 {
500 | height: 100% !important;
501 | }
502 | .h-lg-auto {
503 | height: auto !important;
504 | }
505 | }
506 |
507 | @media (min-width: 1200px) {
508 | .w-xl-25 {
509 | width: 25% !important;
510 | }
511 | .w-xl-50 {
512 | width: 50% !important;
513 | }
514 | .w-xl-75 {
515 | width: 75% !important;
516 | }
517 | .w-xl-100 {
518 | width: 100% !important;
519 | }
520 | .w-xl-auto {
521 | width: auto !important;
522 | }
523 | .h-xl-25 {
524 | height: 25% !important;
525 | }
526 | .h-xl-50 {
527 | height: 50% !important;
528 | }
529 | .h-xl-75 {
530 | height: 75% !important;
531 | }
532 | .h-xl-100 {
533 | height: 100% !important;
534 | }
535 | .h-xl-auto {
536 | height: auto !important;
537 | }
538 | }
539 |
--------------------------------------------------------------------------------
/src/assets/css/components/_buttons.scss:
--------------------------------------------------------------------------------
1 | // Just .btn
2 | .btn {
3 | border-radius: var(--border-radius);
4 | }
5 |
6 | .btn,
7 | button {
8 | box-shadow: var(--box-shadow-300);
9 | font-weight: 500;
10 | letter-spacing: 1px;
11 | line-height: var(--line-height);
12 | text-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
13 | text-transform: none;
14 |
15 | &:hover {
16 | box-shadow: var(--box-shadow-400);
17 | cursor: pointer;
18 | }
19 |
20 | &:focus {
21 | outline: 1px dotted !important; /* stylelint-disable-line declaration-no-important */
22 | outline: 5px auto -webkit-focus-ring-color !important; /* stylelint-disable-line declaration-no-important */
23 | }
24 | }
25 |
26 | // Adjust global dropdown
27 | .dropdown-menu li a {
28 | color: colors(font-color);
29 | font-size: 18px;
30 | }
31 |
32 | // Adjust global list-group buttons
33 | .list-group {
34 | .btn,
35 | button {
36 | box-shadow: none;
37 | font-weight: 400;
38 | text-shadow: none;
39 |
40 | &:hover {
41 | box-shadow: none;
42 | }
43 | }
44 | }
45 |
46 | .btn-link {
47 | box-shadow: none;
48 | color: colors(grey-700);
49 | text-shadow: none;
50 |
51 | &:hover,
52 | &:active,
53 | &:focus {
54 | box-shadow: none;
55 | color: colors(grey-600);
56 | text-decoration: none;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/assets/css/components/_fonts.scss:
--------------------------------------------------------------------------------
1 | // Hosting fonts in cra
2 | // https://github.com/styled-components/styled-components/issues/1513#issuecomment-364782724
3 |
4 | @font-face {
5 | font-family: "Quicksand";
6 | font-weight: 300;
7 | src: url("/fonts/Quicksand/Quicksand-Light.ttf");
8 | }
9 |
10 | @font-face {
11 | font-family: "Quicksand";
12 | font-style: normal;
13 | font-weight: 400;
14 | src: url("/fonts/Quicksand/Quicksand-Regular.ttf");
15 | }
16 |
17 | @font-face {
18 | font-family: "Quicksand";
19 | font-weight: 500;
20 | src: url("/fonts/Quicksand/Quicksand-SemiBold.ttf");
21 | }
22 |
23 | @font-face {
24 | font-family: "Quicksand";
25 | font-weight: 700;
26 | src: url("/fonts/Quicksand/Quicksand-Bold.ttf");
27 | }
28 |
--------------------------------------------------------------------------------
/src/assets/css/components/_form.scss:
--------------------------------------------------------------------------------
1 | .input-wrapper {
2 | margin-bottom: 20px;
3 |
4 | &--no-margin-bottom {
5 | margin-bottom: 0;
6 | }
7 |
8 | &--no-margin-bottom-desktop {
9 | margin-bottom: 20px;
10 |
11 | @media (min-width: 992px) {
12 | margin-bottom: 0;
13 | }
14 | }
15 |
16 | &__label {
17 | color: colors(black);
18 | font-weight: 500;
19 |
20 | &--white {
21 | color: colors(white);
22 | }
23 | }
24 |
25 | &__error-message {
26 | color: colors(red);
27 | margin-top: 10px;
28 |
29 | &--white {
30 | color: colors(white);
31 | }
32 | }
33 | }
34 |
35 | .form-control {
36 | border: var(--border);
37 |
38 | &:disabled,
39 | &[readonly] {
40 | background-color: color(gray-300);
41 | }
42 | }
43 |
44 | input,
45 | select,
46 | textarea {
47 | box-shadow: var(--inset-shadow);
48 | }
49 |
50 | input:not([type="radio"]):not([type="checkbox"]),
51 | select {
52 | font-size: 16px;
53 | height: auto;
54 | line-height: normal;
55 | padding: 18px 12px;
56 | }
57 |
58 | select,
59 | select.form-control {
60 | height: 58px;
61 | }
62 |
63 | // select height needs to be auto for FF
64 | @-moz-document url-prefix("") {
65 | select {
66 | height: auto;
67 | }
68 | }
69 |
70 | textarea {
71 | min-height: 100px;
72 | width: 100%;
73 | }
74 |
75 | input[type="radio"] {
76 | box-shadow: none;
77 | }
78 |
79 | input::placeholder,
80 | textarea::placeholder {
81 | color: colors(gray-700);
82 | }
83 |
84 | .label,
85 | label {
86 | color: colors(black);
87 | font-weight: 500;
88 | }
89 |
90 | .form-check {
91 | display: block;
92 | padding-left: 1.25rem;
93 | position: relative;
94 |
95 | .form-check-input {
96 | margin-right: 20px;
97 | margin-top: 0.4rem;
98 | }
99 |
100 | .form-check-label {
101 | margin-left: 4px;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/assets/css/components/_hr.scss:
--------------------------------------------------------------------------------
1 | hr {
2 | margin-bottom: 20px;
3 | margin-top: 20px;
4 | }
5 |
6 | .hr-large {
7 | margin-bottom: 60px;
8 | margin-top: 60px;
9 | }
10 |
11 | .hr-dashed {
12 | border-style: dashed;
13 | border-width: 2px;
14 | margin-bottom: 30px;
15 | margin-top: 30px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/assets/css/components/_main.scss:
--------------------------------------------------------------------------------
1 | html {
2 | // Need to set the main font-size here to override bootstrap
3 | font-size: var(--font-size);
4 | }
5 |
6 | body {
7 | background: var(--background-color);
8 | color: var(--font-color);
9 | font-family: var(--font-family-base);
10 | font-size: var(--font-size);
11 | font-weight: 400;
12 | line-height: var(--line-height);
13 | min-height: 100%;
14 | min-height: 100vh;
15 | text-rendering: optimizeLegibility;
16 | }
17 |
18 | //These two rules are for proper footer/page alignment
19 | .page-wrapper {
20 | display: flex;
21 | flex-direction: column;
22 | min-height: 100%;
23 | min-height: 100vh;
24 | }
25 | .main-content-wrapper {
26 | // flex: 1; //this doesn't work for ie too well
27 | flex: 1 0 auto;
28 |
29 | // navbar is 58px tall - only needed with fixed nav
30 | // margin-top: 58px;
31 |
32 | &--no-padding {
33 | padding-top: 0;
34 | }
35 |
36 | &--with-padding {
37 | padding-bottom: 25px;
38 | padding-top: 25px;
39 | }
40 |
41 | &--grey-background {
42 | background: color(gray-300);
43 | }
44 | }
45 |
46 | h1,
47 | h2,
48 | h3,
49 | h4,
50 | h5,
51 | h6 {
52 | color: var(--font-color-header);
53 | font-family: var(--font-family-base);
54 | font-weight: 600;
55 | margin-bottom: 1.2rem;
56 | text-transform: none;
57 | }
58 | @media (min-width: 992px) {
59 | h1 {
60 | font-size: 48px;
61 | }
62 |
63 | h2 {
64 | font-size: 44px;
65 | }
66 |
67 | h3 {
68 | font-size: 40px;
69 | }
70 |
71 | h4 {
72 | font-size: 36px;
73 | }
74 |
75 | h5 {
76 | font-size: 32px;
77 | }
78 |
79 | h6 {
80 | font-size: 28px;
81 | }
82 | }
83 |
84 | // Control the default paragraph style here
85 | p {
86 | margin-bottom: 1rem;
87 | margin-top: 0;
88 | }
89 |
90 | //Main link styling
91 | a {
92 | color: var(--link-color);
93 |
94 | &:hover {
95 | color: colors(blue-700);
96 | }
97 | }
98 |
99 | //At large width, container-fluid should have a bit
100 | //more padding left and right
101 | @media (min-width: 1400px) {
102 | .container-fluid {
103 | padding-left: 60px;
104 | padding-right: 60px;
105 | }
106 | }
107 |
108 | //Fix weird bold issue
109 | b,
110 | strong {
111 | font-weight: 500;
112 | }
113 |
114 | // IE10/11 only
115 | // https://github.com/philipwalton/flexbugs/issues/75
116 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
117 | .modal-body {
118 | min-height: 1px;
119 | }
120 | }
121 |
122 | // Make sure disabled is not clickable
123 | [disabled] {
124 | cursor: not-allowed !important; /* stylelint-disable-line declaration-no-important */
125 | opacity: 0.35 !important; /* stylelint-disable-line declaration-no-important */
126 | pointer-events: none;
127 | }
128 |
129 | // Flex stretch image fix
130 | // https://stackoverflow.com/questions/36822370
131 | img {
132 | flex-shrink: 0;
133 | max-width: 100%;
134 | }
135 |
--------------------------------------------------------------------------------
/src/assets/css/components/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal-content {
2 | background: color(white);
3 |
4 | .btn,
5 | button {
6 | box-shadow: none;
7 | }
8 | }
9 |
10 | .modal-header {
11 | align-items: center;
12 | background: color(white);
13 | border-bottom: 1px solid color(gray-900);
14 |
15 | // So we can have the close button on the left
16 | flex-direction: row-reverse;
17 | justify-content: space-between;
18 | padding: 0.75rem 1rem;
19 |
20 | .close {
21 | margin-left: -1rem;
22 | margin-right: -56px;
23 | }
24 |
25 | .modal-title {
26 | margin-left: auto;
27 | margin-right: auto;
28 | }
29 |
30 | .btn,
31 | button {
32 | &:hover {
33 | box-shadow: none;
34 | }
35 | }
36 | }
37 |
38 | .modal-body {
39 | background: color(gray-300);
40 | }
41 |
42 | .modal-footer {
43 | border-top: 0;
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/css/components/_program.scss:
--------------------------------------------------------------------------------
1 | // For grouped elements
2 | .text-content {
3 | margin-bottom: 3rem;
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/css/components/_scrollbox.scss:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/44795011/8014660
2 |
3 | .scrollbox {
4 | background: #fff no-repeat;
5 | background-image: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0)),
6 | radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0));
7 | background-position: 0 0, 0 100%;
8 | background-size: 100% 14px;
9 | overflow: auto;
10 | position: relative;
11 | z-index: 1;
12 | }
13 |
14 | .scrollbox:before,
15 | .scrollbox:after {
16 | background: linear-gradient(to bottom, #fff, #fff 30%, rgba(255, 255, 255, 0));
17 | content: "";
18 | display: block;
19 | height: 30px;
20 | margin: 0 0 -30px;
21 | position: relative;
22 | z-index: -1;
23 | }
24 |
25 | .scrollbox:after {
26 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 70%, #fff);
27 | margin: -30px 0 0;
28 | }
29 |
--------------------------------------------------------------------------------
/src/assets/css/components/_user.scss:
--------------------------------------------------------------------------------
1 | section.user-action {
2 | padding: 35px 0 55px;
3 | }
4 |
5 | .user-block {
6 | background: color(blue);
7 | border-radius: 8px;
8 | margin: 0 auto;
9 | max-width: 100%;
10 | padding: 25px;
11 |
12 | @media (min-width: 992px) {
13 | max-width: 500px;
14 | }
15 |
16 | &__header {
17 | color: colors(white);
18 | }
19 |
20 | &__content {
21 | color: colors(white);
22 |
23 | label {
24 | color: colors(white);
25 | }
26 | }
27 |
28 | a {
29 | color: colors(white);
30 | text-decoration: underline;
31 |
32 | &:hover {
33 | color: colors(grey-200);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/css/components/_variables.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable color-no-hex */
2 |
3 | // Mainly, we want to use CSS Variables in our apps. The thing is, we need
4 | // to declare at least the base colors as SCSS Variables, in addition to
5 | // CSS Variables, as Bootstrap uses them to do some of it's initialization.
6 | // Really, you want to use the CSS Variable when possible.
7 |
8 | // Only for Bootstrap initialization
9 | $blue: #135edb;
10 | $indigo: #3f00ff;
11 | $purple: #800080;
12 | $pink: #ffc0cb;
13 | $red: #fb1505;
14 | $orange: #ff7f00;
15 | $yellow: #fdff00;
16 | $green: #16ca43;
17 | $gray-100: #f8f9fa;
18 | $gray-200: #e9ecef;
19 | $gray-300: #dee2e6;
20 | $gray-400: #ced4da;
21 | $gray-500: #adb5bd;
22 | $gray-600: #6c757d;
23 | $gray-700: #495057;
24 | $gray-800: #343a40;
25 | $gray-900: #212529;
26 | $black: #000;
27 | $white: #fff;
28 | $off-white: darken($white, 1%);
29 | $muted: rgba(15, 53, 86, 0.6);
30 | $font-color: $muted;
31 | $font-color-header: $black;
32 |
33 | // Ok, now start our actual app variables to be accessed with var(--blue)
34 |
35 | // Main colors for app
36 | $colors: (
37 | "blue": $blue,
38 | "indigo": $indigo,
39 | "purple": $purple,
40 | "pink": $pink,
41 | "red": $red,
42 | "orange": $orange,
43 | "yellow": $yellow,
44 | "green": $green,
45 |
46 | gray-100: $gray-100,
47 | gray-200: $gray-200,
48 | gray-300: $gray-300,
49 | gray-400: $gray-400,
50 | gray-500: $gray-500,
51 | gray-600: $gray-600,
52 | gray-700: $gray-700,
53 | gray-800: $gray-800,
54 | gray-900: $gray-900,
55 |
56 | "black": $black,
57 | "white": $white,
58 | off-white: $off-white,
59 | muted: $muted,
60 |
61 | blue-100: tint-color($blue, 8),
62 | blue-200: tint-color($blue, 6),
63 | blue-300: tint-color($blue, 4),
64 | blue-400: tint-color($blue, 2),
65 | blue-500: $blue,
66 | blue-600: shade-color($blue, 2),
67 | blue-700: shade-color($blue, 4),
68 | blue-800: shade-color($blue, 6),
69 | blue-900: shade-color($blue, 8),
70 |
71 | indigo-100: tint-color($indigo, 8),
72 | indigo-200: tint-color($indigo, 6),
73 | indigo-300: tint-color($indigo, 4),
74 | indigo-400: tint-color($indigo, 2),
75 | indigo-500: $indigo,
76 | indigo-600: shade-color($indigo, 2),
77 | indigo-700: shade-color($indigo, 4),
78 | indigo-800: shade-color($indigo, 6),
79 | indigo-900: shade-color($indigo, 8),
80 |
81 | purple-100: tint-color($purple, 8),
82 | purple-200: tint-color($purple, 6),
83 | purple-300: tint-color($purple, 4),
84 | purple-400: tint-color($purple, 2),
85 | purple-500: $purple,
86 | purple-600: shade-color($purple, 2),
87 | purple-700: shade-color($purple, 4),
88 | purple-800: shade-color($purple, 6),
89 | purple-900: shade-color($purple, 8),
90 |
91 | pink-100: tint-color($pink, 8),
92 | pink-200: tint-color($pink, 6),
93 | pink-300: tint-color($pink, 4),
94 | pink-400: tint-color($pink, 2),
95 | pink-500: $pink,
96 | pink-600: shade-color($pink, 2),
97 | pink-700: shade-color($pink, 4),
98 | pink-800: shade-color($pink, 6),
99 | pink-900: shade-color($pink, 8),
100 |
101 | red-100: tint-color($red, 8),
102 | red-200: tint-color($red, 6),
103 | red-300: tint-color($red, 4),
104 | red-400: tint-color($red, 2),
105 | red-500: $red,
106 | red-600: shade-color($red, 2),
107 | red-700: shade-color($red, 4),
108 | red-800: shade-color($red, 6),
109 | red-900: shade-color($red, 8),
110 |
111 | orange-100: tint-color($orange, 8),
112 | orange-200: tint-color($orange, 6),
113 | orange-300: tint-color($orange, 4),
114 | orange-400: tint-color($orange, 2),
115 | orange-500: $orange,
116 | orange-600: shade-color($orange, 2),
117 | orange-700: shade-color($orange, 4),
118 | orange-800: shade-color($orange, 6),
119 | orange-900: shade-color($orange, 8),
120 |
121 | yellow-100: tint-color($yellow, 8),
122 | yellow-200: tint-color($yellow, 6),
123 | yellow-300: tint-color($yellow, 4),
124 | yellow-400: tint-color($yellow, 2),
125 | yellow-500: $yellow,
126 | yellow-600: shade-color($yellow, 2),
127 | yellow-700: shade-color($yellow, 4),
128 | yellow-800: shade-color($yellow, 6),
129 | yellow-900: shade-color($yellow, 8),
130 |
131 | green-100: tint-color($green, 8),
132 | green-200: tint-color($green, 6),
133 | green-300: tint-color($green, 4),
134 | green-400: tint-color($green, 2),
135 | green-500: $green,
136 | green-600: shade-color($green, 2),
137 | green-700: shade-color($green, 4),
138 | green-800: shade-color($green, 6),
139 | green-900: shade-color($green, 8),
140 | );
141 |
142 | // Add the rest of our variables
143 | :root {
144 | // Add $colors array to :root as css variables
145 | @each $name, $color in $colors {
146 | --#{$name}: #{$color};
147 | }
148 |
149 | // Rest of the gloabl CSS Variables...
150 |
151 | // Animations
152 | --transition: all 0.2s ease-in;
153 |
154 | // Shadow
155 | --box-shadow-300: rgba(17, 51, 83, 0.1) 0px 2px 4px 0px;
156 | --box-shadow-400: rgba(17, 51, 83, 0.14) 0px 2px 4px 0px;
157 | --box-shadow-500: rgba(17, 51, 83, 0.18) 0px 2px 4px 0px; // Base
158 | --box-shadow-600: rgba(17, 51, 83, 0.22) 0px 2px 4px 0px;
159 | --box-shadow-700: rgba(17, 51, 83, 0.26) 0px 4px 6px 0px;
160 |
161 | // Text shadow
162 | --text-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
163 |
164 | // Icon shadow
165 | --icon-shadow: drop-shadow(0 0 1px rgba(100, 100, 100, 0.5));
166 |
167 | // Inset shadow
168 | --inset-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
169 |
170 | // Container box-shadow
171 | --container-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.4);
172 |
173 | // Borders
174 | --border: 1px solid var(--gray-400);
175 |
176 | // Gradient
177 | --gradient-primary: linear-gradient(90deg, var(--green-500), var(--green-600));
178 | --gradient-grey: linear-gradient(90deg, var(--gray-200), var(--gray-300));
179 |
180 | // Navbar colors
181 | --navbar-light-color: var(--black);
182 | --navbar-dark-color: var(--white);
183 |
184 | // Fonts
185 | --font-family-base: "Quicksand", "Helvetica Neue", Arial, sans-serif;
186 |
187 | // font sizing
188 | --font-size: 18px;
189 | --line-height: 1.6;
190 |
191 | // Rule border
192 | --rule-border: 1px solid rgba(100, 100, 100, 0.9);
193 |
194 | // Radius
195 | --border-radius: 4px;
196 |
197 | // Theme
198 | --background-color: var(--white);
199 | --background-color-secondary: var(--gray-400);
200 | --font-color: var(--muted);
201 | --font-color-header: var(--black);
202 | --link-color: var(--blue);
203 | --hr-color: var(--gray)
204 | }
205 |
206 | // Using our CSS font variable to set our Bootstrap font base.
207 | $font-family-base: var(--font-family-base) !default;
208 |
209 | // Overriding the Bootstrap theme and base colors using SCSS Variables
210 | $theme-colors: (
211 | primary: $blue,
212 | secondary: $gray-400,
213 | success: $green,
214 | info: lighten($blue, 10%),
215 | warning: $yellow,
216 | danger: $red,
217 | light: $white,
218 | dark: $black
219 | );
220 | $colors: (
221 | blue: $blue,
222 | indigo: $indigo,
223 | purple: $purple,
224 | pink: $pink,
225 | red: $red,
226 | orange: $orange,
227 | yellow: $yellow,
228 | green: $green,
229 | // teal: $teal,
230 | // cyan: $cyan,
231 | white: $white,
232 | gray: $gray-400,
233 | gray-dark: $gray-700
234 | );
235 |
236 | // For Bootstrap
237 | // Set the container width a little bit higher on larger screens
238 | // https://stackoverflow.com/a/53367733/8014660
239 | $container-max-widths: (
240 | sm: 540px,
241 | md: 720px,
242 | lg: 960px,
243 | xl: 1240px
244 | ) !default;
245 |
--------------------------------------------------------------------------------
/src/assets/css/components/_vendor.scss:
--------------------------------------------------------------------------------
1 | //
2 |
--------------------------------------------------------------------------------
/src/assets/css/functions/_color.scss:
--------------------------------------------------------------------------------
1 | @function colors($color-name) {
2 | @return var(--#{$color-name});
3 | }
4 |
5 | $theme-color-interval: 8%;
6 |
7 | @function tint-color($color, $level) {
8 | @return mix(white, $color, $level * $theme-color-interval);
9 | }
10 |
11 | @function shade-color($color, $level) {
12 | @return mix(black, $color, $level * $theme-color-interval);
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/css/vendor/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/css/vendor/.gitkeep
--------------------------------------------------------------------------------
/src/assets/files/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/files/.gitkeep
--------------------------------------------------------------------------------
/src/assets/images/artwork/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/artwork/.gitkeep
--------------------------------------------------------------------------------
/src/assets/images/main/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/main/.gitkeep
--------------------------------------------------------------------------------
/src/assets/images/main/lockup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/main/lockup.png
--------------------------------------------------------------------------------
/src/assets/images/utilities/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/utilities/.gitkeep
--------------------------------------------------------------------------------
/src/assets/images/utilities/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/utilities/404.png
--------------------------------------------------------------------------------
/src/assets/images/utilities/maintenance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/utilities/maintenance.png
--------------------------------------------------------------------------------
/src/assets/images/utilities/user-account-picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/assets/images/utilities/user-account-picture.png
--------------------------------------------------------------------------------
/src/common/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/common/.gitkeep
--------------------------------------------------------------------------------
/src/common/api.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from "axios";
2 | import { isDev } from "./get-env";
3 |
4 | type E = {
5 | code: number;
6 | message: string;
7 | };
8 |
9 | export type ServerError = {
10 | error: E;
11 | errors?: E[];
12 | };
13 |
14 | export const parseAxiosError = (e: AxiosError): ServerError => {
15 | if (e.response) {
16 | return {
17 | error: {
18 | code: e.response.data.error.code || e.response.status,
19 | message: e.response.data.error.message,
20 | },
21 | };
22 | }
23 |
24 | return {
25 | error: {
26 | code: 500,
27 | message: isDev()
28 | ? e.message || "Internal Server Error"
29 | : "Internal Server Error",
30 | },
31 | };
32 | };
33 |
34 | export const parseGeneralError = (e: Error): ServerError => {
35 | if (e.message) {
36 | return {
37 | error: {
38 | code: 500,
39 | message: isDev() ? e.message : "Internal Server Error",
40 | },
41 | };
42 | }
43 |
44 | return {
45 | error: {
46 | code: 500,
47 | message: "Internal Server Error",
48 | },
49 | };
50 | };
51 |
52 | export type ApiResponse = {
53 | data: T;
54 | };
55 |
--------------------------------------------------------------------------------
/src/common/axios-auth-interceptor.ts:
--------------------------------------------------------------------------------
1 | import axios from "@/common/axios";
2 | import createAuthRefreshInterceptor from "axios-auth-refresh";
3 | import { UserTokens } from "@/store/user/types";
4 | import { AxiosResponse } from "axios";
5 |
6 | const getToken = (type: "accessToken" | "refreshToken") =>
7 | localStorage.getItem(type);
8 |
9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
10 | const refreshAuthLogic = async (failedRequest: any) => {
11 | try {
12 | const tokens: AxiosResponse<{
13 | data: {
14 | accessToken: UserTokens["accessToken"];
15 | refreshToken: UserTokens["refreshToken"];
16 | };
17 | }> = await axios.post("user/refreshAccessToken", {
18 | username: "demousername",
19 | refreshToken: getToken("refreshToken"),
20 | });
21 |
22 | localStorage.setItem("accessToken", tokens.data.data.accessToken);
23 | localStorage.setItem("refreshToken", tokens.data.data.refreshToken);
24 | // functional/immutable-data
25 | // eslint-disable-next-line
26 | failedRequest.response.config.headers.Authorization = `Bearer ${getToken(
27 | "accessToken",
28 | )}`;
29 | return Promise.resolve();
30 | } catch (error) {
31 | Promise.reject();
32 | }
33 | };
34 |
35 | export const runAxiosAuthInterceptor = () => {
36 | createAuthRefreshInterceptor(axios, refreshAuthLogic);
37 | };
38 |
--------------------------------------------------------------------------------
/src/common/axios.ts:
--------------------------------------------------------------------------------
1 | // Because we want to customize the baseURL for axios, we export an instance
2 | // here and use *it* as axios throughout our app.
3 |
4 | import axios, { AxiosInstance } from "axios";
5 |
6 | // If using the local external option when serving the frontend,
7 | // you need to provide the base url the external serving provides.
8 | const defaultOptions = {
9 | baseURL: `${process.env.REACT_APP_API_URL}/api/v1`,
10 | };
11 |
12 | // Create instance
13 | const instance = axios.create(defaultOptions);
14 |
15 | // eslint-disable-next-line import/no-default-export
16 | export default instance;
17 |
18 | // Here's our authorization header helper. It'll read the localStorage and
19 | // use a potentially available accessToken to refresh our user
20 | export const setAuthorizationHeader = (a: AxiosInstance) => {
21 | // functional/immutable-data
22 | // eslint-disable-next-line
23 | a.defaults.headers.common.Authorization = `Bearer ${
24 | localStorage.getItem("accessToken") || ""
25 | }`;
26 | };
27 |
--------------------------------------------------------------------------------
/src/common/colors.ts:
--------------------------------------------------------------------------------
1 | // Keep updated with @/assets/components/_variables.scss
2 |
3 | export const black = "#000000";
4 | export const white = "#ffffff";
5 |
6 | export const blue = "#135edb";
7 | export const indigo = "#3F00FF";
8 | export const purple = "#800080";
9 | export const pink = "#ffc0cb";
10 | export const red = "#fb1505";
11 | export const orange = "#FF7F00";
12 | export const yellow = "#FFFF00";
13 | export const green = "#16ca43";
14 |
15 | export const gray100 = "#f8f9fa";
16 | export const gray200 = "#e9ecef";
17 | export const gray300 = "#dee2e6";
18 | export const gray400 = "#ced4da";
19 | export const gray500 = "#adb5bd";
20 | export const gray600 = "#6c757d";
21 | export const gray700 = "#495057";
22 | export const gray800 = "#343a40";
23 | export const gray900 = "#212529";
24 |
25 | // Palettes built with https://noeldelgado.github.io/shadowlord
26 |
27 | export const blue100 = "#e3ecfb";
28 | export const blue200 = "#afc8f3";
29 | export const blue300 = "#7ba5eb";
30 | export const blue400 = "#4781e3";
31 | export const blue500 = "#135edb"; // blue
32 | export const blue600 = "#0f49ab";
33 | export const blue700 = "#0b357b";
34 | export const blue800 = "#06204a";
35 | export const blue900 = "#020b1a";
36 |
37 | export const indigo100 = "#e8e0ff";
38 | export const indigo200 = "#bea8ff";
39 | export const indigo300 = "#9370ff";
40 | export const indigo400 = "#6938ff";
41 | export const indigo500 = "#3F00FF"; // indigo
42 | export const indigo600 = "#3100c7";
43 | export const indigo700 = "#23008f";
44 | export const indigo800 = "#150057";
45 | export const indigo900 = "#08001f";
46 |
47 | export const purple100 = "#f0e0f0";
48 | export const purple200 = "#d4a8d4";
49 | export const purple300 = "#b870b8";
50 | export const purple400 = "#9c389c";
51 | export const purple500 = "#800080"; // purple
52 | export const purple600 = "#640064";
53 | export const purple700 = "#480048";
54 | export const purple800 = "#2c002c";
55 | export const purple900 = "#0f000f";
56 |
57 | export const pink100 = "#fff7f9";
58 | export const pink200 = "#ffeaed";
59 | export const pink300 = "#ffdce2";
60 | export const pink400 = "#ffced6";
61 | export const pink500 = "#ffc0cb"; // pink
62 | export const pink600 = "#c7969e";
63 | export const pink700 = "#8f6c72";
64 | export const pink800 = "#574145";
65 | export const pink900 = "#1f1718";
66 |
67 | export const red100 = "#ffe3e1";
68 | export const red200 = "#feafaa";
69 | export const red300 = "#fd7c73";
70 | export const red400 = "#fc483c";
71 | export const red500 = "#fb1505"; // red
72 | export const red600 = "#c41004";
73 | export const red700 = "#8d0c03";
74 | export const red800 = "#550702";
75 | export const red900 = "#1e0301";
76 |
77 | export const orange100 = "#fff0e0";
78 | export const orange200 = "#ffd3a8";
79 | export const orange300 = "#ffb770";
80 | export const orange400 = "#ff9b38";
81 | export const orange500 = "#FF7F00"; // orange
82 | export const orange600 = "#c76300";
83 | export const orange700 = "#8f4700";
84 | export const orange800 = "#572b00";
85 | export const orange900 = "#1f0f00";
86 |
87 | export const yellow100 = "#ffffe0";
88 | export const yellow200 = "#ffffa8";
89 | export const yellow300 = "#ffff70";
90 | export const yellow400 = "#ffff38";
91 | export const yellow500 = "#FFFF00"; // yellow
92 | export const yellow600 = "#c7c700";
93 | export const yellow700 = "#8f8f00";
94 | export const yellow800 = "#575700";
95 | export const yellow900 = "#1f1f00";
96 |
97 | export const green100 = "#e3f9e8";
98 | export const green200 = "#b0edbf";
99 | export const green300 = "#7de196";
100 | export const green400 = "#49d66c";
101 | export const green500 = "#16ca43"; // green
102 | export const green600 = "#119e34";
103 | export const green700 = "#0c7126";
104 | export const green800 = "#074517";
105 | export const green900 = "#031808";
106 |
--------------------------------------------------------------------------------
/src/common/ga-event.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/react-ga/react-ga#reactgaeventargs
2 | // gaEvent("User Signup", "Submit", "Success");
3 |
4 | import ReactGA from "react-ga";
5 |
6 | export const gaEvent = (category: string, action: string, label: string) => {
7 | ReactGA.event({ category, action, label });
8 | };
9 |
--------------------------------------------------------------------------------
/src/common/ga-listener.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/react-ga/react-ga/issues/122#issuecomment-521781395
2 |
3 | import { useEffect } from "react";
4 | import ReactGA from "react-ga";
5 | import { withRouter, RouteComponentProps } from "react-router";
6 | import { Location, LocationListener, UnregisterCallback } from "history";
7 | import { isProd } from "./get-env";
8 |
9 | const sendPageView: LocationListener = (location: Location): void => {
10 | ReactGA.set({ page: location.pathname });
11 | ReactGA.pageview(location.pathname);
12 | };
13 |
14 | type Props = RouteComponentProps & {
15 | children: JSX.Element;
16 | trackingId?: string;
17 | };
18 | const GAListener = ({ children, trackingId, history }: Props): JSX.Element => {
19 | useEffect((): UnregisterCallback | void => {
20 | if (trackingId && isProd()) {
21 | ReactGA.initialize(trackingId);
22 | sendPageView(history.location, "REPLACE");
23 | return history.listen(sendPageView);
24 | }
25 | }, [history, trackingId]);
26 |
27 | return children;
28 | };
29 |
30 | // eslint-disable-next-line import/no-default-export
31 | export default withRouter(GAListener);
32 |
--------------------------------------------------------------------------------
/src/common/get-env.ts:
--------------------------------------------------------------------------------
1 | export const isDev = () => {
2 | const env = process.env.NODE_ENV || "development";
3 |
4 | if (env === "development") {
5 | return true;
6 | }
7 |
8 | return false;
9 | };
10 |
11 | export const isProd = () => {
12 | const env = process.env.NODE_ENV || "development";
13 |
14 | if (env === "production") {
15 | return true;
16 | }
17 |
18 | return false;
19 | };
20 |
21 | export const isTesting = () => {
22 | const env = process.env.NODE_ENV || "development";
23 |
24 | if (env === "test") {
25 | return true;
26 | }
27 |
28 | return false;
29 | };
30 |
--------------------------------------------------------------------------------
/src/common/logger.ts:
--------------------------------------------------------------------------------
1 | export const logger = (
2 | message: string,
3 | type?: "log" | "warn" | "error" | "info",
4 | ) => {
5 | type = type || "log";
6 |
7 | // eslint-disable-next-line no-console
8 | console[type](message);
9 | };
10 |
--------------------------------------------------------------------------------
/src/common/program/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/common/program/.gitkeep
--------------------------------------------------------------------------------
/src/common/sleep.ts:
--------------------------------------------------------------------------------
1 | export const sleep = async (milliseconds: number) =>
2 | new Promise((resolve) => setTimeout(resolve, milliseconds));
3 |
--------------------------------------------------------------------------------
/src/common/truncate.ts:
--------------------------------------------------------------------------------
1 | export const truncate = (input: string, length: number) =>
2 | input.length > length ? `${input.substring(0, length)}...` : input;
3 |
--------------------------------------------------------------------------------
/src/components/charts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/charts/.gitkeep
--------------------------------------------------------------------------------
/src/components/layouts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/layouts/.gitkeep
--------------------------------------------------------------------------------
/src/components/layouts/main/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/layouts/main/.gitkeep
--------------------------------------------------------------------------------
/src/components/layouts/main/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FallbackProps } from "react-error-boundary";
3 |
4 | export const ErrorBoundaryComponent = ({
5 | componentStack,
6 | error,
7 | }: FallbackProps) => (
8 |
9 |
10 | Sorry, but there was an error displaying the page.
11 |
12 |
Please refresh the page and try again.
13 |
14 |
Here’s what we know…
15 |
16 | Error: {error ? error.toString() : undefined}
17 |
18 |
19 | Stacktrace: {componentStack}
20 |
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/src/components/layouts/main/Maintenance.module.scss:
--------------------------------------------------------------------------------
1 | @import "~@/assets/css/components/variables.scss";
2 |
3 | .section {
4 | padding: 1rem 0;
5 | text-align: left;
6 |
7 | @media(min-width: 992px) {
8 | text-align: center;
9 | }
10 | }
11 |
12 | .image-callout {
13 | max-height: 350px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/layouts/main/Maintenance.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import maintenanceImage from "@/assets/images/utilities/maintenance.png";
4 | import classNames from "classnames";
5 | import styles from "./Maintenance.module.scss";
6 |
7 | export const Maintenance = () => (
8 |
9 |
10 |
11 |
12 |
22 | Maintenance
23 | We're currently doing some house-cleaning.
24 | Please check back later. Thank you!
25 |
26 |
27 |
28 |
29 | );
30 |
--------------------------------------------------------------------------------
/src/components/layouts/main/NotFound.module.scss:
--------------------------------------------------------------------------------
1 | @import "~@/assets/css/components/variables.scss";
2 |
3 | .section {
4 | padding: 1rem 0;
5 | text-align: left;
6 |
7 | @media(min-width: 992px) {
8 | text-align: center;
9 | }
10 | }
11 |
12 | .image-callout {
13 | max-height: 350px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/layouts/main/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col, Button } from "react-bootstrap";
3 | import notFoundImage from "@/assets/images/utilities/404.png";
4 | import { LinkContainer } from "react-router-bootstrap";
5 | import classNames from "classnames";
6 | import styles from "./NotFound.module.scss";
7 |
8 | export const NotFound = () => (
9 |
10 |
11 |
12 |
13 |
23 | Not Found
24 | Hmm, we couldn't quite find what you were looking for.
25 |
26 | Go Home
27 |
28 |
29 |
30 |
31 |
32 | );
33 |
--------------------------------------------------------------------------------
/src/components/layouts/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/layouts/pages/.gitkeep
--------------------------------------------------------------------------------
/src/components/layouts/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { RootState } from "@/store";
4 | import { Container, Row, Col } from "react-bootstrap";
5 |
6 | export const Home = () => {
7 | const user = useSelector((state: RootState) => state.user.user);
8 | const isLoggedIn = !!user.id;
9 |
10 | return (
11 |
12 |
13 |
14 |
15 | Home
16 |
17 |
18 | {isLoggedIn && (
19 |
20 |
Here's your user...
21 |
22 | {JSON.stringify(user)}
23 |
24 |
25 | )}
26 |
27 |
28 | This is a simple SPA built
29 | using Koa (2.5.1) as the backend and React (16.8.3) as the frontend.
30 | If you don't want to create an account you can just use{" "}
31 | demousername and demopassword to
32 | login to the app.
33 |
34 |
35 |
36 | This site has a sister! Visit it here{" "}
37 |
42 | https://koa-vue-notes-web.innermonkdesign.com
43 |
44 | . It's the exact same app - but written in Vue! It's also{" "}
45 |
50 | open-sourced.
51 |
52 |
53 |
54 | {/* React */}
55 |
56 |
61 |
65 | {" "}
66 |
71 |
75 | {" "}
76 |
81 |
85 | {" "}
86 |
91 |
95 | {" "}
96 |
101 |
105 | {" "}
106 |
107 |
108 | {/* Vue */}
109 |
110 |
115 |
119 | {" "}
120 |
125 |
129 | {" "}
130 |
135 |
139 | {" "}
140 |
145 |
149 | {" "}
150 |
155 |
159 | {" "}
160 |
161 |
162 | {/* Koa */}
163 |
164 |
169 |
173 | {" "}
174 |
179 |
183 | {" "}
184 |
189 |
193 | {" "}
194 |
199 |
203 | {" "}
204 |
209 |
213 | {" "}
214 |
215 |
216 |
217 |
218 | );
219 | };
220 |
--------------------------------------------------------------------------------
/src/components/layouts/program/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/layouts/program/.gitkeep
--------------------------------------------------------------------------------
/src/components/layouts/program/CreateNote.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import { CreateNoteForm } from "@/components/partials/forms/components/CreateNoteForm";
4 |
5 | export const CreateNote = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/layouts/program/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import {
4 | ListGroup,
5 | ListGroupItem,
6 | Button,
7 | Container,
8 | Row,
9 | Col,
10 | } from "react-bootstrap";
11 | import { RootState, GeneralThunkDispatch } from "@/store";
12 | import { truncate } from "@/common/truncate";
13 | import { LinkContainer } from "react-router-bootstrap";
14 | import { useHistory } from "react-router-dom";
15 | import { Note } from "@/store/note/types";
16 | import { all } from "@/store/note/actions-api";
17 | import { useToasts } from "react-toast-notifications";
18 | import { setOkToLoadMore, setQuery } from "@/store/note/actions-store";
19 |
20 | export const Dashboard = () => {
21 | const dispatch = useDispatch();
22 | const notes = useSelector((state: RootState) => state.note.notes);
23 | const okToLoadMore = useSelector(
24 | (state: RootState) => state.note.okToLoadMore,
25 | );
26 | const query = useSelector((state: RootState) => state.note.query);
27 | const history = useHistory();
28 | const { addToast } = useToasts();
29 |
30 | const loadProgramData = async () => {
31 | try {
32 | const result = await dispatch(all(query));
33 |
34 | // Sort out the new query data now...
35 | if (result.length === query.limit) {
36 | dispatch(setOkToLoadMore(true));
37 | dispatch(setQuery({ ...query, page: query.page + 1 }));
38 | } else {
39 | dispatch(setOkToLoadMore(false));
40 | }
41 | } catch (error) {
42 | addToast(
43 | "Hmm, there was an error retrieving your data. Please refresh the page and try again.",
44 | {
45 | appearance: "error",
46 | },
47 | );
48 | }
49 | };
50 |
51 | // On the Dashboard's page load, we'll load the program's data.
52 | useEffect(() => {
53 | if (!notes.length) {
54 | loadProgramData();
55 | }
56 | }, []); // eslint-disable-line react-hooks/exhaustive-deps
57 |
58 | const noteClicked = (note: Note) => {
59 | history.push(`/edit-note/${note.id}`);
60 | };
61 |
62 | const noteList = notes.map((n) => (
63 | noteClicked(n)} action>
64 | {n.title}
65 | {truncate(n.content, 20)}
66 |
67 | ));
68 |
69 | const loadMore = () => {
70 | loadProgramData();
71 | };
72 |
73 | return (
74 |
75 |
76 |
77 |
78 |
79 | Create Note
80 |
81 |
82 |
83 | {notes.length > 0 && {noteList} }
84 |
85 |
86 |
87 |
88 | {okToLoadMore && (
89 |
90 | Load more...
91 |
92 | )}
93 |
94 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/src/components/layouts/program/EditNote.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import { EditNoteForm } from "@/components/partials/forms/components/EditNoteForm";
4 | import { useParams, useHistory } from "react-router-dom";
5 | import { find } from "@/store/note/actions-api";
6 | import { useDispatch } from "react-redux";
7 | import { NoteThunkDispatch, Note } from "@/store/note/types";
8 | import { useToasts } from "react-toast-notifications";
9 |
10 | export const EditNote = () => {
11 | const { id } = useParams();
12 | const { addToast } = useToasts();
13 | const dispatch = useDispatch();
14 | const [note, setNote] = useState();
15 | const history = useHistory();
16 |
17 | const getNoteForEdit = async (noteId: number) => {
18 | try {
19 | const n: Note = await dispatch(find(noteId));
20 |
21 | if (!n.id) {
22 | addToast("No note found...", {
23 | appearance: "error",
24 | });
25 |
26 | history.push("/dashboard");
27 |
28 | return;
29 | }
30 |
31 | setNote(n);
32 | } catch (error) {
33 | addToast("No note found...", {
34 | appearance: "error",
35 | });
36 | }
37 | };
38 |
39 | useEffect(() => {
40 | if (id !== undefined) {
41 | getNoteForEdit(Number(id));
42 | }
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, []);
45 |
46 | return (
47 |
48 |
49 | {note && }
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/partials/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/partials/.gitkeep
--------------------------------------------------------------------------------
/src/components/partials/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/partials/components/.gitkeep
--------------------------------------------------------------------------------
/src/components/partials/forms/components/CreateNoteForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | TextArea,
6 | SubmitButton,
7 | } from "@/components/partials/forms/inputs/Inputs";
8 | import { useToasts } from "react-toast-notifications";
9 | import { useDispatch } from "react-redux";
10 | import {
11 | NoteCreatePost,
12 | NoteCreatePostValidation,
13 | } from "@/store/note/api-types";
14 | import { create } from "@/store/note/actions-api";
15 | import { ServerError } from "@/common/api";
16 | import { useHistory } from "react-router-dom";
17 | import { Row, Col, Container, Button } from "react-bootstrap";
18 | import { NoteThunkDispatch } from "@/store/note/types";
19 | import { GoChevronLeft } from "react-icons/go";
20 | import { LinkContainer } from "react-router-bootstrap";
21 |
22 | const defaultValues: NoteCreatePost = {
23 | title: "",
24 | content: "",
25 | };
26 |
27 | export const CreateNoteForm = () => {
28 | const [isLoading, setIsLoading] = useState(false);
29 | const history = useHistory();
30 | const { addToast } = useToasts();
31 | const dispatch = useDispatch();
32 |
33 | const handleSubmit = async (
34 | values: NoteCreatePost,
35 | actions: FormikHelpers,
36 | ) => {
37 | try {
38 | setIsLoading(true);
39 | await dispatch(create(values));
40 | actions.resetForm();
41 | history.push("/dashboard");
42 | } catch (error) {
43 | const e = error as ServerError;
44 | if (e && (e.error || e.errors)) {
45 | //
46 | }
47 | addToast("There was an error creating your note. Please try again.", {
48 | appearance: "error",
49 | });
50 | } finally {
51 | setIsLoading(false);
52 | }
53 | };
54 |
55 | return (
56 |
57 |
62 |
102 |
103 |
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/components/partials/forms/components/EditNoteForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | TextArea,
6 | SubmitButton,
7 | } from "@/components/partials/forms/inputs/Inputs";
8 | import { useToasts } from "react-toast-notifications";
9 | import {
10 | NoteCreatePost,
11 | NoteCreatePostValidation,
12 | } from "@/store/note/api-types";
13 | import { ServerError } from "@/common/api";
14 | import { Row, Col, Container, Button } from "react-bootstrap";
15 | import { Note } from "@/store/note/types";
16 | import { useHistory } from "react-router-dom";
17 | import { update, del as delActionApi } from "@/store/note/actions-api";
18 | import { useDispatch } from "react-redux";
19 | import { GeneralThunkDispatch } from "@/store";
20 | import { GoChevronLeft } from "react-icons/go";
21 | import { LinkContainer } from "react-router-bootstrap";
22 |
23 | type EditNoteFormProps = {
24 | note: Note;
25 | };
26 |
27 | export const EditNoteForm = (props: EditNoteFormProps) => {
28 | let defaultValues: NoteCreatePost = {
29 | title: "",
30 | content: "",
31 | };
32 |
33 | const [isLoading, setIsLoading] = useState(false);
34 | const history = useHistory();
35 | const { addToast } = useToasts();
36 | const dispatch = useDispatch();
37 |
38 | defaultValues = {
39 | title: props.note.title,
40 | content: props.note.content,
41 | };
42 |
43 | const handleSubmit = async (
44 | values: NoteCreatePost,
45 | actions: FormikHelpers,
46 | ) => {
47 | try {
48 | // Ok, so if we're here, the user has edited the note. We'll create a
49 | // new object that has the new values.
50 | const editedNote = {
51 | ...props.note,
52 | title: values.title,
53 | content: values.content,
54 | };
55 |
56 | setIsLoading(true);
57 | await dispatch(update(editedNote));
58 | actions.resetForm();
59 | history.push("/dashboard");
60 | } catch (error) {
61 | const e = error as ServerError;
62 | if (e && (e.error || e.errors)) {
63 | //
64 | }
65 | addToast("There was an error updating your note. Please try again.", {
66 | appearance: "error",
67 | });
68 | } finally {
69 | setIsLoading(false);
70 | }
71 | };
72 |
73 | const del = async () => {
74 | const result = window.confirm("Are you sure you want to delete this note?");
75 | if (!result) {
76 | return;
77 | }
78 |
79 | try {
80 | await dispatch(delActionApi(props.note));
81 | history.push("/dashboard");
82 | } catch (error) {
83 | //
84 | }
85 | };
86 |
87 | return (
88 |
89 |
94 |
95 |
96 |
97 |
98 |
99 | Dashboard
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | Edit Note
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
131 |
132 |
133 |
134 |
135 |
136 |
143 | Delete
144 |
145 |
146 |
147 |
148 |
149 |
150 | );
151 | };
152 |
--------------------------------------------------------------------------------
/src/components/partials/forms/inputs/Inputs.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, useState } from "react";
2 | import { useField } from "formik";
3 | import { v4 as uuidv4 } from "uuid";
4 |
5 | type SubmitButtonProps = {
6 | name: string;
7 | text: string;
8 | loading: boolean;
9 | loadingText: string;
10 | };
11 |
12 | type InputProps = {
13 | name: string;
14 | type: string;
15 | label?: string;
16 | placeholder?: string;
17 | disabled?: boolean;
18 | };
19 |
20 | type TextAreaProps = Omit;
21 |
22 | type CheckboxProps = {
23 | name: string;
24 | label?: string;
25 | disabled?: boolean;
26 | };
27 |
28 | type SelectOption = {
29 | value: string | number;
30 | label: string;
31 | };
32 |
33 | type SelectProps = {
34 | name: string;
35 | label?: string;
36 | placeholder?: string;
37 | options: SelectOption[];
38 | disabled?: boolean;
39 | };
40 |
41 | type RadioOption = {
42 | value: string | number;
43 | label: string;
44 | };
45 |
46 | type RadioProps = {
47 | name: string;
48 | label?: string;
49 | options: RadioOption[];
50 | disabled?: boolean;
51 | };
52 |
53 | export const SubmitButton = (props: SubmitButtonProps) => {
54 | const [id] = useState(() => uuidv4());
55 |
56 | return (
57 |
63 | {props.loading ? (
64 | {props.loadingText}
65 | ) : (
66 | {props.text}
67 | )}
68 |
69 | );
70 | };
71 |
72 | export const TextInput: FunctionComponent = (props: InputProps) => {
73 | const [field, { error, touched }] = useField({
74 | name: props.name,
75 | type: props.type,
76 | });
77 |
78 | const [id] = useState(() => uuidv4());
79 |
80 | return (
81 |
82 | {props.label !== undefined && (
83 |
84 | {props.label}
85 |
86 | )}
87 |
95 | {touched && error ? (
96 |
{error}
97 | ) : null}
98 |
99 | );
100 | };
101 |
102 | export const TextArea: FunctionComponent = (
103 | props: TextAreaProps,
104 | ) => {
105 | const [field, { error, touched }] = useField({
106 | name: props.name,
107 | });
108 |
109 | const [id] = useState(() => uuidv4());
110 |
111 | return (
112 | <>
113 |
114 | {props.label !== undefined && (
115 |
119 | {props.label}
120 |
121 | )}
122 |
129 | {touched && error ? (
130 |
{error}
131 | ) : null}
132 |
133 | >
134 | );
135 | };
136 |
137 | export const Checkbox: FunctionComponent = (
138 | props: CheckboxProps,
139 | ) => {
140 | const [field, { error, touched }] = useField({
141 | name: props.name,
142 | });
143 |
144 | const [id] = useState(() => uuidv4());
145 |
146 | return (
147 |
148 |
149 |
157 |
158 | {props.label}
159 |
160 |
161 |
162 | {touched && error ? (
163 |
{error}
164 | ) : null}
165 |
166 | );
167 | };
168 |
169 | export const Select: FunctionComponent = (props: SelectProps) => {
170 | const [field, { error, touched }] = useField({
171 | name: props.name,
172 | });
173 |
174 | const [id] = useState(() => uuidv4());
175 |
176 | return (
177 |
178 | {props.label !== undefined && (
179 |
180 | {props.label}
181 |
182 | )}
183 |
190 | {props.options.map((o) => {
191 | return (
192 |
193 | {o.label}
194 |
195 | );
196 | })}
197 |
198 | {touched && error ? (
199 |
{error}
200 | ) : null}
201 |
202 | );
203 | };
204 |
205 | export const Radio: FunctionComponent = (props: RadioProps) => {
206 | const [field, { error, touched }] = useField({
207 | name: props.name,
208 | });
209 |
210 | const [id] = useState(() => uuidv4());
211 |
212 | // https://github.com/jaredpalmer/formik/issues/1243
213 | const createEvent = (value: string) => {
214 | return {
215 | persist: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
216 | target: {
217 | type: "change",
218 | name: props.name,
219 | value,
220 | },
221 | };
222 | };
223 |
224 | const handleChange = (e: React.ChangeEvent) => {
225 | field.onChange(createEvent(e.target.value));
226 | };
227 |
228 | return (
229 |
230 | {props.label !== undefined && (
231 |
232 | {props.label}
233 |
234 | )}
235 | {props.options.map((o) => {
236 | return (
237 |
238 |
247 |
248 | {o.label}
249 |
250 |
251 | );
252 | })}
253 | {touched && error ? (
254 |
{error}
255 | ) : null}
256 |
257 | );
258 | };
259 |
--------------------------------------------------------------------------------
/src/components/partials/main/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/partials/main/.gitkeep
--------------------------------------------------------------------------------
/src/components/partials/main/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 |
4 | export const Footer = () => (
5 |
6 |
7 |
8 |
9 |
14 | Koa-React-Notes
15 | {" "}
16 | is a SPA using Koa (2.3) as the{" "}
17 |
22 | backend
23 | {" "}
24 | and React (16.8.3) as the{" "}
25 |
30 | frontend
31 |
32 | .
33 |
34 |
35 |
36 |
37 |
38 |
39 | Made with{" "}
40 |
41 | ☕️
42 | {" "}
43 | by{" "}
44 |
49 | John Datserakis
50 |
51 | .
52 |
53 |
54 |
55 |
56 | );
57 |
--------------------------------------------------------------------------------
/src/components/partials/main/Nav.module.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .navbar {
3 | padding-left: 0;
4 | padding-right: 0;
5 | }
6 | }
7 |
8 | // Keep navbar-buttons properly inline
9 | .navbar-button {
10 | line-height: 29px;
11 | }
12 |
13 | :global(.navbar-toggler) {
14 | border: none;
15 | box-shadow: none;
16 |
17 | &:hover {
18 | box-shadow: none;
19 | opacity: 0.9;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/partials/main/Nav.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Nav as BoostrapNav,
4 | Navbar,
5 | NavDropdown,
6 | Button,
7 | Container,
8 | } from "react-bootstrap";
9 | import { LinkContainer } from "react-router-bootstrap";
10 | import logo from "@/assets/images/main/lockup.png";
11 | import userAccountPicture from "@/assets/images/utilities/user-account-picture.png";
12 | import { logout } from "@/store/user/actions-api";
13 | import { useDispatch, useSelector } from "react-redux";
14 | import { UserThunkDispatch } from "@/store/user/types";
15 | import { useHistory } from "react-router-dom";
16 | import { RootState, GeneralThunkDispatch } from "@/store";
17 | import { clearNotes } from "@/store/note/actions-store";
18 | import classNames from "classnames";
19 | import { GoLinkExternal } from "react-icons/go";
20 | import styles from "./Nav.module.scss";
21 |
22 | export const Nav = () => {
23 | const dispatchUser = useDispatch();
24 | const dispatchNote = useDispatch();
25 | const history = useHistory();
26 | const user = useSelector((state: RootState) => state.user.user);
27 | const isLoggedIn = !!user.id;
28 |
29 | const navLogout = async () => {
30 | await dispatchUser(logout());
31 | dispatchNote(clearNotes());
32 | history.push("/");
33 | };
34 |
35 | return !isLoggedIn ? (
36 |
37 |
38 |
39 |
40 |
50 |
51 |
52 |
53 |
54 |
55 |
61 | View on GitHub
62 |
63 |
64 |
65 |
66 | Login
67 |
68 |
69 |
70 | Signup
71 |
72 |
73 |
74 |
75 |
76 |
77 | ) : (
78 |
79 |
80 |
81 |
82 |
90 |
91 |
92 |
93 |
94 |
95 |
106 | }
107 | id="basic-nav-dropdown"
108 | >
109 |
110 | Signed in as {user.email}
111 |
112 |
113 |
114 | Dashboard
115 |
116 |
117 | Create Note
118 |
119 |
120 |
121 | App Homepage
122 |
123 | {
125 | navLogout();
126 | }}
127 | >
128 | Logout
129 |
130 |
131 |
132 |
133 |
134 |
135 | );
136 | };
137 |
--------------------------------------------------------------------------------
/src/components/partials/modals/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/partials/modals/.gitkeep
--------------------------------------------------------------------------------
/src/components/partials/program/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/partials/program/.gitkeep
--------------------------------------------------------------------------------
/src/components/user/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/components/user/.gitkeep
--------------------------------------------------------------------------------
/src/components/user/components/Forgot.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import { ForgotForm } from "@/components/user/forms/ForgotForm";
4 |
5 | export const Forgot = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/user/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { LoginForm } from "@/components/user/forms/LoginForm";
3 | import { Container, Row, Col } from "react-bootstrap";
4 |
5 | export const Login = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/user/components/Reset.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import { ResetForm } from "@/components/user/forms/ResetForm";
4 |
5 | export const Reset = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/user/components/Signup.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 | import { SignupForm } from "@/components/user/forms/SignupForm";
4 |
5 | export const Signup = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/user/forms/ForgotForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | SubmitButton,
6 | } from "@/components/partials/forms/inputs/Inputs";
7 | import { useToasts } from "react-toast-notifications";
8 | import { useDispatch } from "react-redux";
9 | import { forgot } from "@/store/user/actions-api";
10 | import {
11 | UserForgotPost,
12 | UserForgotPostValidation,
13 | } from "@/store/user/api-types";
14 | import { UserThunkDispatch } from "@/store/user/types";
15 | import { ServerError } from "@/common/api";
16 | import { useHistory, Link } from "react-router-dom";
17 | import { Row, Col, Container } from "react-bootstrap";
18 |
19 | const defaultValues: UserForgotPost = {
20 | email: "",
21 | url: `${process.env.REACT_APP_URL}/user/reset`,
22 | type: "web",
23 | };
24 |
25 | export const ForgotForm = () => {
26 | const [isLoading, setIsLoading] = useState(false);
27 | const history = useHistory();
28 | const { addToast } = useToasts();
29 | const dispatch = useDispatch();
30 |
31 | const handleSubmit = async (
32 | values: UserForgotPost,
33 | actions: FormikHelpers,
34 | ) => {
35 | try {
36 | setIsLoading(true);
37 | await dispatch(forgot(values));
38 |
39 | // Clear inputs
40 | actions.resetForm();
41 |
42 | // Push to dashboard
43 | history.push("/");
44 |
45 | addToast("Please check your email", { appearance: "success" });
46 | } catch (error) {
47 | const e = error as ServerError;
48 | if (e && (e.error || e.errors)) {
49 | //
50 | }
51 |
52 | addToast("Please check your email", { appearance: "success" });
53 | } finally {
54 | setIsLoading(false);
55 | }
56 | };
57 |
58 | return (
59 | <>
60 |
61 |
66 |
67 |
68 |
69 | Forgot
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Login {" | "}
96 | Signup
97 |
98 |
99 |
100 | >
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/components/user/forms/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | SubmitButton,
6 | } from "@/components/partials/forms/inputs/Inputs";
7 | import { useToasts } from "react-toast-notifications";
8 | import { useDispatch } from "react-redux";
9 | import { login } from "@/store/user/actions-api";
10 | import { all } from "@/store/note/actions-api";
11 | import { UserLoginPost, UserLoginPostValidation } from "@/store/user/api-types";
12 | import { UserThunkDispatch } from "@/store/user/types";
13 | import { ServerError } from "@/common/api";
14 | import { useHistory, Link } from "react-router-dom";
15 | import { Row, Col, Container } from "react-bootstrap";
16 |
17 | const defaultValues: UserLoginPost = {
18 | username: "",
19 | password: "",
20 | };
21 |
22 | export const LoginForm = () => {
23 | const [isLoading, setIsLoading] = useState(false);
24 | const history = useHistory();
25 | const { addToast } = useToasts();
26 | const dispatch = useDispatch();
27 |
28 | const handleSubmit = async (
29 | values: UserLoginPost,
30 | actions: FormikHelpers,
31 | ) => {
32 | try {
33 | setIsLoading(true);
34 | await dispatch(login(values));
35 |
36 | // Get user's notes
37 | await dispatch(all({ sort: "", order: "desc", page: 0, limit: 1000 }));
38 |
39 | // Clear inputs
40 | actions.resetForm();
41 |
42 | // Push to dashboard
43 | history.push("/dashboard");
44 | } catch (error) {
45 | const e = error as ServerError;
46 | if (e && (e.error || e.errors)) {
47 | //
48 | }
49 |
50 | addToast("Hmm, those details don't seem right. Please try again.", {
51 | appearance: "error",
52 | });
53 | } finally {
54 | setIsLoading(false);
55 | }
56 | };
57 |
58 | return (
59 | <>
60 |
61 |
66 |
67 |
68 |
69 | Login
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Forgot Password {" | "}
102 | Signup
103 |
104 |
105 |
106 | >
107 | );
108 | };
109 |
--------------------------------------------------------------------------------
/src/components/user/forms/ResetForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | SubmitButton,
6 | } from "@/components/partials/forms/inputs/Inputs";
7 | import { useToasts } from "react-toast-notifications";
8 | import { useDispatch } from "react-redux";
9 | import { reset } from "@/store/user/actions-api";
10 | import {
11 | UserResetPost,
12 | UserResetPostWithPasswordConfirm,
13 | UserResetPostWithPasswordConfirmValidation,
14 | } from "@/store/user/api-types";
15 | import { UserThunkDispatch } from "@/store/user/types";
16 | import { ServerError } from "@/common/api";
17 | import { useHistory, useLocation } from "react-router-dom";
18 | import { Row, Col, Container } from "react-bootstrap";
19 |
20 | function useQuery() {
21 | return new URLSearchParams(useLocation().search);
22 | }
23 |
24 | const initialDefaultValues: UserResetPostWithPasswordConfirm = {
25 | passwordResetToken: "",
26 | email: "",
27 | password: "",
28 | passwordConfirm: "",
29 | };
30 |
31 | export const ResetForm = () => {
32 | const [isLoading, setIsLoading] = useState(false);
33 | const history = useHistory();
34 | const { addToast } = useToasts();
35 | const dispatch = useDispatch();
36 | const query = useQuery();
37 |
38 | const [defaultValues, setDefaultValues] = useState<
39 | UserResetPostWithPasswordConfirm
40 | >(initialDefaultValues);
41 |
42 | const passwordResetToken = query.get("passwordResetToken");
43 | const email = query.get("email")?.replace(" ", "+");
44 |
45 | useEffect(() => {
46 | if (passwordResetToken != null && email != null) {
47 | setDefaultValues({
48 | passwordResetToken,
49 | email,
50 | password: "",
51 | passwordConfirm: "",
52 | });
53 | }
54 | }, [email, passwordResetToken]);
55 |
56 | const handleSubmit = async (
57 | values: UserResetPostWithPasswordConfirm,
58 | actions: FormikHelpers,
59 | ) => {
60 | try {
61 | setIsLoading(true);
62 | const { passwordConfirm, ...valuesNoPasswordConfirm } = values;
63 | const convertedValues: UserResetPost = { ...valuesNoPasswordConfirm };
64 | await dispatch(reset(convertedValues));
65 |
66 | // Clear inputs
67 | actions.resetForm();
68 |
69 | // Push to dashboard
70 | history.push("/");
71 |
72 | addToast("Password Reset. Please login.", { appearance: "success" });
73 | } catch (error) {
74 | const e = error as ServerError;
75 | if (e && (e.error || e.errors)) {
76 | //
77 | }
78 |
79 | addToast(
80 | "Hmm, your reset link is no longer valid. Please create a new reset link and try again.",
81 | {
82 | appearance: "error",
83 | },
84 | );
85 | } finally {
86 | setIsLoading(false);
87 | }
88 | };
89 |
90 | return (
91 | <>
92 |
93 |
99 |
100 |
101 |
102 | Reset
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
119 |
120 |
121 |
122 |
123 |
124 |
130 |
131 |
132 |
133 |
134 |
135 | >
136 | );
137 | };
138 |
--------------------------------------------------------------------------------
/src/components/user/forms/SignupForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Formik, Form, FormikHelpers } from "formik";
3 | import {
4 | TextInput,
5 | SubmitButton,
6 | } from "@/components/partials/forms/inputs/Inputs";
7 | import { useToasts } from "react-toast-notifications";
8 | import { useDispatch } from "react-redux";
9 | import { signup } from "@/store/user/actions-api";
10 | import {
11 | UserSignupPost,
12 | UserSignupPostWithPasswordConfirm,
13 | UserSignupPostWithPasswordConfirmValidation,
14 | } from "@/store/user/api-types";
15 | import { UserThunkDispatch } from "@/store/user/types";
16 | import { ServerError } from "@/common/api";
17 | import { useHistory, Link } from "react-router-dom";
18 | import { Row, Col, Container } from "react-bootstrap";
19 | import { gaEvent } from "@/common/ga-event";
20 |
21 | const defaultValues: UserSignupPostWithPasswordConfirm = {
22 | firstName: "",
23 | lastName: "",
24 | username: "",
25 | email: "",
26 | password: "",
27 | passwordConfirm: "",
28 | };
29 |
30 | export const SignupForm = () => {
31 | const [isLoading, setIsLoading] = useState(false);
32 | const history = useHistory();
33 | const { addToast } = useToasts();
34 | const dispatch = useDispatch();
35 |
36 | const handleSubmit = async (
37 | values: UserSignupPostWithPasswordConfirm,
38 | actions: FormikHelpers,
39 | ) => {
40 | try {
41 | setIsLoading(true);
42 | const { passwordConfirm, ...valuesNoPasswordConfirm } = values;
43 | const convertedValues: UserSignupPost = { ...valuesNoPasswordConfirm };
44 | await dispatch(signup(convertedValues));
45 |
46 | // Clear inputs
47 | actions.resetForm();
48 |
49 | // Fire gaEvent
50 | gaEvent("User Signup", "Submit", "Success");
51 |
52 | // Push home
53 | history.push("/");
54 |
55 | addToast("Account created. Please login.", { appearance: "success" });
56 | } catch (error) {
57 | const e = error as ServerError;
58 | if (e && (e.error || e.errors)) {
59 | if (e.error.message === "DUPLICATE_USERNAME") {
60 | addToast("That username is taken. Please try again.", {
61 | appearance: "error",
62 | });
63 | }
64 | if (e.error.message === "DUPLICATE_EMAIL") {
65 | addToast("That email is taken. Please try again.", {
66 | appearance: "error",
67 | });
68 | }
69 | } else {
70 | addToast("Hmm, those details don't seem right. Please try again.", {
71 | appearance: "error",
72 | });
73 | }
74 | } finally {
75 | setIsLoading(false);
76 | }
77 | };
78 |
79 | return (
80 | <>
81 |
82 |
87 |
88 |
89 |
90 | Signup
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
131 |
132 |
133 |
134 |
135 |
136 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | Login {" | "}
151 | Forgot Password
152 |
153 |
154 |
155 | >
156 | );
157 | };
158 |
--------------------------------------------------------------------------------
/src/data/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/data/.gitkeep
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import { ToastProvider } from "react-toast-notifications";
5 | import ErrorBoundary from "react-error-boundary";
6 | import { ErrorBoundaryComponent } from "@/components/layouts/main/ErrorBoundary";
7 | import { App } from "./App";
8 | import { configureStore } from "./store";
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById("root"),
19 | );
20 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
--------------------------------------------------------------------------------
/src/router/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/router/.gitkeep
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // testing-library cheatsheet
2 | // https://testing-library.com/docs/react-testing-library/cheatsheet
3 |
4 | import "@testing-library/jest-dom/extend-expect";
5 |
--------------------------------------------------------------------------------
/src/store/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/src/store/.gitkeep
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, Action } from "redux";
2 | import thunkMiddleware, { ThunkAction, ThunkDispatch } from "redux-thunk";
3 | import { composeWithDevTools } from "redux-devtools-extension";
4 | import { userReducer } from "@/store/user/reducers";
5 | import { noteReducer } from "./note/reducers";
6 |
7 | export const rootReducer = combineReducers({
8 | user: userReducer,
9 | note: noteReducer,
10 | });
11 |
12 | export type AppState = ReturnType;
13 |
14 | export const configureStore = () => {
15 | const middlewares = [thunkMiddleware];
16 | const middleWareEnhancer = applyMiddleware(...middlewares);
17 |
18 | const store = createStore(
19 | rootReducer,
20 | composeWithDevTools(middleWareEnhancer),
21 | );
22 |
23 | return store;
24 | };
25 |
26 | export type RootState = ReturnType;
27 |
28 | // Async thunk helpers
29 | // https://github.com/reduxjs/redux-thunk/issues/213#issuecomment-603392173
30 |
31 | export type ThunkResult = ThunkAction;
32 | export type GeneralThunkDispatch = ThunkDispatch;
33 |
--------------------------------------------------------------------------------
/src/store/note/actions-api.ts:
--------------------------------------------------------------------------------
1 | import { parseAxiosError } from "@/common/api";
2 | import {
3 | addNotes,
4 | addNoteToStack,
5 | editNoteInStack,
6 | deleteNoteFromStack,
7 | } from "@/store/note/actions-store";
8 | import { NoteCreatePost } from "@/store/note/api-types";
9 | import { Note, NotesQuery } from "@/store/note/types";
10 | import {
11 | all as noteAll,
12 | create as noteCreate,
13 | find as noteFind,
14 | update as noteUpdate,
15 | del as noteDel,
16 | } from "@/store/note/api-requests";
17 | import { ThunkResult, GeneralThunkDispatch } from "@/store";
18 |
19 | export const all = (data: NotesQuery): ThunkResult> => async (
20 | dispatch: GeneralThunkDispatch,
21 | ) => {
22 | try {
23 | const result = await noteAll(data);
24 | dispatch(addNotes(result));
25 | return result;
26 | } catch (error) {
27 | return Promise.reject(parseAxiosError(error));
28 | }
29 | };
30 |
31 | export const create = (
32 | data: NoteCreatePost,
33 | ): ThunkResult> => async (dispatch: GeneralThunkDispatch) => {
34 | try {
35 | const result = await noteCreate(data);
36 | dispatch(addNoteToStack(result));
37 | return result;
38 | } catch (error) {
39 | return Promise.reject(parseAxiosError(error));
40 | }
41 | };
42 |
43 | export const find = (data: number): ThunkResult> => async () => {
44 | try {
45 | const result = await noteFind(data);
46 | return result;
47 | } catch (error) {
48 | return Promise.reject(parseAxiosError(error));
49 | }
50 | };
51 |
52 | export const update = (data: Note): ThunkResult> => async (
53 | dispatch: GeneralThunkDispatch,
54 | ) => {
55 | try {
56 | const result = await noteUpdate(data);
57 | dispatch(editNoteInStack(result));
58 | return result;
59 | } catch (error) {
60 | return Promise.reject(parseAxiosError(error));
61 | }
62 | };
63 |
64 | export const del = (data: Note): ThunkResult> => async (
65 | dispatch: GeneralThunkDispatch,
66 | ) => {
67 | try {
68 | const result = await noteDel(data);
69 | dispatch(deleteNoteFromStack(result));
70 | return result;
71 | } catch (error) {
72 | return Promise.reject(parseAxiosError(error));
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/src/store/note/actions-store.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Note,
3 | NotesQuery,
4 | SET_NOTES,
5 | ADD_NOTES,
6 | ADD_NOTE_TO_STACK,
7 | DELETE_NOTE_FROM_STACK,
8 | EDIT_NOTE_IN_STACK,
9 | CLEAR_NOTES,
10 | SET_OK_TO_LOAD_MORE,
11 | SET_QUERY,
12 | } from "@/store/note/types";
13 |
14 | export const setNotes = (notes: Note[]) => {
15 | return {
16 | type: SET_NOTES,
17 | payload: notes,
18 | };
19 | };
20 |
21 | export const addNotes = (notes: Note[]) => {
22 | return {
23 | type: ADD_NOTES,
24 | payload: notes,
25 | };
26 | };
27 |
28 | export const addNoteToStack = (note: Note) => {
29 | return {
30 | type: ADD_NOTE_TO_STACK,
31 | payload: note,
32 | };
33 | };
34 |
35 | export const deleteNoteFromStack = (note: Note) => {
36 | return {
37 | type: DELETE_NOTE_FROM_STACK,
38 | payload: note,
39 | };
40 | };
41 |
42 | export const editNoteInStack = (note: Note) => {
43 | return {
44 | type: EDIT_NOTE_IN_STACK,
45 | payload: note,
46 | };
47 | };
48 |
49 | export const clearNotes = () => {
50 | return {
51 | type: CLEAR_NOTES,
52 | };
53 | };
54 |
55 | export const setOkToLoadMore = (value: boolean) => {
56 | return {
57 | type: SET_OK_TO_LOAD_MORE,
58 | payload: value,
59 | };
60 | };
61 |
62 | export const setQuery = (value: NotesQuery) => {
63 | return {
64 | type: SET_QUERY,
65 | payload: value,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/src/store/note/api-requests.ts:
--------------------------------------------------------------------------------
1 | import { Note, NotesQuery } from "@/store/note/types";
2 | import axios, { setAuthorizationHeader } from "@/common/axios";
3 | import { AxiosResponse } from "axios";
4 | import { NoteCreatePost, NoteEditPut } from "./api-types";
5 |
6 | const routeMain = "notes";
7 |
8 | export const all = async (data: NotesQuery): Promise => {
9 | setAuthorizationHeader(axios);
10 | const result: AxiosResponse<{
11 | data: { notes: Note[] };
12 | }> = await axios.get(routeMain, { params: data });
13 |
14 | return result.data.data.notes;
15 | };
16 |
17 | export const find = async (data: number): Promise => {
18 | setAuthorizationHeader(axios);
19 | const result: AxiosResponse<{
20 | data: { note: Note };
21 | }> = await axios.get(`${routeMain}/${data}`);
22 |
23 | return result.data.data.note;
24 | };
25 |
26 | export const create = async (data: NoteCreatePost): Promise => {
27 | setAuthorizationHeader(axios);
28 | const result: AxiosResponse<{
29 | data: { note: Note };
30 | }> = await axios.post(routeMain, data);
31 |
32 | return result.data.data.note;
33 | };
34 |
35 | export const update = async (data: Note): Promise => {
36 | const putRequest: NoteEditPut = {
37 | title: data.title,
38 | content: data.content,
39 | };
40 |
41 | setAuthorizationHeader(axios);
42 | const result: AxiosResponse<{
43 | data: { note: Note };
44 | }> = await axios.put(`${routeMain}/${data.id}`, putRequest);
45 |
46 | return result.data.data.note;
47 | };
48 |
49 | export const del = async (data: Note): Promise => {
50 | setAuthorizationHeader(axios);
51 | await axios.delete(`${routeMain}/${data.id}`);
52 | return data;
53 | };
54 |
--------------------------------------------------------------------------------
/src/store/note/api-types.ts:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export type NoteCreatePost = {
4 | title: string;
5 | content: string;
6 | };
7 |
8 | export type NoteEditPut = {
9 | title: string;
10 | content: string;
11 | };
12 |
13 | export const NoteCreatePostValidation = Yup.object({
14 | title: Yup.string().required("Title is required"),
15 | content: Yup.string().required("Content is required"),
16 | });
17 |
--------------------------------------------------------------------------------
/src/store/note/reducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NoteState,
3 | SET_NOTES,
4 | ADD_NOTES,
5 | ADD_NOTE_TO_STACK,
6 | DELETE_NOTE_FROM_STACK,
7 | EDIT_NOTE_IN_STACK,
8 | CLEAR_NOTES,
9 | SET_OK_TO_LOAD_MORE,
10 | SET_QUERY,
11 | NoteActionTypes,
12 | } from "./types";
13 |
14 | const initialState: NoteState = {
15 | notes: [],
16 | okToLoadMore: false,
17 | query: {
18 | sort: "",
19 | order: "desc" as const,
20 | page: 0,
21 | limit: 20,
22 | },
23 | };
24 |
25 | export const noteReducer = (
26 | state = initialState,
27 | action: NoteActionTypes,
28 | ): NoteState => {
29 | switch (action.type) {
30 | case SET_NOTES:
31 | return {
32 | ...state,
33 | notes: action.payload,
34 | };
35 | case ADD_NOTES:
36 | return {
37 | ...state,
38 | notes: [...state.notes, ...action.payload],
39 | };
40 | case ADD_NOTE_TO_STACK:
41 | return {
42 | ...state,
43 | notes: [action.payload, ...state.notes],
44 | };
45 | case DELETE_NOTE_FROM_STACK: {
46 | const index = state.notes
47 | .map((note) => note.id)
48 | .indexOf(action.payload.id);
49 | return {
50 | ...state,
51 | notes: [
52 | ...state.notes.slice(0, index),
53 | ...state.notes.slice(index + 1),
54 | ],
55 | };
56 | }
57 | case EDIT_NOTE_IN_STACK: {
58 | const index = state.notes
59 | .map((note) => note.id)
60 | .indexOf(action.payload.id);
61 | if (index === -1) return { ...state }; // Only update if found
62 | return {
63 | ...state,
64 | notes: [
65 | ...state.notes.slice(0, index),
66 | // The next line would be {...note, title: action.payload} if we were
67 | // just updating 1 property on the object
68 | action.payload,
69 | ...state.notes.slice(index + 1),
70 | ],
71 | };
72 | }
73 | case CLEAR_NOTES:
74 | return initialState;
75 | case SET_OK_TO_LOAD_MORE:
76 | return {
77 | ...state,
78 | okToLoadMore: action.payload,
79 | };
80 | case SET_QUERY:
81 | return {
82 | ...state,
83 | query: action.payload,
84 | };
85 | default:
86 | return state;
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/src/store/note/types.ts:
--------------------------------------------------------------------------------
1 | import { ThunkDispatch, ThunkAction } from "redux-thunk";
2 | import { RootState } from "@/store";
3 |
4 | export type Note = {
5 | id: number;
6 | userId: number;
7 | title: string;
8 | content: string;
9 | ipAddress?: string;
10 | updatedAt?: string;
11 | createdAt: string;
12 | };
13 |
14 | export type NotesQuery = {
15 | sort: string;
16 | order: "desc" | "asc";
17 | page: number;
18 | limit: number;
19 | };
20 |
21 | export type NoteState = {
22 | notes: Note[];
23 | okToLoadMore: boolean;
24 | query: NotesQuery;
25 | };
26 |
27 | export const SET_NOTES = "SET_NOTES";
28 | export const ADD_NOTES = "ADD_NOTES";
29 | export const ADD_NOTE_TO_STACK = "ADD_NOTE_TO_STACK";
30 | export const DELETE_NOTE_FROM_STACK = "DELETE_NOTE_FROM_STACK";
31 | export const EDIT_NOTE_IN_STACK = "EDIT_NOTE_IN_STACK";
32 | export const CLEAR_NOTES = "CLEAR_NOTES";
33 | export const SET_OK_TO_LOAD_MORE = "SET_OK_TO_LOAD_MORE";
34 | export const SET_QUERY = "SET_QUERY";
35 |
36 | type SetNotesAction = {
37 | type: typeof SET_NOTES;
38 | payload: Note[];
39 | };
40 |
41 | type AddNotesAction = {
42 | type: typeof ADD_NOTES;
43 | payload: Note[];
44 | };
45 |
46 | type AddNoteToStackAction = {
47 | type: typeof ADD_NOTE_TO_STACK;
48 | payload: Note;
49 | };
50 |
51 | type DeleteNoteFromStackAction = {
52 | type: typeof DELETE_NOTE_FROM_STACK;
53 | payload: Note;
54 | };
55 |
56 | type EditNoteInStackAction = {
57 | type: typeof EDIT_NOTE_IN_STACK;
58 | payload: Note;
59 | };
60 |
61 | type ClearNotesAction = {
62 | type: typeof CLEAR_NOTES;
63 | };
64 |
65 | type SetOkToLoadMore = {
66 | type: typeof SET_OK_TO_LOAD_MORE;
67 | payload: boolean;
68 | };
69 |
70 | type SetQuery = {
71 | type: typeof SET_QUERY;
72 | payload: NotesQuery;
73 | };
74 |
75 | export type NoteActionTypes =
76 | | SetNotesAction
77 | | AddNotesAction
78 | | AddNoteToStackAction
79 | | DeleteNoteFromStackAction
80 | | EditNoteInStackAction
81 | | ClearNotesAction
82 | | SetOkToLoadMore
83 | | SetQuery;
84 |
85 | export type NoteThunkDispatch = ThunkDispatch<
86 | RootState,
87 | undefined,
88 | NoteActionTypes
89 | >;
90 |
91 | export type NoteThunkResult = ThunkAction<
92 | Result,
93 | RootState,
94 | undefined,
95 | NoteActionTypes
96 | >;
97 |
--------------------------------------------------------------------------------
/src/store/user/actions-api.ts:
--------------------------------------------------------------------------------
1 | import jwtDecode from "jwt-decode";
2 | import { parseAxiosError } from "@/common/api";
3 | import {
4 | login as userLogin,
5 | signup as userSignup,
6 | forgot as userForgot,
7 | reset as userReset,
8 | } from "@/store/user/api-requests";
9 | import {
10 | UserLoginPost,
11 | UserSignupPost,
12 | UserForgotPost,
13 | UserResetPost,
14 | } from "@/store/user/api-types";
15 | import { UserTokens, JwtDecodeData } from "@/store/user/types";
16 | import { setUser, clearUser } from "@/store/user/actions-store";
17 | import { ThunkResult, GeneralThunkDispatch } from "..";
18 |
19 | export const login = (
20 | data: UserLoginPost,
21 | ): ThunkResult> => async (
22 | dispatch: GeneralThunkDispatch,
23 | ) => {
24 | try {
25 | // Authenticate user using creds
26 | const result = await userLogin(data);
27 |
28 | // Take the accessToken and decode it, giving the user
29 | const decoded: JwtDecodeData = jwtDecode(result.accessToken);
30 | dispatch(setUser(decoded.data));
31 |
32 | // Store the accessTokena and refreshToken in localStorage
33 | localStorage.setItem("accessToken", result.accessToken);
34 | localStorage.setItem("refreshToken", result.refreshToken);
35 |
36 | return result;
37 | } catch (error) {
38 | return Promise.reject(parseAxiosError(error));
39 | }
40 | };
41 |
42 | export const signup = (
43 | data: UserSignupPost,
44 | ): ThunkResult> => async () => {
45 | try {
46 | const result = userSignup(data);
47 | return result;
48 | } catch (error) {
49 | return Promise.reject(parseAxiosError(error));
50 | }
51 | };
52 |
53 | export const forgot = (
54 | data: UserForgotPost,
55 | ): ThunkResult> => async () => {
56 | try {
57 | const result = userForgot(data);
58 | return result;
59 | } catch (error) {
60 | return Promise.reject(parseAxiosError(error));
61 | }
62 | };
63 |
64 | export const reset = (
65 | data: UserResetPost,
66 | ): ThunkResult> => async (
67 | dispatch: GeneralThunkDispatch, // eslint-disable-line no-unused-vars
68 | ) => {
69 | try {
70 | userReset(data);
71 | return Promise.resolve();
72 | } catch (error) {
73 | return Promise.reject(parseAxiosError(error));
74 | }
75 | };
76 |
77 | export const logout = (): ThunkResult> => async (
78 | dispatch: GeneralThunkDispatch,
79 | ) => {
80 | dispatch(clearUser());
81 | localStorage.removeItem("accessToken");
82 | localStorage.removeItem("refreshToken");
83 | return Promise.resolve();
84 | };
85 |
--------------------------------------------------------------------------------
/src/store/user/actions-store.ts:
--------------------------------------------------------------------------------
1 | import { UserShort, SET_USER, CLEAR_USER } from "./types";
2 |
3 | export const setUser = (user: UserShort) => {
4 | return {
5 | type: SET_USER,
6 | payload: user,
7 | };
8 | };
9 |
10 | export const clearUser = () => {
11 | return {
12 | type: CLEAR_USER,
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/user/api-requests.ts:
--------------------------------------------------------------------------------
1 | import axios from "@/common/axios";
2 | import { AxiosResponse } from "axios";
3 | import {
4 | UserLoginPost,
5 | UserSignupPost,
6 | UserForgotPost,
7 | UserResetPost,
8 | } from "./api-types";
9 | import { UserTokens } from "./types";
10 |
11 | const routeLogin = "user/authenticate";
12 | const routeSignup = "user/signup";
13 | const routeForgot = "user/forgot";
14 | const routerReset = "user/reset";
15 |
16 | export const login = async (data: UserLoginPost): Promise => {
17 | const result: AxiosResponse<{
18 | data: {
19 | accessToken: UserTokens["accessToken"];
20 | refreshToken: UserTokens["refreshToken"];
21 | };
22 | }> = await axios.post(routeLogin, data);
23 |
24 | return result.data.data;
25 | };
26 |
27 | export const signup = async (data: UserSignupPost): Promise => {
28 | const result: AxiosResponse<{
29 | data: { id: number };
30 | }> = await axios.post(routeSignup, data);
31 | return result.data.data.id;
32 | };
33 |
34 | export const forgot = async (data: UserForgotPost): Promise => {
35 | const result: AxiosResponse<{
36 | data: { passwordResetToken: string };
37 | }> = await axios.post(routeForgot, data);
38 | return result.data.data.passwordResetToken;
39 | };
40 |
41 | export const reset = async (data: UserResetPost): Promise => {
42 | await axios.post(routerReset, data);
43 | return Promise.resolve();
44 | };
45 |
--------------------------------------------------------------------------------
/src/store/user/api-types.ts:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export type UserLoginPost = {
4 | username: string;
5 | password: string;
6 | };
7 |
8 | export type UserSignupPost = {
9 | firstName: string;
10 | lastName: string;
11 | username: string;
12 | email: string;
13 | password: string;
14 | };
15 |
16 | export type UserSignupPostWithPasswordConfirm = UserSignupPost & {
17 | passwordConfirm: string;
18 | };
19 |
20 | export type UserForgotPost = {
21 | email: string;
22 | url: string;
23 | type: "web";
24 | };
25 |
26 | export type UserResetPost = {
27 | passwordResetToken: string;
28 | email: string;
29 | password: string;
30 | };
31 |
32 | export type UserResetPostWithPasswordConfirm = UserResetPost & {
33 | passwordConfirm: string;
34 | };
35 |
36 | //
37 |
38 | export const UserLoginPostValidation = Yup.object({
39 | username: Yup.string().required("Username is required"),
40 | password: Yup.string()
41 | .required("Password is required")
42 | .min(8, "Password must be at least 6 characters"),
43 | });
44 |
45 | export const UserSignupPostWithPasswordConfirmValidation = Yup.object({
46 | firstName: Yup.string().required("First Name is required"),
47 | lastName: Yup.string().required("Last Name is required"),
48 | username: Yup.string().required("Username is required"),
49 | email: Yup.string().required("Email is required").email(),
50 | password: Yup.string()
51 | .required("Password is required")
52 | .min(8, "Password must be at least 6 characters"),
53 | passwordConfirm: Yup.string()
54 | .oneOf([Yup.ref("password"), null], "Passwords must match")
55 | .required("Password confirm is required"),
56 | });
57 |
58 | export const UserForgotPostValidation = Yup.object({
59 | email: Yup.string().required("Email is required").email(),
60 | });
61 |
62 | export const UserResetPostWithPasswordConfirmValidation = Yup.object({
63 | passwordResetToken: Yup.string().required("Required"),
64 | email: Yup.string().required("Email is required").email(),
65 | password: Yup.string()
66 | .required("Password is required")
67 | .min(8, "Password must be at least 6 characters"),
68 | passwordConfirm: Yup.string()
69 | .oneOf([Yup.ref("password"), null], "Passwords must match")
70 | .required("Password confirm is required"),
71 | });
72 |
--------------------------------------------------------------------------------
/src/store/user/reducers.ts:
--------------------------------------------------------------------------------
1 | import { UserState, SET_USER, UserActionTypes, CLEAR_USER } from "./types";
2 |
3 | const initialState: UserState = {
4 | user: {
5 | id: 0,
6 | token: "",
7 | username: "",
8 | email: "",
9 | },
10 | };
11 |
12 | export const userReducer = (
13 | state = initialState,
14 | action: UserActionTypes,
15 | ): UserState => {
16 | switch (action.type) {
17 | case SET_USER:
18 | return {
19 | ...state,
20 | user: action.payload,
21 | };
22 | case CLEAR_USER:
23 | return initialState;
24 | default:
25 | return state;
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/store/user/types.ts:
--------------------------------------------------------------------------------
1 | import { ThunkDispatch, ThunkAction } from "redux-thunk";
2 | import { RootState } from "@/store";
3 |
4 | export type UserTokens = {
5 | accessToken: string;
6 | refreshToken: string;
7 | };
8 |
9 | export type UserShort = {
10 | id: number;
11 | token: string;
12 | username: string;
13 | email: string;
14 | };
15 |
16 | export type UserState = {
17 | user: UserShort;
18 | };
19 |
20 | export const SET_USER = "SET_USER";
21 | export const CLEAR_USER = "CLEAR_USER";
22 |
23 | type SetUserAction = {
24 | type: typeof SET_USER;
25 | payload: UserShort;
26 | };
27 |
28 | type ClearUser = {
29 | type: typeof CLEAR_USER;
30 | };
31 |
32 | export type UserActionTypes = SetUserAction | ClearUser;
33 |
34 | export type JwtDecodeData = {
35 | data: UserShort;
36 | iat: number;
37 | exp: number;
38 | };
39 |
40 | export type UserThunkDispatch = ThunkDispatch<
41 | RootState,
42 | undefined,
43 | UserActionTypes
44 | >;
45 |
46 | export type UserThunkResult = ThunkAction<
47 | Result,
48 | RootState,
49 | undefined,
50 | UserActionTypes
51 | >;
52 |
--------------------------------------------------------------------------------
/src/stories/Footer.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withKnobs } from "@storybook/addon-knobs";
3 | import { Footer } from "../components/partials/main/Footer";
4 |
5 | // eslint-disable-next-line import/no-default-export
6 | export default {
7 | title: "Footer",
8 | decorators: [withKnobs],
9 | };
10 |
11 | export const MainFooter = () => {
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/typings/example-custom-typings.ts:
--------------------------------------------------------------------------------
1 | declare module "react-bootstrap" {
2 | export function hello(world: string): void;
3 | }
4 |
5 | export {};
6 |
--------------------------------------------------------------------------------
/storybook-static/_headers:
--------------------------------------------------------------------------------
1 | # All paths
2 | /*
3 | #
4 | Content-Security-Policy: frame-ancestors 'self'
5 | #
6 | X-Frame-Options: DENY
7 | #
8 | X-Content-Type-Options: nosniff
9 | #
10 | X-XSS-Protection: 1; mode=block
11 | #
12 | Strict-Transport-Security: max-age=31536000; includeSubDomains
13 |
--------------------------------------------------------------------------------
/storybook-static/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.js": "./main.7852663b061ecb81fcb8.bundle.js",
4 | "main.js.map": "./main.7852663b061ecb81fcb8.bundle.js.map",
5 | "runtime~main.js": "./runtime~main.7852663b061ecb81fcb8.bundle.js",
6 | "runtime~main.js.map": "./runtime~main.7852663b061ecb81fcb8.bundle.js.map",
7 | "vendors~main.js": "./vendors~main.7852663b061ecb81fcb8.bundle.js",
8 | "vendors~main.js.map": "./vendors~main.7852663b061ecb81fcb8.bundle.js.map",
9 | "iframe.html": "./iframe.html",
10 | "precache-manifest.f0615d9b410374f324e50434eb805ae0.js": "./precache-manifest.f0615d9b410374f324e50434eb805ae0.js",
11 | "service-worker.js": "./service-worker.js",
12 | "vendors~main.7852663b061ecb81fcb8.bundle.js.LICENSE.txt": "./vendors~main.7852663b061ecb81fcb8.bundle.js.LICENSE.txt"
13 | },
14 | "entrypoints": [
15 | "runtime~main.7852663b061ecb81fcb8.bundle.js",
16 | "vendors~main.7852663b061ecb81fcb8.bundle.js",
17 | "main.7852663b061ecb81fcb8.bundle.js"
18 | ]
19 | }
--------------------------------------------------------------------------------
/storybook-static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/favicon.ico
--------------------------------------------------------------------------------
/storybook-static/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/.gitkeep
--------------------------------------------------------------------------------
/storybook-static/fonts/Quicksand/Quicksand-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/Quicksand/Quicksand-Bold.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/Quicksand/Quicksand-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/Quicksand/Quicksand-Light.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/Quicksand/Quicksand-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/Quicksand/Quicksand-Medium.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/Quicksand/Quicksand-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/Quicksand/Quicksand-Regular.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/Quicksand/Quicksand-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/Quicksand/Quicksand-SemiBold.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-Black.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-BlackItalic.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-Bold.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-BoldItalic.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-ExtraLight.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-Italic.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-Light.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-LightItalic.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-Regular.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-SemiBold.ttf
--------------------------------------------------------------------------------
/storybook-static/fonts/SourceSansPro/SourceSansPro-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/fonts/SourceSansPro/SourceSansPro-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/storybook-static/iframe.html:
--------------------------------------------------------------------------------
1 | Storybook No Preview Sorry, but you either have no stories or none are selected somehow.
Please check the Storybook config. Try reloading the page. If the problem persists, check the browser console, or the terminal you've run Storybook from.
--------------------------------------------------------------------------------
/storybook-static/index.html:
--------------------------------------------------------------------------------
1 | Storybook
--------------------------------------------------------------------------------
/storybook-static/koa-react-notes-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johndatserakis/koa-react-notes-web/16036b6f5d8cf270c8ed72cd00377f7ed169ced9/storybook-static/koa-react-notes-icon.png
--------------------------------------------------------------------------------
/storybook-static/main.7852663b061ecb81fcb8.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"main.7852663b061ecb81fcb8.bundle.js","sources":["webpack:///main.7852663b061ecb81fcb8.bundle.js"],"mappings":"AAAA","sourceRoot":""}
--------------------------------------------------------------------------------
/storybook-static/main.a7e89035c30c3711e235.bundle.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{501:function(n,o,p){p(502),p(645),p(1403),n.exports=p(1463)},564:function(n,o){}},[[501,1,2]]]);
--------------------------------------------------------------------------------
/storybook-static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Koa-React-Notes",
3 | "name": "Koa-React-Notes",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "32x32",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "koa-react-notes-icon.png",
12 | "type": "image/png",
13 | "sizes": "200x200"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/storybook-static/precache-manifest.f0615d9b410374f324e50434eb805ae0.js:
--------------------------------------------------------------------------------
1 | self.__precacheManifest = (self.__precacheManifest || []).concat([
2 | {
3 | "revision": "26d8a9cd253621746ad092c1c805f5a1",
4 | "url": "iframe.html"
5 | },
6 | {
7 | "url": "main.7852663b061ecb81fcb8.bundle.js"
8 | },
9 | {
10 | "url": "runtime~main.7852663b061ecb81fcb8.bundle.js"
11 | },
12 | {
13 | "url": "vendors~main.7852663b061ecb81fcb8.bundle.js"
14 | },
15 | {
16 | "url": "vendors~main.7852663b061ecb81fcb8.bundle.js.LICENSE.txt"
17 | }
18 | ]);
--------------------------------------------------------------------------------
/storybook-static/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/storybook-static/runtime~main.72c61344ec336373a1bf.bundle.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c
40 | *
41 | * Copyright (c) 2014-2017, Jon Schlinkert.
42 | * Released under the MIT License.
43 | */
44 |
45 | /**
46 | * @license
47 | * Lodash
48 | * Copyright OpenJS Foundation and other contributors
49 | * Released under MIT license
50 | * Based on Underscore.js 1.8.3
51 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
52 | */
53 |
54 | /** @license React v0.18.0
55 | * scheduler.production.min.js
56 | *
57 | * Copyright (c) Facebook, Inc. and its affiliates.
58 | *
59 | * This source code is licensed under the MIT license found in the
60 | * LICENSE file in the root directory of this source tree.
61 | */
62 |
63 | /** @license React v16.12.0
64 | * react-dom.production.min.js
65 | *
66 | * Copyright (c) Facebook, Inc. and its affiliates.
67 | *
68 | * This source code is licensed under the MIT license found in the
69 | * LICENSE file in the root directory of this source tree.
70 | */
71 |
72 | /** @license React v16.12.0
73 | * react-is.production.min.js
74 | *
75 | * Copyright (c) Facebook, Inc. and its affiliates.
76 | *
77 | * This source code is licensed under the MIT license found in the
78 | * LICENSE file in the root directory of this source tree.
79 | */
80 |
81 | /** @license React v16.12.0
82 | * react.production.min.js
83 | *
84 | * Copyright (c) Facebook, Inc. and its affiliates.
85 | *
86 | * This source code is licensed under the MIT license found in the
87 | * LICENSE file in the root directory of this source tree.
88 | */
89 |
90 | /**!
91 | * @fileOverview Kickass library to create and place poppers near their reference elements.
92 | * @version 1.16.1
93 | * @license
94 | * Copyright (c) 2016 Federico Zivolo and contributors
95 | *
96 | * Permission is hereby granted, free of charge, to any person obtaining a copy
97 | * of this software and associated documentation files (the "Software"), to deal
98 | * in the Software without restriction, including without limitation the rights
99 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
100 | * copies of the Software, and to permit persons to whom the Software is
101 | * furnished to do so, subject to the following conditions:
102 | *
103 | * The above copyright notice and this permission notice shall be included in all
104 | * copies or substantial portions of the Software.
105 | *
106 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
107 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
108 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
109 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
110 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
111 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
112 | * SOFTWARE.
113 | */
114 |
--------------------------------------------------------------------------------
/storybook-static/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 |
14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
15 |
16 | importScripts(
17 | "precache-manifest.f0615d9b410374f324e50434eb805ae0.js"
18 | );
19 |
20 | self.addEventListener('message', (event) => {
21 | if (event.data && event.data.type === 'SKIP_WAITING') {
22 | self.skipWaiting();
23 | }
24 | });
25 |
26 | workbox.core.clientsClaim();
27 |
28 | /**
29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
30 | * requests for URLs in the manifest.
31 | * See https://goo.gl/S9QRab
32 | */
33 | self.__precacheManifest = [].concat(self.__precacheManifest || []);
34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
35 |
36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("./index.html"), {
37 |
38 | blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/],
39 | });
40 |
--------------------------------------------------------------------------------
/storybook-static/vendors~main.7852663b061ecb81fcb8.bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | Copyright (c) 2017 Jed Watson.
9 | Licensed under the MIT License (MIT), see
10 | http://jedwatson.github.io/classnames
11 | */
12 |
13 | /*!
14 | * escape-html
15 | * Copyright(c) 2012-2013 TJ Holowaychuk
16 | * Copyright(c) 2015 Andreas Lubbe
17 | * Copyright(c) 2015 Tiancheng "Timothy" Gu
18 | * MIT Licensed
19 | */
20 |
21 | /*!
22 | * https://github.com/es-shims/es5-shim
23 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License
24 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE
25 | */
26 |
27 | /*!
28 | * https://github.com/paulmillr/es6-shim
29 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com)
30 | * and contributors, MIT License
31 | * es6-shim: v0.35.4
32 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE
33 | * Details and documentation:
34 | * https://github.com/paulmillr/es6-shim/
35 | */
36 |
37 | /*!
38 | * is-plain-object
39 | *
40 | * Copyright (c) 2014-2017, Jon Schlinkert.
41 | * Released under the MIT License.
42 | */
43 |
44 | /*!
45 | * isobject
46 | *
47 | * Copyright (c) 2014-2017, Jon Schlinkert.
48 | * Released under the MIT License.
49 | */
50 |
51 | /** @license React v0.19.1
52 | * scheduler.production.min.js
53 | *
54 | * Copyright (c) Facebook, Inc. and its affiliates.
55 | *
56 | * This source code is licensed under the MIT license found in the
57 | * LICENSE file in the root directory of this source tree.
58 | */
59 |
60 | /** @license React v16.13.1
61 | * react-dom.production.min.js
62 | *
63 | * Copyright (c) Facebook, Inc. and its affiliates.
64 | *
65 | * This source code is licensed under the MIT license found in the
66 | * LICENSE file in the root directory of this source tree.
67 | */
68 |
69 | /** @license React v16.13.1
70 | * react.production.min.js
71 | *
72 | * Copyright (c) Facebook, Inc. and its affiliates.
73 | *
74 | * This source code is licensed under the MIT license found in the
75 | * LICENSE file in the root directory of this source tree.
76 | */
77 |
--------------------------------------------------------------------------------
/storybook-static/vendors~main.7852663b061ecb81fcb8.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"vendors~main.7852663b061ecb81fcb8.bundle.js","sources":["webpack:///vendors~main.7852663b061ecb81fcb8.bundle.js"],"mappings":";AAAA","sourceRoot":""}
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "react",
9 | "lib": [
10 | "dom",
11 | "dom.iterable",
12 | "esnext"
13 | ],
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "noEmit": true,
17 | "noImplicitReturns": true,
18 | "noUnusedLocals": false,
19 | "resolveJsonModule": true,
20 | "skipLibCheck": true,
21 | "strict": true,
22 | "target": "es5",
23 | "typeRoots": [
24 | "./node_modules/@types",
25 | "./typings/**"
26 | ]
27 | },
28 | "extends": "./tsconfig.paths.json",
29 | "include": [
30 | "src",
31 | "craco.config.js"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": [
6 | "src/*"
7 | ]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------