) => {
74 | e.preventDefault();
75 | const product = e.currentTarget.dataset.product;
76 | if (product) {
77 | searchProduct(product);
78 | }
79 | };
80 |
81 | return (
82 |
88 |
94 |
95 |
104 |
113 |
114 |
120 | {isFetching ? (
121 |
122 |
123 |
124 | ) : (
125 | <>
126 | {Object.keys(parsedProducts).map(
127 | (letter: string, index: number) => (
128 |
129 |
130 |
131 | {letter}
132 |
133 | - {parsedProducts[letter].len} results
134 |
135 |
136 |
137 |
138 | {parsedProducts[letter].results.map(
139 | (item: ProductsApiType) => (
140 |
146 |
147 |
148 |
149 | {item.name}
150 |
151 |
152 | {item.desc}
153 |
154 |
155 |
156 |
157 |
158 |
159 | )
160 | )}
161 |
162 |
163 | )
164 | )}
165 | >
166 | )}
167 |
168 |
174 |
175 |
176 | {categories.map((category) => (
177 | -
178 | {category.name}
179 |
180 | ))}
181 |
182 |
183 |
184 |
192 |
193 | );
194 | }
195 |
196 | export default Sidebar;
197 |
--------------------------------------------------------------------------------
/src/components/Sidebar/__tests__/Sidebar.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 |
4 | import { Provider } from "react-redux";
5 | import store from "store";
6 |
7 | import Sidebar from "components/Sidebar";
8 |
9 | describe("Sidebar.tsx", () => {
10 | it("should render component", () => {
11 | render(
12 |
13 |
14 |
15 | );
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/Sidebar/index.ts:
--------------------------------------------------------------------------------
1 | import { compose } from "redux";
2 | import { connect } from "react-redux";
3 |
4 | import { toggleSidebar } from "store/sidebar/actions";
5 | import { searchProduct } from "store/search/actions";
6 |
7 | import { AppState } from "store/rootReducer";
8 |
9 | import Sidebar from "./Sidebar";
10 |
11 | const mapStateToProps = ({ sidebar, api }: AppState) => ({
12 | sidebar,
13 | api,
14 | });
15 |
16 | const mapDispatchToProps = {
17 | toggleSidebar,
18 | searchProduct,
19 | };
20 |
21 | const enhancer = compose(connect(mapStateToProps, mapDispatchToProps));
22 |
23 | export default enhancer(Sidebar);
24 |
--------------------------------------------------------------------------------
/src/containers/Layout/Layout.module.scss:
--------------------------------------------------------------------------------
1 | @import "styles/themes.scss";
2 |
3 | .Layout {
4 | display: flex;
5 | width: 100%;
6 | height: 100%;
7 | position: relative;
8 | }
9 |
10 | .SkipLink {
11 | background: var(--primary);
12 | height: 30px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | left: 50%;
17 | padding: 10px 20px;
18 | position: absolute;
19 | transform: translateY(-100%);
20 | transition: transform 0.3s;
21 | text-align: center;
22 | z-index: 1000;
23 | font-weight: 400;
24 | font-size: 1.4rem;
25 | color: var(--skiplink-text);
26 | }
27 |
28 | .SkipLink:focus {
29 | transform: translateY(0%);
30 | }
31 |
--------------------------------------------------------------------------------
/src/containers/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Sidebar from "components/Sidebar";
4 | import Main from "components/Main";
5 |
6 | import ModalSettings from "components/Modals/ModalSettings";
7 | import ModalObjectInfo from "components/Modals/ModalObjectInfo";
8 |
9 | import styles from "./Layout.module.scss";
10 |
11 | function Layout() {
12 | return (
13 |
14 | {/* Skip links */}
15 |
23 |
24 | {/* Page layout */}
25 |
26 |
27 |
28 | {/* List of modals on page */}
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default Layout;
36 |
--------------------------------------------------------------------------------
/src/containers/Layout/__tests__/Layout.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 |
4 | import { Provider } from "react-redux";
5 | import store from "store";
6 |
7 | import Layout from "containers/Layout";
8 |
9 | describe("Layout.tsx", () => {
10 | it("should render component", () => {
11 | render(
12 |
13 |
14 |
15 | );
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/containers/Layout/index.ts:
--------------------------------------------------------------------------------
1 | import { compose } from "redux";
2 | import { connect } from "react-redux";
3 |
4 | import Layout from "./Layout";
5 |
6 | const mapStateToProps = null;
7 | const mapDispatchToProps = null;
8 |
9 | const enhancer = compose(connect(mapStateToProps, mapDispatchToProps));
10 |
11 | export default enhancer(Layout);
12 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import * as serviceWorker from "utils/serviceWorker";
4 | import Modal from "react-modal";
5 |
6 | import { Provider } from "react-redux";
7 | import store from "store";
8 | import WebFont from "webfontloader";
9 | import "utils/i18n";
10 |
11 | import "normalize.css";
12 | import "styles/index.scss";
13 |
14 | import App from "components/App";
15 |
16 | WebFont.load({
17 | google: {
18 | families: ["Montserrat:400,600,700"],
19 | },
20 | });
21 |
22 | Modal.setAppElement("#root");
23 |
24 | ReactDOM.render(
25 |
26 |
27 |
28 |
29 | ,
30 | document.getElementById("root")
31 | );
32 |
33 | serviceWorker.unregister();
34 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/store/actions.ts:
--------------------------------------------------------------------------------
1 | export type Action = {
2 | type: string;
3 | payload?: P;
4 | };
5 |
6 | export const createAction =
(
7 | type: string,
8 | payload?: P
9 | ): Action => ({
10 | type,
11 | payload,
12 | });
13 |
--------------------------------------------------------------------------------
/src/store/api/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { FETCH_API_REQUEST } from "./constants";
3 |
4 | export const fetchApiData = () => {
5 | return createAction(FETCH_API_REQUEST);
6 | };
7 |
--------------------------------------------------------------------------------
/src/store/api/api.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import {
3 | CategoriesApiType,
4 | ObjectToCategoryApiType,
5 | ProductsApiType,
6 | } from "./reducer";
7 |
8 | export const fetchProductApi = () => {
9 | return new Promise>((resolve) => {
10 | // ... ORDER BY name ASC
11 | const arr = [...db["products"]];
12 | arr.sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0));
13 | resolve(arr);
14 | });
15 | };
16 |
17 | export const fetchProductsAutcompleteApi = (name: string) => {
18 | return new Promise>((resolve) => {
19 | const arr = [...db["products"]];
20 |
21 | let query = name.toLowerCase();
22 | const results = arr.filter((item) => {
23 | return item.name.toLowerCase().indexOf(query) > -1;
24 | });
25 |
26 | resolve(results);
27 | });
28 | };
29 |
30 | export const fetchProductsByObjectId = (objectId: string) => {
31 | return new Promise>((resolve) => {
32 | const arr = [...db["products"]];
33 |
34 | const results = arr.filter((item) => {
35 | return item.objectId === objectId;
36 | });
37 |
38 | resolve(results);
39 | });
40 | };
41 |
42 | export const fetchObjectIdFromObjectCategories = (objectId: string) => {
43 | return new Promise>((resolve) => {
44 | const arr = [...db["object-to-category"]];
45 |
46 | const results = arr.filter((item) => {
47 | return item.objectId === objectId;
48 | });
49 |
50 | resolve(results);
51 | });
52 | };
53 |
54 | export const fetchCategoriesApi = () => {
55 | return new Promise>((resolve) => {
56 | resolve(db["categories"]);
57 | });
58 | };
59 |
60 | export const fetchCategoryIdFromCategories = (categoryId: number) => {
61 | return new Promise>((resolve) => {
62 | const arr = [...db["categories"]];
63 |
64 | const results = arr.filter((item) => {
65 | return item.id === categoryId;
66 | });
67 |
68 | resolve(results);
69 | });
70 | };
71 |
72 | export const fetchObjectToCategoryApi = () => {
73 | return new Promise>((resolve) => {
74 | resolve(db["object-to-category"]);
75 | });
76 | };
77 |
--------------------------------------------------------------------------------
/src/store/api/constants.ts:
--------------------------------------------------------------------------------
1 | export const FETCH_API_REQUEST = "FETCH_API_REQUEST";
2 | export const FETCH_API_SUCCESS = "FETCH_API_SUCCESS";
3 | export const FETCH_API_FAILED = "FETCH_API_FAILED";
4 |
--------------------------------------------------------------------------------
/src/store/api/db.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | categories: [
3 | {
4 | id: 1,
5 | name: "alcohol",
6 | },
7 | {
8 | id: 2,
9 | name: "bakery products",
10 | },
11 | {
12 | id: 3,
13 | name: "groceries",
14 | },
15 | {
16 | id: 4,
17 | name: "teas and coffees",
18 | },
19 | {
20 | id: 5,
21 | name: "sweets",
22 | },
23 | {
24 | id: 6,
25 | name: "mineral water",
26 | },
27 | {
28 | id: 7,
29 | name: "beverages and juices",
30 | },
31 | {
32 | id: 8,
33 | name: "meat and cold cuts",
34 | },
35 | {
36 | id: 9,
37 | name: "cheeses",
38 | },
39 | {
40 | id: 10,
41 | name: "frozen foods",
42 | },
43 | {
44 | id: 11,
45 | name: "savoury snacks",
46 | },
47 | {
48 | id: 12,
49 | name: "fishes",
50 | },
51 | {
52 | id: 13,
53 | name: "honey and dried fruits",
54 | },
55 | {
56 | id: 14,
57 | name: "eggs",
58 | },
59 | {
60 | id: 15,
61 | name: "delicatessen",
62 | },
63 | {
64 | id: 16,
65 | name: "spices",
66 | },
67 | {
68 | id: 17,
69 | name: "preserves",
70 | },
71 | {
72 | id: 18,
73 | name: "ready meals",
74 | },
75 | {
76 | id: 19,
77 | name: "cleaning products",
78 | },
79 | {
80 | id: 20,
81 | name: "animal world",
82 | },
83 | {
84 | id: 21,
85 | name: "ice creams",
86 | },
87 | {
88 | id: 22,
89 | name: "dairy products",
90 | },
91 | {
92 | id: 23,
93 | name: "deals",
94 | },
95 | {
96 | id: 24,
97 | name: "checkout",
98 | },
99 | {
100 | id: 25,
101 | name: "fruits and vegetables",
102 | },
103 | ],
104 | products: [
105 | {
106 | id: 1,
107 | name: "Guinness Draught",
108 | desc: "-",
109 | price: "2.50",
110 | objectId: "o_5",
111 | },
112 | {
113 | id: 2,
114 | name: "Orange juice",
115 | desc: "-",
116 | price: "2.50",
117 | objectId: "o_10",
118 | },
119 | {
120 | id: 3,
121 | name: "Milk",
122 | desc: "-",
123 | price: "2.50",
124 | objectId: "o_8",
125 | },
126 | {
127 | id: 4,
128 | name: "Chocolate",
129 | desc: "-",
130 | price: "2.50",
131 | objectId: "o_25",
132 | },
133 | {
134 | id: 5,
135 | name: "Salmon",
136 | desc: "-",
137 | price: "2.50",
138 | objectId: "o_21",
139 | },
140 | {
141 | id: 6,
142 | name: "Pasta",
143 | desc: "-",
144 | price: "2.50",
145 | objectId: "o_35",
146 | },
147 | {
148 | id: 7,
149 | name: "Eggs",
150 | desc: "-",
151 | price: "2.50",
152 | objectId: "o_18",
153 | },
154 | {
155 | id: 8,
156 | name: "Heineken",
157 | desc: "-",
158 | price: "2.50",
159 | objectId: "o_11",
160 | },
161 | {
162 | id: 9,
163 | name: "Corona Extra",
164 | desc: "-",
165 | price: "2.50",
166 | objectId: "o_11",
167 | },
168 | {
169 | id: 10,
170 | name: "Punk IPA",
171 | desc: "-",
172 | price: "2.50",
173 | objectId: "o_1",
174 | },
175 | {
176 | id: 11,
177 | name: "Bud Light",
178 | desc: "-",
179 | price: "2.50",
180 | objectId: "o_5",
181 | },
182 | {
183 | id: 12,
184 | name: "Traditional Lager",
185 | desc: "-",
186 | price: "2.50",
187 | objectId: "o_5",
188 | },
189 | {
190 | id: 13,
191 | name: "Lay's salted",
192 | desc: "-",
193 | price: "5.99",
194 | objectId: "o_28",
195 | },
196 | {
197 | id: 14,
198 | name: "Pepper",
199 | desc: "-",
200 | price: "2.00",
201 | objectId: "o_15",
202 | },
203 | {
204 | id: 15,
205 | name: "Pierogi",
206 | desc: "True Polish OG food",
207 | price: "9.99",
208 | objectId: "o_24",
209 | },
210 | {
211 | id: 16,
212 | name: "Instant soup",
213 | desc: "-",
214 | price: "2.50",
215 | objectId: "o_14",
216 | },
217 | {
218 | id: 17,
219 | name: "Salt",
220 | desc: "-",
221 | price: "1.00",
222 | objectId: "o_16",
223 | },
224 | {
225 | id: 18,
226 | name: "Coffee",
227 | desc: "-",
228 | price: "15.00",
229 | objectId: "o_33",
230 | },
231 | {
232 | id: 19,
233 | name: "Black tea",
234 | desc: "-",
235 | price: "1.00",
236 | objectId: "o_32",
237 | },
238 | {
239 | id: 20,
240 | name: "Green tea",
241 | desc: "-",
242 | price: "1.00",
243 | objectId: "o_32",
244 | },
245 | {
246 | id: 21,
247 | name: "Tomatoes",
248 | desc: "-",
249 | price: "2.00",
250 | objectId: "o_34",
251 | },
252 | {
253 | id: 22,
254 | name: "Bananas",
255 | desc: "-",
256 | price: "4.99",
257 | objectId: "o_30",
258 | },
259 | {
260 | id: 23,
261 | name: "Apples",
262 | desc: "-",
263 | price: "3.99",
264 | objectId: "o_30",
265 | },
266 | {
267 | id: 24,
268 | name: "Salty sticks",
269 | desc: "-",
270 | price: "3.99",
271 | objectId: "o_28",
272 | },
273 | {
274 | id: 25,
275 | name: "Cream tubes",
276 | desc: "-",
277 | price: "4.50",
278 | objectId: "o_26",
279 | },
280 | {
281 | id: 26,
282 | name: "Waffles",
283 | desc: "-",
284 | price: "4.50",
285 | objectId: "o_27",
286 | },
287 | {
288 | id: 27,
289 | name: "American cookies",
290 | desc: "-",
291 | price: "4.39",
292 | objectId: "o_25",
293 | },
294 | {
295 | id: 28,
296 | name: "Bread",
297 | desc: "-",
298 | price: "3.00",
299 | objectId: "o_38",
300 | },
301 | {
302 | id: 29,
303 | name: "Roll",
304 | desc: "-",
305 | price: "3.29",
306 | objectId: "o_43",
307 | },
308 | {
309 | id: 30,
310 | name: "Corn",
311 | desc: "-",
312 | price: "2.40",
313 | objectId: "o_36",
314 | },
315 | {
316 | id: 31,
317 | name: "Tomato sauce",
318 | desc: "-",
319 | price: "5.19",
320 | objectId: "o_37",
321 | },
322 | {
323 | id: 32,
324 | name: "Gouda cheese",
325 | desc: "-",
326 | price: "5.19",
327 | objectId: "o_13",
328 | },
329 | {
330 | id: 33,
331 | name: "Blue cheese",
332 | desc: "-",
333 | price: "6.00",
334 | objectId: "o_39",
335 | },
336 | {
337 | id: 34,
338 | name: "Ham",
339 | desc: "-",
340 | price: "5.00",
341 | objectId: "o_42",
342 | },
343 | {
344 | id: 35,
345 | name: "Salami",
346 | desc: "-",
347 | price: "3.00",
348 | objectId: "o_41",
349 | },
350 | {
351 | id: 36,
352 | name: "Peanuts",
353 | desc: "-",
354 | price: "6.00",
355 | objectId: "o_22",
356 | },
357 | {
358 | id: 37,
359 | name: "Honey",
360 | desc: "-",
361 | price: "20.00",
362 | objectId: "o_22",
363 | },
364 | {
365 | id: 38,
366 | name: "Jam",
367 | desc: "-",
368 | price: "7.00",
369 | objectId: "o_23",
370 | },
371 | {
372 | id: 39,
373 | name: "Detergent",
374 | desc: "-",
375 | price: "25.00",
376 | objectId: "o_31",
377 | },
378 | {
379 | id: 40,
380 | name: "karma dla psa",
381 | desc: "-",
382 | price: "30.00",
383 | objectId: "o_29",
384 | },
385 | {
386 | id: 41,
387 | name: "Ice cream",
388 | desc: "-",
389 | price: "2.89",
390 | objectId: "o_20",
391 | },
392 | {
393 | id: 42,
394 | name: "Frozen vegetables",
395 | desc: "-",
396 | price: "12.00",
397 | objectId: "o_17",
398 | },
399 | {
400 | id: 43,
401 | name: "Candy box",
402 | desc: "-",
403 | price: "5.00",
404 | objectId: "o_19",
405 | },
406 | {
407 | id: 44,
408 | name: "Natural yogurt",
409 | desc: "-",
410 | price: "3.00",
411 | objectId: "o_7",
412 | },
413 | {
414 | id: 45,
415 | name: "Cottage cheese",
416 | desc: "-",
417 | price: "3.00",
418 | objectId: "o_2",
419 | },
420 | {
421 | id: 46,
422 | name: "Sparkling water",
423 | desc: "-",
424 | price: "1.79",
425 | objectId: "o_4",
426 | },
427 | {
428 | id: 47,
429 | name: "Mineral water",
430 | desc: "-",
431 | price: "2.79",
432 | objectId: "o_9",
433 | },
434 | {
435 | id: 48,
436 | name: "Cola",
437 | desc: "-",
438 | price: "4.79",
439 | objectId: "o_6",
440 | },
441 | {
442 | id: 49,
443 | name: "Orange soda",
444 | desc: "-",
445 | price: "4.79",
446 | objectId: "o_3",
447 | },
448 | {
449 | id: 50,
450 | name: "Vodka",
451 | desc: "-",
452 | price: "25.00",
453 | objectId: "o_12",
454 | },
455 | {
456 | id: 51,
457 | name: "Chewing gums",
458 | desc: "-",
459 | price: "3.00",
460 | objectId: "o_40",
461 | },
462 | ],
463 | "object-to-category": [
464 | {
465 | id: 1,
466 | categoryId: 1,
467 | objectId: "o_1",
468 | },
469 | {
470 | id: 2,
471 | categoryId: 22,
472 | objectId: "o_2",
473 | },
474 | {
475 | id: 3,
476 | categoryId: 7,
477 | objectId: "o_3",
478 | },
479 | {
480 | id: 4,
481 | categoryId: 6,
482 | objectId: "o_4",
483 | },
484 | {
485 | id: 5,
486 | categoryId: 1,
487 | objectId: "o_5",
488 | },
489 | {
490 | id: 6,
491 | categoryId: 7,
492 | objectId: "o_6",
493 | },
494 | {
495 | id: 7,
496 | categoryId: 22,
497 | objectId: "o_7",
498 | },
499 | {
500 | id: 8,
501 | categoryId: 22,
502 | objectId: "o_8",
503 | },
504 | {
505 | id: 9,
506 | categoryId: 6,
507 | objectId: "o_9",
508 | },
509 | {
510 | id: 10,
511 | categoryId: 7,
512 | objectId: "o_10",
513 | },
514 | {
515 | id: 11,
516 | categoryId: 1,
517 | objectId: "o_11",
518 | },
519 | {
520 | id: 12,
521 | categoryId: 1,
522 | objectId: "o_12",
523 | },
524 | {
525 | id: 13,
526 | categoryId: 9,
527 | objectId: "o_13",
528 | },
529 | {
530 | id: 14,
531 | categoryId: 18,
532 | objectId: "o_14",
533 | },
534 | {
535 | id: 15,
536 | categoryId: 16,
537 | objectId: "o_15",
538 | },
539 | {
540 | id: 16,
541 | categoryId: 16,
542 | objectId: "o_16",
543 | },
544 | {
545 | id: 17,
546 | categoryId: 10,
547 | objectId: "o_17",
548 | },
549 | {
550 | id: 18,
551 | categoryId: 14,
552 | objectId: "o_18",
553 | },
554 | {
555 | id: 19,
556 | categoryId: 23,
557 | objectId: "o_19",
558 | },
559 | {
560 | id: 20,
561 | categoryId: 21,
562 | objectId: "o_20",
563 | },
564 | {
565 | id: 21,
566 | categoryId: 12,
567 | objectId: "o_21",
568 | },
569 | {
570 | id: 22,
571 | categoryId: 13,
572 | objectId: "o_22",
573 | },
574 | {
575 | id: 23,
576 | categoryId: 17,
577 | objectId: "o_23",
578 | },
579 | {
580 | id: 24,
581 | categoryId: 15,
582 | objectId: "o_24",
583 | },
584 | {
585 | id: 25,
586 | categoryId: 5,
587 | objectId: "o_25",
588 | },
589 | {
590 | id: 26,
591 | categoryId: 5,
592 | objectId: "o_26",
593 | },
594 | {
595 | id: 27,
596 | categoryId: 5,
597 | objectId: "o_27",
598 | },
599 | {
600 | id: 28,
601 | categoryId: 11,
602 | objectId: "o_28",
603 | },
604 | {
605 | id: 29,
606 | categoryId: 20,
607 | objectId: "o_29",
608 | },
609 | {
610 | id: 30,
611 | categoryId: 25,
612 | objectId: "o_30",
613 | },
614 | {
615 | id: 31,
616 | categoryId: 19,
617 | objectId: "o_31",
618 | },
619 | {
620 | id: 32,
621 | categoryId: 4,
622 | objectId: "o_32",
623 | },
624 | {
625 | id: 33,
626 | categoryId: 4,
627 | objectId: "o_33",
628 | },
629 | {
630 | id: 34,
631 | categoryId: 25,
632 | objectId: "o_34",
633 | },
634 | {
635 | id: 35,
636 | categoryId: 3,
637 | objectId: "o_35",
638 | },
639 | {
640 | id: 36,
641 | categoryId: 3,
642 | objectId: "o_36",
643 | },
644 | {
645 | id: 37,
646 | categoryId: 3,
647 | objectId: "o_37",
648 | },
649 | {
650 | id: 38,
651 | categoryId: 2,
652 | objectId: "o_38",
653 | },
654 | {
655 | id: 39,
656 | categoryId: 9,
657 | objectId: "o_39",
658 | },
659 | {
660 | id: 40,
661 | categoryId: 24,
662 | objectId: "o_40",
663 | },
664 | {
665 | id: 41,
666 | categoryId: 8,
667 | objectId: "o_41",
668 | },
669 | {
670 | id: 42,
671 | categoryId: 8,
672 | objectId: "o_42",
673 | },
674 | {
675 | id: 43,
676 | categoryId: 2,
677 | objectId: "o_43",
678 | },
679 | ],
680 | };
681 |
--------------------------------------------------------------------------------
/src/store/api/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { Action } from "store/actions";
3 |
4 | import {
5 | FETCH_API_REQUEST,
6 | FETCH_API_SUCCESS,
7 | FETCH_API_FAILED,
8 | } from "./constants";
9 |
10 | export type ProductsApiType = {
11 | id: number;
12 | name: string;
13 | price: string;
14 | desc: string;
15 | objectId: string;
16 | };
17 |
18 | export type CategoriesApiType = {
19 | id: number;
20 | name: string;
21 | };
22 |
23 | export type ObjectToCategoryApiType = {
24 | id: number;
25 | categoryId: number;
26 | objectId: string;
27 | };
28 |
29 | export type IState = {
30 | readonly products: Array;
31 | readonly categories: Array;
32 | readonly objectToCategory: Array;
33 | readonly error: Error | null;
34 | readonly isFetching: boolean;
35 | };
36 |
37 | export const initialState: IState = {
38 | products: [],
39 | categories: [],
40 | objectToCategory: [],
41 | isFetching: false,
42 | error: null,
43 | };
44 |
45 | export const api: Reducer = (
46 | state = initialState,
47 | action: Action
48 | ) => {
49 | switch (action.type) {
50 | case FETCH_API_REQUEST:
51 | return {
52 | ...state,
53 | isFetching: true,
54 | error: null,
55 | };
56 | case FETCH_API_SUCCESS:
57 | return {
58 | ...state,
59 | isFetching: false,
60 | products: action.payload.products,
61 | categories: action.payload.categories,
62 | objectToCategory: action.payload.objectToCategory,
63 | };
64 | case FETCH_API_FAILED:
65 | return {
66 | ...state,
67 | isFetching: false,
68 | error: action.payload,
69 | };
70 | default:
71 | return state;
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/src/store/api/sagas.ts:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, all, put } from "redux-saga/effects";
2 |
3 | import {
4 | ProductsApiType,
5 | CategoriesApiType,
6 | ObjectToCategoryApiType,
7 | } from "./reducer";
8 |
9 | import {
10 | fetchCategoriesApi,
11 | fetchObjectToCategoryApi,
12 | fetchProductApi,
13 | } from "./api";
14 |
15 | import {
16 | FETCH_API_REQUEST,
17 | FETCH_API_FAILED,
18 | FETCH_API_SUCCESS,
19 | } from "./constants";
20 |
21 | function* fetchData() {
22 | try {
23 | const productsData: Array = yield call(fetchProductApi);
24 | const categoriesData: Array = yield call(
25 | fetchCategoriesApi
26 | );
27 | const objectToCategoryData: Array = yield call(
28 | fetchObjectToCategoryApi
29 | );
30 |
31 | const payload = {
32 | products: productsData,
33 | categories: categoriesData,
34 | objectToCategory: objectToCategoryData,
35 | };
36 |
37 | yield put({
38 | type: FETCH_API_SUCCESS,
39 | payload,
40 | });
41 | } catch (error) {
42 | yield put({ type: FETCH_API_FAILED, payload: error });
43 | }
44 | }
45 |
46 | export function* apiRootSaga() {
47 | yield all([takeLatest(FETCH_API_REQUEST, fetchData)]);
48 | }
49 |
--------------------------------------------------------------------------------
/src/store/graph/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import {
3 | INIT_GRAPH_REQUEST,
4 | SET_START_VERTEX,
5 | EDIT_START_VERTEX_TOGGLE,
6 | } from "./constants";
7 |
8 | export type Route = {
9 | startVertexKey: string;
10 | endVertexKey: string;
11 | };
12 |
13 | export const initGraph = () => {
14 | return createAction(INIT_GRAPH_REQUEST);
15 | };
16 |
17 | export const setStartVertex = (startVertex: string) => {
18 | return createAction(SET_START_VERTEX, startVertex);
19 | };
20 |
21 | export const toggleEditMode = () => {
22 | return createAction(EDIT_START_VERTEX_TOGGLE);
23 | };
24 |
--------------------------------------------------------------------------------
/src/store/graph/constants.ts:
--------------------------------------------------------------------------------
1 | export const INIT_GRAPH_REQUEST = "INIT_GRAPH_REQUEST";
2 | export const INIT_GRAPH_SUCCESS = "INIT_GRAPH_SUCCESS";
3 | export const INIT_GRAPH_FAILED = "INIT_GRAPH_FAILED";
4 |
5 | export const SET_START_VERTEX = "SET_START_VERTEX";
6 |
7 | export const EDIT_START_VERTEX_TOGGLE = "EDIT_START_VERTEX_TOGGLE";
8 | export const EDIT_START_VERTEX_SUCCESS = "EDIT_START_VERTEX_SUCCESS";
9 |
--------------------------------------------------------------------------------
/src/store/graph/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { Action } from "store/actions";
3 |
4 | import Graph from "algorithms/graph/Graph";
5 |
6 | import {
7 | INIT_GRAPH_REQUEST,
8 | INIT_GRAPH_SUCCESS,
9 | INIT_GRAPH_FAILED,
10 | SET_START_VERTEX,
11 | EDIT_START_VERTEX_TOGGLE,
12 | } from "./constants";
13 |
14 | export type IState = {
15 | readonly graph: Graph | null;
16 | readonly isGenerating: boolean;
17 | readonly startVertex: string;
18 | readonly isEditMode: boolean;
19 | };
20 |
21 | export const initialState: IState = {
22 | graph: null,
23 | isGenerating: false,
24 | isEditMode: false,
25 | startVertex: "v_95",
26 | };
27 |
28 | export const graph: Reducer = (
29 | state = initialState,
30 | action: Action
31 | ) => {
32 | switch (action.type) {
33 | case INIT_GRAPH_REQUEST:
34 | return {
35 | ...state,
36 | isGenerating: true,
37 | };
38 | case INIT_GRAPH_SUCCESS:
39 | return {
40 | ...state,
41 | isGenerating: false,
42 | graph: action.payload,
43 | };
44 | case INIT_GRAPH_FAILED:
45 | return {
46 | ...state,
47 | isGenerating: false,
48 | };
49 | case SET_START_VERTEX:
50 | return {
51 | ...state,
52 | startVertex: action.payload,
53 | isEditMode: false,
54 | };
55 | case EDIT_START_VERTEX_TOGGLE:
56 | return {
57 | ...state,
58 | isEditMode: !state.isEditMode,
59 | };
60 | default:
61 | return state;
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/store/graph/sagas.ts:
--------------------------------------------------------------------------------
1 | import { takeLatest, put, all } from "redux-saga/effects";
2 |
3 | import { mapData } from "components/Map/mapData";
4 | import { getGraphFromJSON } from "algorithms/graph/Utils";
5 |
6 | import {
7 | INIT_GRAPH_REQUEST,
8 | INIT_GRAPH_SUCCESS,
9 | INIT_GRAPH_FAILED,
10 | } from "./constants";
11 |
12 | export function* buildGraph() {
13 | try {
14 | const graph = getGraphFromJSON(mapData);
15 |
16 | yield put({
17 | type: INIT_GRAPH_SUCCESS,
18 | payload: graph,
19 | });
20 | } catch (error) {
21 | yield put({
22 | type: INIT_GRAPH_FAILED,
23 | });
24 | }
25 | }
26 |
27 | export function* graphRootSaga() {
28 | yield all([takeLatest(INIT_GRAPH_REQUEST, buildGraph)]);
29 | }
30 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import createSagaMiddleware from "redux-saga";
3 | import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
4 | import { createBrowserHistory } from "history";
5 | import rootReducer from "./rootReducer";
6 | import rootSaga from "./rootSaga";
7 |
8 | export const history = createBrowserHistory();
9 |
10 | const configureStore = () => {
11 | const initialState = {};
12 | const sagaMiddleware = createSagaMiddleware();
13 | const middleware = [sagaMiddleware];
14 |
15 | const store = createStore(
16 | rootReducer(history),
17 | initialState,
18 | composeWithDevTools(applyMiddleware(...middleware))
19 | );
20 |
21 | sagaMiddleware.run(rootSaga);
22 | return store;
23 | };
24 |
25 | const store = configureStore();
26 |
27 | // Auto-update values after state changes to persist them
28 | store.subscribe(() => {
29 | const { settings } = store.getState();
30 |
31 | localStorage.setItem("lang", settings.lang);
32 | localStorage.setItem("theme", settings.theme);
33 | });
34 |
35 | export default store;
36 |
--------------------------------------------------------------------------------
/src/store/modals/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { OPEN_MODAL, CLOSE_MODAL } from "./constants";
3 |
4 | export type ModalPayload = {
5 | modalName: string;
6 | data?: T;
7 | };
8 |
9 | export const openModal = (data: ModalPayload) => {
10 | return createAction(OPEN_MODAL, data);
11 | };
12 |
13 | export const closeModal = () => {
14 | return createAction(CLOSE_MODAL);
15 | };
16 |
--------------------------------------------------------------------------------
/src/store/modals/constants.ts:
--------------------------------------------------------------------------------
1 | export const MODAL_SETTINGS = "MODAL_SETTINGS";
2 |
3 | export const OPEN_MODAL = "OPEN_MODAL";
4 | export const CLOSE_MODAL = "CLOSE_MODAL";
5 |
--------------------------------------------------------------------------------
/src/store/modals/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { OPEN_MODAL, CLOSE_MODAL } from "./constants";
3 | import { Action } from "store/actions";
4 |
5 | export type IState = {
6 | readonly activeModal: string;
7 | readonly isOpen: boolean;
8 | readonly data: any;
9 | };
10 |
11 | export const initialState: IState = {
12 | activeModal: "",
13 | isOpen: false,
14 | data: null,
15 | };
16 |
17 | export const modals: Reducer = (
18 | state = initialState,
19 | action: Action
20 | ) => {
21 | switch (action.type) {
22 | case OPEN_MODAL:
23 | return {
24 | ...state,
25 | activeModal: action.payload.modalName,
26 | data: action.payload.data,
27 | isOpen: true,
28 | };
29 | case CLOSE_MODAL:
30 | return {
31 | ...state,
32 | activeModal: "",
33 | data: null,
34 | isOpen: false,
35 | };
36 | default:
37 | return state;
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/store/path/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { GET_PATH_REQUEST, EXIT_PATH_PREVIEW_REQUEST } from "./constants";
3 |
4 | export type Route = {
5 | endVertexKey: string;
6 | };
7 |
8 | export const getPath = (payload: Route) => {
9 | return createAction(GET_PATH_REQUEST, payload);
10 | };
11 |
12 | export const exitPathPreview = () => {
13 | return createAction(EXIT_PATH_PREVIEW_REQUEST);
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/path/constants.ts:
--------------------------------------------------------------------------------
1 | export const GET_PATH_REQUEST = "GET_PATH_REQUEST";
2 | export const GET_PATH_SUCCESS = "GET_PATH_SUCCESS";
3 | export const GET_PATH_FAILED = "GET_PATH_FAILED";
4 |
5 | export const EXIT_PATH_PREVIEW_REQUEST = "EXIT_PATH_PREVIEW_REQUEST";
6 | export const EXIT_PATH_PREVIEW_SUCCESS = "EXIT_PATH_PREVIEW_SUCCESS";
7 |
--------------------------------------------------------------------------------
/src/store/path/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { Action } from "store/actions";
3 |
4 | import Vertex from "algorithms/graph/Vertex";
5 | import Edge from "algorithms/graph/Edge";
6 |
7 | import {
8 | GET_PATH_REQUEST,
9 | GET_PATH_SUCCESS,
10 | GET_PATH_FAILED,
11 | EXIT_PATH_PREVIEW_SUCCESS,
12 | } from "./constants";
13 |
14 | export type IState = {
15 | readonly isGenerating: boolean;
16 | readonly isPathPreview: boolean;
17 | readonly pathTimeline: GSAPTimeline | null;
18 | readonly dijkstra: {
19 | readonly vertices: Vertex[];
20 | readonly edges: Edge[];
21 | };
22 | };
23 |
24 | export const initialState: IState = {
25 | isGenerating: false,
26 | isPathPreview: false,
27 | pathTimeline: null,
28 | dijkstra: {
29 | vertices: [],
30 | edges: [],
31 | },
32 | };
33 |
34 | export const path: Reducer = (
35 | state = initialState,
36 | action: Action
37 | ) => {
38 | switch (action.type) {
39 | case GET_PATH_REQUEST:
40 | return {
41 | ...state,
42 | isGenerating: true,
43 | };
44 | case GET_PATH_SUCCESS:
45 | return {
46 | ...state,
47 | isGenerating: false,
48 | isPathPreview: true,
49 | pathTimeline: action.payload.timeline,
50 | dijkstra: {
51 | vertices: action.payload.vertices,
52 | edges: action.payload.edges,
53 | },
54 | };
55 | case GET_PATH_FAILED:
56 | return {
57 | ...state,
58 | isGenerating: false,
59 | };
60 | case EXIT_PATH_PREVIEW_SUCCESS:
61 | return {
62 | ...state,
63 | isPathPreview: false,
64 | pathTimeline: null,
65 | dijkstra: {
66 | vertices: [],
67 | edges: [],
68 | },
69 | };
70 | default:
71 | return state;
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/src/store/path/sagas.ts:
--------------------------------------------------------------------------------
1 | import { takeLatest, put, all, select } from "redux-saga/effects";
2 |
3 | import { AppState } from "store/rootReducer";
4 | import { Action } from "store/actions";
5 | import { Route } from "store/graph/actions";
6 |
7 | import { gsap } from "gsap";
8 | import { getPathFromDijkstra } from "algorithms/graph/Utils";
9 | import dijkstra from "algorithms/graph/Dijkstra";
10 |
11 | import {
12 | GET_PATH_REQUEST,
13 | GET_PATH_SUCCESS,
14 | GET_PATH_FAILED,
15 | EXIT_PATH_PREVIEW_REQUEST,
16 | EXIT_PATH_PREVIEW_SUCCESS,
17 | } from "./constants";
18 | import Vertex from "algorithms/graph/Vertex";
19 |
20 | export function* getPath(action: Action) {
21 | try {
22 | if (action.payload) {
23 | const { graph, startVertex: startVertexKey } = yield select(
24 | (state: AppState) => state.graph
25 | );
26 | const { endVertexKey } = action.payload;
27 |
28 | const startVertex: Vertex = graph.getVertices()[startVertexKey];
29 | const endVertex: Vertex = graph.getVertices()[endVertexKey];
30 |
31 | const { previousVertices } = dijkstra(graph, startVertex);
32 | const { vertices, edges } = getPathFromDijkstra(
33 | graph.getEdges(),
34 | previousVertices,
35 | endVertex
36 | );
37 |
38 | const timeline = gsap.timeline({
39 | paused: true,
40 | });
41 |
42 | yield put({
43 | type: GET_PATH_SUCCESS,
44 | payload: {
45 | vertices,
46 | edges,
47 | timeline,
48 | },
49 | });
50 | }
51 | } catch (error) {
52 | yield put({
53 | type: GET_PATH_FAILED,
54 | });
55 | }
56 | }
57 | export function* resetPath() {
58 | try {
59 | const { pathTimeline } = yield select((state: AppState) => state.path);
60 | pathTimeline.reverse();
61 |
62 | // GSAP isn't removing inline styles after reversing,
63 | // so i have to manually add and remove this class,
64 | // to allow theme switching.
65 | pathTimeline._last._targets[0].classList.remove("Object--active");
66 |
67 | yield put({
68 | type: EXIT_PATH_PREVIEW_SUCCESS,
69 | });
70 | } catch (error) {
71 | }
72 | }
73 |
74 | export function* pathRootSaga() {
75 | yield all([takeLatest(GET_PATH_REQUEST, getPath)]);
76 | yield all([takeLatest(EXIT_PATH_PREVIEW_REQUEST, resetPath)]);
77 | }
78 |
--------------------------------------------------------------------------------
/src/store/rootReducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { History } from "history";
3 |
4 | import { settings, IState as SettingsState } from "./settings/reducer";
5 | import { sidebar, IState as SidebarState } from "./sidebar/reducer";
6 | import { modals, IState as ModalState } from "./modals/reducer";
7 | import { graph, IState as GraphState } from "./graph/reducer";
8 | import { path, IState as PathState } from "./path/reducer";
9 | import { search, IState as SearchState } from "./search/reducer";
10 | import { api, IState as ApiState } from "./api/reducer";
11 |
12 | export interface AppState {
13 | settings: SettingsState;
14 | sidebar: SidebarState;
15 | modals: ModalState;
16 | graph: GraphState;
17 | path: PathState;
18 | search: SearchState;
19 | api: ApiState;
20 | }
21 |
22 | const rootReducer = combineReducers({
23 | settings,
24 | sidebar,
25 | modals,
26 | graph,
27 | path,
28 | search,
29 | api,
30 | });
31 |
32 | export type RootState = ReturnType;
33 |
34 | export default (history: History) => rootReducer;
35 |
--------------------------------------------------------------------------------
/src/store/rootSaga.ts:
--------------------------------------------------------------------------------
1 | import { all, fork } from "redux-saga/effects";
2 |
3 | import { graphRootSaga } from "store/graph/sagas";
4 | import { pathRootSaga } from "store/path/sagas";
5 | import { searchRootSaga } from "store/search/sagas";
6 | import { apiRootSaga } from "store/api/sagas";
7 |
8 | const sagas = [graphRootSaga, pathRootSaga, searchRootSaga, apiRootSaga];
9 |
10 | export default function* rootSaga() {
11 | yield all(sagas.map((saga) => fork(saga)));
12 | }
13 |
--------------------------------------------------------------------------------
/src/store/search/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { SEARCH_PRODUCT_REQUEST } from "./constants";
3 |
4 | export const searchProduct = (productName: string) => {
5 | return createAction(SEARCH_PRODUCT_REQUEST, productName);
6 | };
7 |
--------------------------------------------------------------------------------
/src/store/search/api.ts:
--------------------------------------------------------------------------------
1 | import { ProductsApiType } from "store/api/reducer";
2 | import db from "store/api/db";
3 |
4 | export const searchProductApi = (productName: string) => {
5 | return new Promise>((resolve) => {
6 | const arr = [...db["products"]];
7 |
8 | const results = arr.filter((item) => {
9 | return item.name === productName;
10 | });
11 |
12 | resolve(results);
13 | });
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/search/constants.ts:
--------------------------------------------------------------------------------
1 | export const SEARCH_PRODUCT_REQUEST = "SEARCH_PRODUCT_REQUEST";
2 | export const SEARCH_PRODUCT_SUCCESS = "SEARCH_PRODUCT_SUCCESS";
3 | export const SEARCH_PRODUCT_FAILED = "SEARCH_PRODUCT_FAILED";
4 |
--------------------------------------------------------------------------------
/src/store/search/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { Action } from "store/actions";
3 |
4 | import {
5 | SEARCH_PRODUCT_FAILED,
6 | SEARCH_PRODUCT_REQUEST,
7 | SEARCH_PRODUCT_SUCCESS,
8 | } from "./constants";
9 |
10 | type ProductType = {
11 | id: number;
12 | name: string;
13 | desc: string;
14 | objectId: string;
15 | };
16 |
17 | export type IState = {
18 | readonly isPending: boolean;
19 | readonly searchResult: ProductType | null;
20 | };
21 |
22 | export const initialState: IState = {
23 | isPending: false,
24 | searchResult: null,
25 | };
26 |
27 | export const search: Reducer = (
28 | state = initialState,
29 | action: Action
30 | ) => {
31 | switch (action.type) {
32 | case SEARCH_PRODUCT_REQUEST:
33 | return {
34 | ...state,
35 | isPending: true,
36 | };
37 | case SEARCH_PRODUCT_SUCCESS:
38 | return {
39 | ...state,
40 | isPending: false,
41 | searchResult: action.payload,
42 | };
43 | case SEARCH_PRODUCT_FAILED:
44 | return {
45 | ...state,
46 | isPending: false,
47 | searchResult: {},
48 | };
49 | default:
50 | return state;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/src/store/search/sagas.ts:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, all, put } from "redux-saga/effects";
2 | import { Action } from "store/actions";
3 | import { apiArrayToObject } from "utils/helpers";
4 | import { objectToVertexKey } from "algorithms/graph/Utils";
5 | import { ProductsApiType } from "store/api/reducer";
6 |
7 | import {
8 | SEARCH_PRODUCT_FAILED,
9 | SEARCH_PRODUCT_REQUEST,
10 | SEARCH_PRODUCT_SUCCESS,
11 | } from "./constants";
12 |
13 | import { GET_PATH_REQUEST } from "store/path/constants";
14 |
15 | import { searchProductApi } from "./api";
16 |
17 | function* searchProduct(action: Action) {
18 | try {
19 | if (action.payload) {
20 | const productName = action.payload;
21 | const response: Array = yield call(
22 | searchProductApi,
23 | productName
24 | );
25 |
26 | // Shitty workaround for json-server because when product is not found
27 | // it still returns 200 so I have to check it manually.
28 | if (response && response.length) {
29 | // Also I'm fetching single product but json-server returns it
30 | // as single element array instead of object...
31 | const product: ProductsApiType = apiArrayToObject(response);
32 | yield put({ type: SEARCH_PRODUCT_SUCCESS, payload: product });
33 |
34 | const endVertex = objectToVertexKey(product.objectId);
35 |
36 | if (endVertex) {
37 | yield put({
38 | type: GET_PATH_REQUEST,
39 | payload: {
40 | endVertexKey: endVertex,
41 | },
42 | });
43 | }
44 | } else {
45 | throw new Error();
46 | }
47 | }
48 | } catch (error) {
49 | yield put({ type: SEARCH_PRODUCT_FAILED });
50 | }
51 | }
52 |
53 | export function* searchRootSaga() {
54 | yield all([takeLatest(SEARCH_PRODUCT_REQUEST, searchProduct)]);
55 | }
56 |
--------------------------------------------------------------------------------
/src/store/settings/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { SWITCH_LANG, SWITCH_THEME } from "./constants";
3 |
4 | export const switchTheme = (theme: string) => {
5 | return createAction(SWITCH_THEME, theme);
6 | };
7 |
8 | export const switchLang = (lang: string) => {
9 | return createAction(SWITCH_LANG, lang);
10 | };
11 |
--------------------------------------------------------------------------------
/src/store/settings/constants.ts:
--------------------------------------------------------------------------------
1 | export const SWITCH_LANG = "SWITCH_LANG";
2 | export const SWITCH_THEME = "SWITCH_THEME";
3 |
--------------------------------------------------------------------------------
/src/store/settings/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { SWITCH_LANG, SWITCH_THEME } from "./constants";
3 | import { Action } from "store/actions";
4 |
5 | export type IState = {
6 | readonly theme: string;
7 | readonly lang: string;
8 | };
9 |
10 | export const initialState: IState = {
11 | theme: localStorage.getItem("theme") || "light",
12 | lang: localStorage.getItem("lang") || "pl",
13 | };
14 |
15 | export const settings: Reducer = (
16 | state = initialState,
17 | action: Action
18 | ) => {
19 | switch (action.type) {
20 | case SWITCH_THEME:
21 | return {
22 | ...state,
23 | theme: action.payload,
24 | };
25 | case SWITCH_LANG:
26 | return {
27 | ...state,
28 | lang: action.payload,
29 | };
30 | default:
31 | return state;
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/store/sidebar/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "store/actions";
2 | import { TOGGLE_SIDEBAR } from "./constants";
3 |
4 | export const toggleSidebar = () => createAction(TOGGLE_SIDEBAR);
5 |
--------------------------------------------------------------------------------
/src/store/sidebar/constants.ts:
--------------------------------------------------------------------------------
1 | export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR";
2 |
--------------------------------------------------------------------------------
/src/store/sidebar/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 | import { TOGGLE_SIDEBAR } from "./constants";
3 | import { Action } from "store/actions";
4 |
5 | export type IState = {
6 | readonly isOpen: boolean;
7 | };
8 |
9 | export const initialState: IState = {
10 | isOpen: true,
11 | };
12 |
13 | export const sidebar: Reducer = (
14 | state = initialState,
15 | action: Action
16 | ) => {
17 | switch (action.type) {
18 | case TOGGLE_SIDEBAR:
19 | return {
20 | ...state,
21 | isOpen: !state.isOpen,
22 | };
23 | default:
24 | return state;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "styles/themes.scss";
2 |
3 | *,
4 | h1,
5 | h2,
6 | h3,
7 | h4,
8 | h5,
9 | h6,
10 | p {
11 | margin: 0;
12 | padding: 0;
13 | box-sizing: border-box;
14 | }
15 |
16 | html,
17 | body,
18 | #root,
19 | #app {
20 | height: 100%;
21 | }
22 |
23 | html {
24 | font-size: 62.5%;
25 | }
26 |
27 | body {
28 | font-family: "Montserrat";
29 | font-size: 1.6rem;
30 | }
31 |
32 | // Override for my theme
33 | .react-transform-component,
34 | .react-transform-element {
35 | width: 100% !important;
36 | height: 100% !important;
37 | }
38 |
39 | .ReactModal__Overlay {
40 | opacity: 0;
41 | }
42 |
43 | .ReactModal__Overlay--after-open {
44 | opacity: 1;
45 | }
46 |
47 | .ReactModal__Overlay--before-close {
48 | opacity: 0;
49 | }
50 |
51 | .ModalOverlay {
52 | display: flex;
53 | align-items: center;
54 | justify-content: center;
55 | position: fixed;
56 | top: 0px;
57 | left: 0px;
58 | right: 0px;
59 | bottom: 0px;
60 | overflow: hidden;
61 | transition: opacity 200ms ease-in-out;
62 | background: rgba(0, 0, 0, 0.6) !important;
63 | z-index: 10;
64 | }
65 |
66 | .sr-only {
67 | position: absolute;
68 | width: 1px;
69 | height: 1px;
70 | padding: 0;
71 | margin: -1px;
72 | overflow: hidden;
73 | clip: rect(0, 0, 0, 0);
74 | white-space: nowrap; /* added line */
75 | border: 0;
76 | }
77 |
78 | // GSAP isn't removing inline styles after reversing,
79 | // so i have to manually add and remove this class,
80 | // to allow theme switching.
81 | .Object--active {
82 | fill: var(--object-fill--active) !important;
83 | stroke: var(--object-stroke--active) !important;
84 | }
85 |
--------------------------------------------------------------------------------
/src/styles/themes.scss:
--------------------------------------------------------------------------------
1 | @import "styles/variables.scss";
2 |
3 | // Default colors
4 | :root {
5 | --primary-white: #ffffff;
6 | --green: #2ecc71;
7 |
8 | --primary: #1b78d0;
9 | --primary--light: #54a8f8;
10 | --primary--dark: #000000;
11 |
12 | --floor: #ffffff;
13 | --floor-stroke: #f8f8f8;
14 |
15 | --vertex: #e4e4e4;
16 | --object-fill: #d6ebff;
17 | --object-fill--hover: #b2d5f6;
18 | --object-stroke: #87b6e2;
19 |
20 | --map-bg: #f3f7fa;
21 |
22 | --close-red: #db3b3b;
23 |
24 | --input-bg: #ffffff;
25 | --input-separator: #f1f1f1;
26 | --input-placeholder: #afafaf;
27 |
28 | --searchProduct-icon: #ffffff;
29 | --searchProduct-input: #ffffff;
30 | --searchProduct-input-color: #000000;
31 | --searchProduct-input-placeholder: #ffffff;
32 | --searchProduct-submit-color: #ffffff;
33 | --searchProduct-submit-bg: #1b78d0;
34 |
35 | --autocomplete-bg: #ffffff;
36 | --autocomplete-text: #6c6c6c;
37 | --autocomplete-hover: #f7fbff;
38 | --autocomplete-borderTop: #e1ecee;
39 | --autocomplete-border: #e1ecee;
40 |
41 | --pathPreview-bg: #ffffff;
42 | --pathPreview-text: #000000;
43 |
44 | --button-disabled: #dadada;
45 | --button-editMode: #2ecc71;
46 | --button-color: #ffffff;
47 | --button-tooltip-bg: #000000;
48 | --button-tooltip-color: #ffffff;
49 |
50 | --object-fill--active: #c7ffcc;
51 | --object-stroke--active: #4fba5a;
52 |
53 | --scrollbar-track: #e7e7e7;
54 | --scrollbar-thumb: #1b78d0;
55 | --scrollbar-thumb--sidebar: #a9a9a9;
56 |
57 | --modal-bg: #ffffff;
58 | --modal-text: #000000;
59 | --modal-text-light: #6c6c6c;
60 | --modal-border: #e1e2e2;
61 |
62 | --modal-settings-titleUnderline: #e1e2e2;
63 | --modal-settings-text: #2e2e2e;
64 | --modal-settings-langBorder: #4e4e4e;
65 | --modal-settings-langBg: 0;
66 | --modal-settings-langColor: #000000;
67 |
68 | --modal-switch-bg: #e1e2e2;
69 | --modal-switch-dot: #ffffff;
70 | --modal-switch-outline: #5d9dd5;
71 |
72 | --modal-settings-langBorder: #e1e2e2;
73 |
74 | --sidebar-bg: #ffffff;
75 | --sidebar-headerTitle: #000000;
76 | --sidebar-headerText: #6c6c6c;
77 |
78 | --sidebar-buttonMain-bg: #1b78d0;
79 | --sidebar-buttonMain-bg--hover: #166bbb;
80 | --sidebar-buttonMain-text: #ffffff;
81 |
82 | --sidebar-category-letter: #000000;
83 | --sidebar-category-results: #000000;
84 |
85 | --sidebar-buttonSecond-bg: #daebff;
86 | --sidebar-buttonSecond-bg--hover: #cadff6;
87 | --sidebar-buttonSecond-text: #2668a6;
88 |
89 | --sidebar-categoryItem-bg: #f4faff;
90 | --sidebar-categoryItem-photo: #d4e8ff;
91 | --sidebar-categoryItem-title: #000000;
92 | --sidebar-categoryItem-text: #000000;
93 | --sidebar-categoryItem-itemLink: #daebff;
94 |
95 | --sidebar-footer: #000000;
96 |
97 | --skiplink-text: #ffffff;
98 | }
99 |
100 | // Dark theme colors
101 | [data-theme="dark"] {
102 | --primary-white: #ffffff;
103 | --green: #2ecc71;
104 |
105 | --primary: #1b78d0;
106 | --primary--light: #54a8f8;
107 | --primary--dark: #000000;
108 |
109 | --floor: #343947;
110 | --floor-stroke: #414757;
111 |
112 | --vertex: #4c5f6e;
113 | --object-fill: #355877;
114 | --object-fill--hover: #477eaf;
115 | --object-stroke: #7ec1ff;
116 |
117 | --map-bg: #1a1f27;
118 |
119 | --close-red: #db3b3b;
120 |
121 | --input-bg: #282a30;
122 | --input-separator: #4e4e4e;
123 | --input-placeholder: #afafaf;
124 |
125 | --searchProduct-icon: #282a30;
126 | --searchProduct-input: #282a30;
127 | --searchProduct-input-color: #ffffff;
128 | --searchProduct-input-placeholder: #6c6c6c;
129 | --searchProduct-submit-color: #ffffff;
130 | --searchProduct-submit-bg: #1b78d0;
131 |
132 | --autocomplete-bg: #282a30;
133 | --autocomplete-text: #afafaf;
134 | --autocomplete-hover: #0b0b0c;
135 | --autocomplete-border: #4e4e4e;
136 |
137 | --pathPreview-bg: #282a30;
138 | --pathPreview-text: #ffffff;
139 |
140 | --button-disabled: #3b3f49;
141 | --button-editMode: #2ecc71;
142 | --button-color: #ffffff;
143 | --button-tooltip-bg: #3b3f49;
144 | --button-tooltip-color: #ffffff;
145 |
146 | --object-fill--active: #4fba5a;
147 | --object-stroke--active: #2ecc71;
148 |
149 | --scrollbar-track: #4e4e4e;
150 | --scrollbar-thumb: #1b78d0;
151 | --scrollbar-thumb--sidebar: #4e4e4e;
152 |
153 | --modal-bg: #32343b;
154 | --modal-text: #ffffff;
155 | --modal-text-light: #bdbdbd;
156 | --modal-border: #4e4e4e;
157 |
158 | --modal-settings-titleUnderline: #4e4e4e;
159 | --modal-settings-text: #2e2e2e;
160 | --modal-settings-langBorder: #4e4e4e;
161 | --modal-settings-langBg: #ffffff;
162 | --modal-settings-langColor: #000000;
163 |
164 | --modal-switch-bg: #e1e2e2;
165 | --modal-switch-dot: #ffffff;
166 | --modal-switch-outline: #5d9dd5;
167 |
168 | --modal-settings-langBorder: #e1e2e2;
169 |
170 | --sidebar-bg: #282a30;
171 | --sidebar-headerTitle: #ffffff;
172 | --sidebar-headerText: #c1c1c1;
173 |
174 | --sidebar-buttonMain-bg: #1b78d0;
175 | --sidebar-buttonMain-bg--hover: #166bbb;
176 | --sidebar-buttonMain-text: #ffffff;
177 |
178 | --sidebar-buttonSecond-bg: #3f4862;
179 | --sidebar-buttonSecond-bg--hover: #333b53;
180 | --sidebar-buttonSecond-text: #ffffff;
181 |
182 | --sidebar-category-letter: #ffffff;
183 | --sidebar-category-results: #c1c1c1;
184 |
185 | --sidebar-categoryItem-bg: #363941;
186 | --sidebar-categoryItem-photo: #282a30;
187 | --sidebar-categoryItem-title: #ffffff;
188 | --sidebar-categoryItem-text: #c1c1c1;
189 | --sidebar-categoryItem-itemLink: #99caf8;
190 |
191 | --sidebar-footer: #ffffff;
192 |
193 | --skiplink-text: #ffffff;
194 | }
195 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejb2k/pathfinding_app/40b3880586173b0f55dbcf496cc07228c143f455/src/styles/variables.scss
--------------------------------------------------------------------------------
/src/utils/__tests__/helpers.test.ts:
--------------------------------------------------------------------------------
1 | import { apiArrayToObject, capitalize } from "utils/helpers";
2 |
3 | describe("helpers.ts", () => {
4 | it("should return first element from array", () => {
5 | const arr = ["john", "doe"];
6 |
7 | expect(apiArrayToObject(arr)).toBe(arr[0]);
8 | });
9 |
10 | it("should capitalize first letter in string", () => {
11 | const letter = "t";
12 | const capitalized = capitalize(letter);
13 |
14 | expect(capitalized).toBe(letter.toUpperCase());
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/utils/__tests__/initLocalStorage.test.ts:
--------------------------------------------------------------------------------
1 | import { apiArrayToObject, capitalize } from "utils/helpers";
2 |
3 | describe("initLocalStorage.ts", () => {
4 | it("should return first element from array", () => {
5 | const arr = ["john", "doe"];
6 |
7 | expect(apiArrayToObject(arr)).toBe(arr[0]);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export const apiArrayToObject = (arr: Array) => {
2 | return arr[0];
3 | };
4 |
5 | export const capitalize = (text: string) => {
6 | return text.charAt(0).toUpperCase() + text.slice(1);
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 |
4 | import translation_pl from "./translations/pl.json";
5 | import translation_en from "./translations/en.json";
6 |
7 | const resources = {
8 | pl: {
9 | translation: translation_pl,
10 | },
11 | en: {
12 | translation: translation_en,
13 | },
14 | };
15 |
16 | i18n.use(initReactI18next).init({
17 | resources,
18 | lng: "pl",
19 | keySeparator: false,
20 | interpolation: {
21 | escapeValue: false,
22 | },
23 | });
24 |
25 | export default i18n;
26 |
--------------------------------------------------------------------------------
/src/utils/i18n/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "Welcome to React and react-i18next"
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/i18n/translations/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "Witaj w React i react-i18next"
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/initLocalStorage.ts:
--------------------------------------------------------------------------------
1 | export const initLocalStorageSettings = () => {
2 | if (!localStorage.getItem("lang")) {
3 | localStorage.setItem("lang", "pl");
4 | }
5 | if (!localStorage.getItem("theme")) {
6 | localStorage.setItem("theme", "light");
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready
142 | .then(registration => {
143 | registration.unregister();
144 | })
145 | .catch(error => {
146 | console.error(error.message);
147 | });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/utils/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 | import "@testing-library/jest-dom";
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react",
17 | "baseUrl": "src",
18 | "downlevelIteration": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------