├── .gitignore
├── .nvmrc
├── .prettierrc
├── FUNDING.yml
├── LICENSE
├── README.md
├── algorithms
├── array-flat
│ └── array-flat.js
├── array-intersect
│ ├── array-intersect.js
│ └── array-intersect.test.js
├── array-methods
│ └── array-methods.js
├── corresponding-node
│ └── corresponding-node.js
├── curry
│ └── curry.js
├── debounce
│ └── debounce.js
├── decode-message
│ ├── decode-message.js
│ └── decode-message.test.js
├── deep-equals
│ ├── deep-equals.js
│ └── deep-equals.test.js
├── event-target
│ └── event-target.js
├── first-bad-version
│ ├── first-bad-version.js
│ └── first-bad-version.test.js
├── flatten
│ ├── cleaner-flatten.js
│ └── flatten.js
├── implement-a-queue-by-using-stack
│ └── implement-a-queue-by-using-stack.js
├── memoize
│ └── memoize.js
├── promisify
│ └── promisify.js
├── remove-characters
│ ├── remove-characters.js
│ └── remove-characters.test.js
├── repeated-chars
│ └── repeated-chars.js
├── testing-framework
│ └── testing-framework.js
├── throttle
│ └── throttle.js
├── traverse-dom-level-by-level
│ ├── traverse-dom-level-by-level.js
│ └── traverse-dom-level-by-level.test.js
└── trending-stocks
│ └── trending-stocks.js
├── architecture
├── README.md
├── architecture
│ └── draft.md
└── monorepo
│ └── draft.md
├── images
└── tag-input.png
├── interviews
├── README.md
└── system-design.md
├── javascript
├── README.md
├── create-dom
│ └── index.js
├── debounce
│ ├── README.md
│ └── index.js
├── deep-equal
│ └── index.js
├── event-emitter
│ ├── README.md
│ ├── index.js
│ └── tests
│ │ └── index.js
├── memoization
│ ├── README.md
│ ├── benchmark
│ │ ├── factorial.js
│ │ ├── index.js
│ │ ├── sum.js
│ │ └── test.js
│ └── index.js
├── promises
│ ├── README.md
│ ├── examples
│ │ ├── promise1.js
│ │ ├── promise2.js
│ │ └── promise3.js
│ ├── from-scratch
│ │ ├── README.md
│ │ ├── promise.js
│ │ └── promiseAll.js
│ └── use-cases
│ │ ├── delay.js
│ │ ├── document-ready.js
│ │ ├── fetch-pokemons.js
│ │ ├── fetch.js
│ │ ├── request.js
│ │ └── retries.js
├── throttle
│ ├── README.md
│ └── index.js
└── update-timer
│ └── index.js
├── package.json
├── react
├── .eslintrc
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── README.md
├── docs
│ └── nested-comments.md
├── hooks
│ ├── useFetch.js
│ ├── useInterval.js
│ ├── useLocalStorage.js
│ ├── useMap.js
│ ├── useStateWithHistory.js
│ └── useWindowSize.js
├── next.config.js
├── package.json
├── pages
│ ├── _app.js
│ ├── context
│ │ └── index.js
│ ├── fetch
│ │ └── index.js
│ ├── form
│ │ ├── README.md
│ │ └── index.js
│ ├── index.js
│ ├── nested-comments
│ │ └── index.js
│ ├── parent-state
│ │ └── index.js
│ ├── phone-input
│ │ └── index.js
│ ├── progress-bar
│ │ ├── README.md
│ │ └── index.js
│ ├── question-list
│ │ └── index.js
│ ├── tag-input
│ │ ├── README.md
│ │ └── index.js
│ ├── tip-calculator
│ │ └── index.js
│ └── todo
│ │ └── index.js
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ └── globals.css
└── yarn.lock
├── testing
└── README.md
└── ui-challenges
├── README.md
├── pokemon-card
├── README.md
├── images
│ ├── charmeleon.png
│ ├── fire.png
│ ├── pokemon-challenge.png
│ └── ui-charmeleon-card.png
├── index.html
└── style.css
├── read-more
├── README.md
├── index.html
└── style.css
├── smooth-scrollable-list
├── README.md
├── index.html
└── style.css
├── template
├── README.md
├── index.html
└── style.css
└── year-progress-bar
├── README.md
├── images
└── year-progress-bar.png
├── index.html
├── index.js
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # typescript
34 | *.tsbuildinfo
35 |
36 | .eslintcache
37 | /local
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.8.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "tabWidth": 2
5 | }
6 |
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [imteekay]
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) TK
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Crafting Frontend
4 |
5 | Research, studies, and practice on the craft of frontend.
6 |
7 | - [Architecture](architecture)
8 | - [React](react)
9 | - [JavaScript](javascript)
10 | - [Interviews](interviews)
11 | - [Testing](testing)
12 | - [UI Challenges](ui-challenges)
13 |
14 | ## License
15 |
16 | [MIT](/LICENSE) © [TK](https://iamtk.co)
17 |
18 |
19 |
--------------------------------------------------------------------------------
/algorithms/array-flat/array-flat.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/implement-Array-prototype.flat
2 |
3 | // This is a JavaScript coding problem from BFE.dev
4 |
5 | function flat(arr, depth = 1) {
6 | let output = [];
7 |
8 | for (let item of arr) {
9 | if (Array.isArray(item)) {
10 | if (depth > 0) {
11 | output.push(...flat(item, depth - 1));
12 | } else {
13 | output.push(item);
14 | }
15 | } else {
16 | output.push(item);
17 | }
18 | }
19 |
20 | return output;
21 | }
22 |
--------------------------------------------------------------------------------
/algorithms/array-intersect/array-intersect.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/array-intersect
2 | // Runtime: O(N), N = size of arr1 and arr2
3 | // Space: O(N), N = size of arr1 and arr2 (for the new array and the hashmap)
4 |
5 | export function getIntersection(arr1, arr2) {
6 | let map = new Map();
7 | let result = [];
8 |
9 | for (let num of arr1) {
10 | map.set(num, 1);
11 | }
12 |
13 | for (let num of arr2) {
14 | if (map.get(num) === 1) {
15 | result.push(num);
16 | map.set(num, 0);
17 | }
18 | }
19 |
20 | return result;
21 | }
22 |
--------------------------------------------------------------------------------
/algorithms/array-intersect/array-intersect.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { getIntersection } from './array-intersect';
3 |
4 | describe('getIntersection', () => {
5 | it('', () => {
6 | expect(getIntersection([1, 2, 3], [3, 2, 1])).toEqual([3, 2, 1]);
7 | expect(getIntersection([], [3, 2, 1])).toEqual([]);
8 | expect(
9 | getIntersection(
10 | [1, 100, 200, 8, 8, 8, 3, 6, 100, 10, 10],
11 | [8, 7, 7, 50, 50, 1, 1, 1, 1, 3, 3]
12 | )
13 | ).toEqual([8, 1, 3]);
14 | expect(
15 | getIntersection(
16 | [
17 | 1,
18 | 2,
19 | 1,
20 | 2,
21 | 1,
22 | 2,
23 | 1,
24 | 2,
25 | 1,
26 | 2,
27 | 1,
28 | 2,
29 | 1,
30 | 2,
31 | 1,
32 | 2,
33 | 3,
34 | 3,
35 | 3,
36 | 3,
37 | 3,
38 | 3,
39 | ,
40 | 2,
41 | 2,
42 | 2,
43 | ],
44 | [2]
45 | )
46 | ).toEqual([2]);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/algorithms/array-methods/array-methods.js:
--------------------------------------------------------------------------------
1 | /*
2 | Without calling Array.prototype.map(), Array.prototype.filter(), Array.prototype.reduce(), or Array.prototype.forEach(),
3 | implement the following three similar functions on the Array prototype:
4 |
5 | myMap(callback)
6 |
7 | - Without mutating the original array, this function should call the passed callback function on every element of
8 | the array and return a new array containing the results of all these calls, in the corresponding order.
9 | - The callback function can take in up to three parameters:
10 | - The current value in the array.
11 | - The current index in the array.
12 | - The array itself.
13 |
14 | myFilter(callback)
15 | - Without mutating the original array, this function should call the passed callback function on every element
16 | of the array and return a new array containing the values of the original array that, when passed to the
17 | callback function, returned true. These values should maintain their original order.
18 | - The callback function takes in the same arguments as the ones that the callback function in myMap takes in.
19 |
20 | myReduce(callback, initialValue)
21 | - Without mutating the original array, this function should call the passed callback function on every element
22 | of the array and return the result of the last call to the callback.
23 | - The callback function can take in up to four parameters:
24 | - The accumulator, which is the return value of the previous call to the callback. On the first call to the callback, the accumulator should be set to the initialValue. If the initialValue is undefined, then it should be set to the first value of the array, and the callback should skip the first element in the array and be called directly on the second element.
25 | - The current value in the array.
26 | - The current index in the array.
27 | - The array itself.
28 |
29 | If the array contains no elements, the initialValue should be returned. Note that this differs slightly from the Array.prototype.reduce function.
30 | */
31 |
32 | Array.prototype.myMap = function (callback) {
33 | let arr = [];
34 |
35 | for (let index = 0; index < this.length; index++) {
36 | arr.push(callback(this[index], index, this));
37 | }
38 |
39 | return arr;
40 | };
41 |
42 | Array.prototype.myFilter = function (callback) {
43 | let arr = [];
44 |
45 | for (let index = 0; index < this.length; index++) {
46 | if (callback(this[index], index, this) === true) {
47 | arr.push(this[index]);
48 | }
49 | }
50 |
51 | return arr;
52 | };
53 |
54 | Array.prototype.myReduce = function (callback, initialValue) {
55 | let acc = initialValue;
56 |
57 | for (let index = 0; index < this.length; index++) {
58 | if (index === 0 && initialValue === undefined) {
59 | acc = this[index];
60 | } else {
61 | acc = callback(acc, this[index], index, this);
62 | }
63 | }
64 |
65 | return acc;
66 | };
67 |
--------------------------------------------------------------------------------
/algorithms/corresponding-node/corresponding-node.js:
--------------------------------------------------------------------------------
1 | function correspondingNode(tree1, tree2, node1) {
2 | if (tree1 === node1) {
3 | return tree2;
4 | }
5 |
6 | let children1 = [...tree1.children];
7 | let children2 = [...tree2.children];
8 |
9 | for (let index = 0; index < children1.length; index++) {
10 | let child1 = children1[index];
11 | let child2 = children2[index];
12 | let corresponding = correspondingNode(child1, child2, node1);
13 |
14 | if (corresponding) {
15 | return corresponding;
16 | }
17 | }
18 |
19 | return null;
20 | }
21 |
--------------------------------------------------------------------------------
/algorithms/curry/curry.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/implement-curry
2 |
3 | function curry(fn) {
4 | const curried = (...args) => {
5 | if (args.length >= fn.length) {
6 | return fn(...args);
7 | } else {
8 | return (...restArgs) => curried(...[...args, ...restArgs]);
9 | }
10 | };
11 |
12 | return curried;
13 | }
14 |
--------------------------------------------------------------------------------
/algorithms/debounce/debounce.js:
--------------------------------------------------------------------------------
1 | function debounce(callback, delay, immediate = false) {
2 | let timerId;
3 |
4 | return function (...args) {
5 | clearTimeout(timerId);
6 |
7 | if (immediate && timerId == null) {
8 | callback.apply(this, args);
9 | }
10 |
11 | timerId = setTimeout(() => {
12 | if (!immediate) callback.apply(this, args);
13 | timerId = null;
14 | }, delay);
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/algorithms/decode-message/decode-message.js:
--------------------------------------------------------------------------------
1 | // Decode message
2 | // https://bigfrontend.dev/problem/decode-message
3 |
4 | export function decode(message) {
5 | let code = '';
6 | let row = -1;
7 | let col = -1;
8 | let move = 'downRight';
9 |
10 | const moveDownRight = () => {
11 | row++;
12 | col++;
13 |
14 | while (row < message.length && col < message[0].length) {
15 | code += message[row][col];
16 | row++;
17 | col++;
18 | }
19 |
20 | row--;
21 | col--;
22 | };
23 |
24 | const moveUpRight = () => {
25 | row--;
26 | col++;
27 |
28 | while (row > 0 && col < message[0].length - 1) {
29 | code += message[row][col];
30 | row--;
31 | col++;
32 | }
33 |
34 | row++;
35 | col--;
36 | };
37 |
38 | while (row !== message.length - 1 && col !== message[0].length - 1) {
39 | if (move === 'downRight') {
40 | moveDownRight();
41 | move = 'upRight';
42 | }
43 |
44 | if (move === 'upRight') {
45 | moveUpRight();
46 | move = 'downRight';
47 | }
48 | }
49 |
50 | return code;
51 | }
52 |
--------------------------------------------------------------------------------
/algorithms/decode-message/decode-message.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { decode } from './decode-message';
3 |
4 | describe('decode', () => {
5 | it('', () => {
6 | expect(decode([])).toEqual('');
7 | expect(decode([['A']])).toEqual('A');
8 | expect(decode([[['A'], ['B']]])).toEqual('A');
9 | expect(
10 | decode([
11 | ['I', 'B', 'C', 'A', 'L', 'K', 'A'],
12 | ['D', 'R', 'F', 'C', 'A', 'E', 'A'],
13 | ['G', 'H', 'O', 'E', 'L', 'A', 'D'],
14 | ])
15 | ).toEqual('IROCLED');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/algorithms/deep-equals/deep-equals.js:
--------------------------------------------------------------------------------
1 | // https://www.algoexpert.io/frontend/coding-questions/deep-equals
2 |
3 | /**
4 | * Possible type
5 | * - number
6 | * - string
7 | * - boolean
8 | * - null
9 | * - undefined
10 | * - NaN
11 | * - object
12 | * - can have all primitive values
13 | * - can have arrays
14 | * - can have nested objects
15 | * - array
16 | * - can have all primitive values
17 | * - can have nested arrays
18 | * - can have objects
19 | */
20 |
21 | function isObject(value) {
22 | return (
23 | typeof value === 'object' &&
24 | ![null, undefined].includes(value) &&
25 | !Array.isArray(value)
26 | );
27 | }
28 |
29 | export function deepEquals(valueOne, valueTwo) {
30 | const hasSameType =
31 | typeof valueOne === typeof valueTwo ||
32 | (Array.isArray(valueOne) && isObject(valueTwo)) ||
33 | (Array.isArray(valueTwo) && isObject(valueOne)) ||
34 | (Array.isArray(valueOne) && valueTwo === null) ||
35 | (Array.isArray(valueTwo) && valueOne === null) ||
36 | (Array.isArray(valueOne) && valueTwo === undefined) ||
37 | (Array.isArray(valueTwo) && valueOne === undefined) ||
38 | (isObject(valueOne) && valueTwo === null) ||
39 | (isObject(valueTwo) && valueOne === null) ||
40 | (isObject(valueOne) && valueTwo === undefined) ||
41 | (isObject(valueTwo) && valueOne === undefined);
42 |
43 | const areEqual = valueOne === valueTwo;
44 | const areNaN = Number.isNaN(valueOne) && Number.isNaN(valueTwo);
45 |
46 | // Handle different types
47 | if (!hasSameType) {
48 | return false;
49 | }
50 |
51 | // Handle arrays
52 | if (Array.isArray(valueOne) && Array.isArray(valueTwo)) {
53 | if (valueOne.length !== valueTwo.length) {
54 | return false;
55 | }
56 |
57 | for (let index = 0; index < valueOne.length; index++) {
58 | if (!deepEquals(valueOne[index], valueTwo[index])) {
59 | return false;
60 | }
61 | }
62 |
63 | return true;
64 | }
65 |
66 | // Handle objects
67 | if (isObject(valueOne) && isObject(valueTwo)) {
68 | for (let key of Object.keys(valueOne)) {
69 | if (
70 | !(
71 | valueTwo.hasOwnProperty(key) &&
72 | deepEquals(valueOne[key], valueTwo[key])
73 | )
74 | ) {
75 | return false;
76 | }
77 | }
78 |
79 | for (let key of Object.keys(valueTwo)) {
80 | if (
81 | !(
82 | valueOne.hasOwnProperty(key) &&
83 | deepEquals(valueOne[key], valueTwo[key])
84 | )
85 | ) {
86 | return false;
87 | }
88 | }
89 |
90 | return true;
91 | }
92 |
93 | return areEqual || areNaN;
94 | }
95 |
--------------------------------------------------------------------------------
/algorithms/deep-equals/deep-equals.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { deepEquals } from './deep-equals';
3 |
4 | describe('deepEquals', () => {
5 | describe('primitives', () => {
6 | it('', () => {
7 | expect(deepEquals(1, 1)).toEqual(true);
8 | expect(deepEquals(1, 2)).toEqual(false);
9 | expect(deepEquals('abc', 'abc')).toEqual(true);
10 | expect(deepEquals('bc', 'abc')).toEqual(false);
11 | expect(deepEquals(true, true)).toEqual(true);
12 | expect(deepEquals(true, false)).toEqual(false);
13 | expect(deepEquals(NaN, null)).toEqual(false);
14 | expect(deepEquals(NaN, NaN)).toEqual(true);
15 | expect(deepEquals(null, [1])).toEqual(false);
16 | expect(deepEquals([1], null)).toEqual(false);
17 | expect(deepEquals(undefined, [1])).toEqual(false);
18 | expect(deepEquals([1], undefined)).toEqual(false);
19 | });
20 | });
21 |
22 | describe('arrays', () => {
23 | it('', () => {
24 | expect(deepEquals([0], [1])).toEqual(false);
25 | expect(deepEquals([0, 1, 2], [4, 5, 6])).toEqual(false);
26 | expect(deepEquals([0, 'abc', 2], [0, 'abc', '2'])).toEqual(false);
27 | expect(deepEquals([0, 'abc', '2'], [0, 'abc', 2])).toEqual(false);
28 | expect(deepEquals([null, null, true], [null, undefined, true])).toEqual(
29 | false
30 | );
31 | expect(deepEquals([null, undefined, true], [null, null, true])).toEqual(
32 | false
33 | );
34 | expect(deepEquals([0, 1, 2], [0, 1, 2])).toEqual(true);
35 | expect(deepEquals([0, 1, [1, 2, 3]], [0, 1, [1, 2, 3]])).toEqual(true);
36 | expect(deepEquals([{}], [{}])).toEqual(true);
37 | expect(deepEquals([{}, {}, []], [{}, {}, []])).toEqual(true);
38 | expect(deepEquals([], {})).toEqual(false);
39 | expect(deepEquals([], undefined)).toEqual(false);
40 | expect(deepEquals(undefined, [])).toEqual(false);
41 | expect(deepEquals([{}, null], [{}])).toEqual(false);
42 | });
43 | });
44 |
45 | describe('objects', () => {
46 | it('', () => {
47 | expect(deepEquals({}, {})).toEqual(true);
48 | expect(deepEquals({ a: 123 }, { a: '123' })).toEqual(false);
49 | expect(deepEquals({ 1: 1, 2: 2, 3: 3 }, { 1: 1, 2: 2, 3: 3 })).toEqual(
50 | true
51 | );
52 | expect(deepEquals({ 1: 1, 2: 2, 3: 3 }, { 2: 2, 1: 1, 3: 3 })).toEqual(
53 | true
54 | );
55 | expect(
56 | deepEquals(
57 | { 1: 1, 2: 2, 3: { a: 'a', b: 'b' } },
58 | { 2: 2, 1: 1, 3: { a: 'a', b: 'b' } }
59 | )
60 | ).toEqual(true);
61 | expect(
62 | deepEquals(
63 | { 1: 1, 2: 2, 3: { a: 'a', b: ['b', 23] } },
64 | { 2: 2, 1: 1, 3: { a: 'a', b: ['b', 23] } }
65 | )
66 | ).toEqual(true);
67 | expect(
68 | deepEquals(
69 | { 1: 1, 2: 2, 3: { a: 'a', b: ['b', 23] } },
70 | { 2: 2, 1: 1, 3: { a: 'a', b: ['b', 22] } }
71 | )
72 | ).toEqual(false);
73 |
74 | expect(deepEquals({}, null)).toEqual(false);
75 | expect(deepEquals(null, {})).toEqual(false);
76 | expect(deepEquals({}, undefined)).toEqual(false);
77 | expect(deepEquals(undefined, {})).toEqual(false);
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/algorithms/event-target/event-target.js:
--------------------------------------------------------------------------------
1 | /*
2 | Implement an EventTarget class (similar to the EventTarget interface of the DOM),
3 | which keeps track of event listeners and dispatches events.
4 |
5 | Your EventTarget class should have the following three methods:
6 |
7 | addEventListener
8 |
9 | - This function takes in two arguments: the name of an event as a string and a callback function,
10 | to be called when the event is dispatched to the target.
11 | - For example, target.addEventListener('click', onClick) should make it such that the onClick
12 | callback is called when the 'click' event is dispatched to the target.
13 | A target should be able to have multiple event listeners for the same event (for example, onClick1
14 | and onClick2, both attached to the 'click' event). However, adding the same exact event listener
15 | twice (with the same event and the same callback) should have no effect.
16 |
17 | removeEventListener
18 |
19 | - This function takes in the same arguments as addEventListener and removes the relevant event listener.
20 | - For example, target.removeEventListener('click', onClick) should undo the effects of the addEventListener
21 | call in the bullet point above.
22 | - If there's no current event listener for the passed-in arguments, removeEventListener should have no effect.
23 | Also, if two different callbacks have been added for the same 'click' event (e.g., onClick1 and onClick2),
24 | removing one shouldn't remove the other.
25 |
26 | dispatchEvent
27 |
28 | - This function takes in the name of an event as a string. If there are no event listeners for that event,
29 | nothing should happen. Otherwise, event listeners that do exist for that event should have their callback functions invoked.
30 | - For example, given the event listener added in the first bullet point and assuming it hadn't been removed,
31 | dispatchEvent('click') would call onClick.
32 | - Events can be dispatched multiple times, and each time, every associated callback should be invoked.
33 | */
34 |
35 | class EventTarget {
36 | constructor() {
37 | this.events = new Map();
38 | }
39 |
40 | addEventListener(name, callback) {
41 | this.events.set(name, [...(this.events.get(name) || []), callback]);
42 | }
43 |
44 | removeEventListener(name, callback) {
45 | const removedCallback = (this.events.get(name) || []).filter(
46 | (event) => event !== callback
47 | );
48 | this.events.set(name, removedCallback);
49 | }
50 |
51 | dispatchEvent(name) {
52 | if (this.events.has(name)) {
53 | for (const callback of this.events.get(name)) {
54 | callback();
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/algorithms/first-bad-version/first-bad-version.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/first-bad-version
2 |
3 | /*
4 | * type IsBad = (version: number) => boolean
5 | * @param {IsBad} isBad
6 | */
7 | export function firstBadVersion(isBad) {
8 | return (version) => {
9 | let first = -1;
10 | let start = 0;
11 | let end = version;
12 | let middle = Math.floor((start + end) / 2);
13 |
14 | while (start <= end) {
15 | if (isBad(middle)) {
16 | first = middle;
17 | end = middle - 1;
18 | } else {
19 | start = middle + 1;
20 | }
21 |
22 | middle = Math.floor((start + end) / 2);
23 | }
24 |
25 | return first;
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/algorithms/first-bad-version/first-bad-version.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { firstBadVersion } from './first-bad-version';
3 |
4 | describe('firstBadVersion', () => {
5 | it('', () => {
6 | expect(firstBadVersion((i) => i >= 4)(100)).toEqual(4);
7 | expect(firstBadVersion((i) => i >= 4)(4)).toEqual(4);
8 | expect(firstBadVersion((i) => i >= 5)(3)).toEqual(-1);
9 | expect(firstBadVersion((i) => i >= 1)(1)).toEqual(1);
10 | expect(firstBadVersion((i) => i >= 1)(2)).toEqual(1);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/algorithms/flatten/cleaner-flatten.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Write a flatten function that takes in a value and returns a flattened version of that value.
3 | * For the purpose of this problem, a flattened value is defined as follows:
4 | * Primitive values should be left unchanged.
5 | * Nested arrays should have their values brought up to the top level array
6 | * For example, [1, 2, [3, 4, [5, 6]]] would be flattened to [1, 2, 3, 4, 5, 6].
7 | * Nested objects should have their key-value pairs brought up to the top level object.
8 | * For example, {a: 1, b: {c: 2, d: 3, e: {f: 4}}} would be flattened to {a: 1, c: 2, d: 3, f: 4}.
9 | * Note that this means the keys "b" and "e" were completely removed, since their values were
10 | * flattened to the top level. In the event of a key collision (e.g. {a: 1, b: {a: 2}}), any
11 | * associated value can be used.
12 | * Arrays nested in objects and objects nested in arrays should be flattened. For example,
13 | * {a: [1, 2, [3, 4]]} would be flattened to {a: [1, 2, 3, 4]}, and [{a: 1, b: {c: 2, d: 3}}]
14 | * would be flattened to [{a: 1, c: 2, d: 3}].
15 | * For simplicity, you can assume the value as well as any nested values will not be functions.
16 | * Additionally, you can assume that all object keys are strings. Your solution can return a
17 | * flattened value in place, or it can return a new value, either is acceptable.
18 | */
19 |
20 | function flattenArray(value) {
21 | return value.reduce((array, v) => {
22 | if (v === null) {
23 | return array;
24 | }
25 |
26 | if (Array.isArray(v)) {
27 | return [...array, ...flattenArray(v)];
28 | }
29 |
30 | if (typeof v === 'object') {
31 | return [...array, flattenObject(v)];
32 | }
33 |
34 | return [...array, v];
35 | }, []);
36 | }
37 |
38 | function flattenObject(value) {
39 | return Object.entries(value).reduce((obj, [key, val]) => {
40 | if (val === null || Array.isArray(val)) {
41 | const flattenedValue = flatten(val);
42 |
43 | return {
44 | ...obj,
45 | [key]: flattenedValue,
46 | };
47 | }
48 |
49 | if (typeof val === 'object') {
50 | const flattedObj = flattenObject(val);
51 |
52 | if (JSON.stringify(flattedObj) === '{}') {
53 | return obj;
54 | }
55 |
56 | return {
57 | ...obj,
58 | ...flattedObj,
59 | };
60 | }
61 |
62 | return {
63 | ...obj,
64 | [key]: val,
65 | };
66 | }, {});
67 | }
68 |
69 | function flatten(value) {
70 | if (value === null) {
71 | return value;
72 | }
73 |
74 | if (Array.isArray(value)) {
75 | return flattenArray(value);
76 | }
77 |
78 | if (typeof value === 'object') {
79 | return flattenObject(value);
80 | }
81 |
82 | return value;
83 | }
84 |
--------------------------------------------------------------------------------
/algorithms/flatten/flatten.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Write a flatten function that takes in a value and returns a flattened version of that value.
3 | * For the purpose of this problem, a flattened value is defined as follows:
4 | * Primitive values should be left unchanged.
5 | * Nested arrays should have their values brought up to the top level array
6 | * For example, [1, 2, [3, 4, [5, 6]]] would be flattened to [1, 2, 3, 4, 5, 6].
7 | * Nested objects should have their key-value pairs brought up to the top level object.
8 | * For example, {a: 1, b: {c: 2, d: 3, e: {f: 4}}} would be flattened to {a: 1, c: 2, d: 3, f: 4}.
9 | * Note that this means the keys "b" and "e" were completely removed, since their values were
10 | * flattened to the top level. In the event of a key collision (e.g. {a: 1, b: {a: 2}}), any
11 | * associated value can be used.
12 | * Arrays nested in objects and objects nested in arrays should be flattened. For example,
13 | * {a: [1, 2, [3, 4]]} would be flattened to {a: [1, 2, 3, 4]}, and [{a: 1, b: {c: 2, d: 3}}]
14 | * would be flattened to [{a: 1, c: 2, d: 3}].
15 | * For simplicity, you can assume the value as well as any nested values will not be functions.
16 | * Additionally, you can assume that all object keys are strings. Your solution can return a
17 | * flattened value in place, or it can return a new value, either is acceptable.
18 | */
19 |
20 | function flatten(value) {
21 | if (value === null) {
22 | return value;
23 | }
24 |
25 | if (Array.isArray(value)) {
26 | return value.reduce((array, v) => {
27 | if (Array.isArray(v)) {
28 | return [...array, ...flatten(v)];
29 | }
30 |
31 | if (v === null) {
32 | return array;
33 | }
34 |
35 | if (typeof v === 'object') {
36 | return [...array, flatten(v)];
37 | }
38 |
39 | return [...array, v];
40 | }, []);
41 | }
42 |
43 | if (typeof value === 'object') {
44 | return Object.entries(value).reduce((obj, [key, val]) => {
45 | if (val === null) {
46 | return {
47 | ...obj,
48 | [key]: val,
49 | };
50 | }
51 |
52 | if (typeof val === 'object') {
53 | const flattedObj = flatten(val);
54 |
55 | if (JSON.stringify(flattedObj) === '{}') {
56 | return obj;
57 | }
58 |
59 | if (Array.isArray(val)) {
60 | return {
61 | ...obj,
62 | [key]: flattedObj,
63 | };
64 | }
65 |
66 | return {
67 | ...obj,
68 | ...flattedObj,
69 | };
70 | }
71 |
72 | return {
73 | ...obj,
74 | [key]: val,
75 | };
76 | }, {});
77 | }
78 |
79 | return value;
80 | }
81 |
--------------------------------------------------------------------------------
/algorithms/implement-a-queue-by-using-stack/implement-a-queue-by-using-stack.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/implement-a-queue-by-using-stack
2 |
3 | class Queue {
4 | constructor() {
5 | this.stack = new Stack();
6 | }
7 |
8 | enqueue(element) {
9 | this.stack.push(element);
10 | }
11 |
12 | peek() {
13 | let tempStack = new Stack();
14 | let peekElement;
15 |
16 | while (this.size() > 0) {
17 | peekElement = this.stack.pop();
18 | tempStack.push(peekElement);
19 | }
20 |
21 | while (tempStack.size() > 0) {
22 | this.stack.push(tempStack.pop());
23 | }
24 |
25 | return peekElement;
26 | }
27 |
28 | size() {
29 | return this.stack.size();
30 | }
31 |
32 | dequeue() {
33 | let tempStack = new Stack();
34 | let peekElement;
35 |
36 | while (this.size() > 0) {
37 | peekElement = this.stack.pop();
38 | if (this.size() > 0) tempStack.push(peekElement);
39 | }
40 |
41 | while (tempStack.size() > 0) {
42 | this.stack.push(tempStack.pop());
43 | }
44 |
45 | return peekElement;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/algorithms/memoize/memoize.js:
--------------------------------------------------------------------------------
1 | /**
2 | Write a memoize function that takes in a required callback function and an optional resolver function.
3 | The memoize function returns a memoized version of the callback function, which is defined as follows:
4 |
5 | All of the return values of the memoized function are cached. If the memoized callback is called with
6 | an existing cache key (defined below), then that cached value is returned without invoking the callback again.
7 | The cache key is defined based on the optional resolver function. If a resolver function is not
8 | provided, then the cache key is the result of passing the memoized function arguments to JSON.stringify
9 | as an array. If a custom resolver function is provided, then the arguments should be individually passed
10 | to that function instead, and its return value will be the cache key (note that this can be of any type).
11 |
12 | The memoized function should also have three methods:
13 | clear(): Clears out the cache.
14 | delete(...args): Deletes the cache entry corresponding to the passed arguments if one exists.
15 | has(...args): Returns a boolean of true if the cache has an entry corresponding to the passed arguments, otherwise false.
16 |
17 | For simplicity, you don't need to worry about binding a this context (i.e., you can assume that the callback doesn't reference this).
18 | */
19 |
20 | function memoize(callback, resolver) {
21 | let cache = new Map();
22 | let getCacheKey = (...args) => {
23 | return resolver ? resolver(...args) : JSON.stringify(args);
24 | };
25 |
26 | let memo = (...args) => {
27 | const cacheKey = getCacheKey(...args);
28 | const cacheValue = cache.has(cacheKey)
29 | ? cache.get(cacheKey)
30 | : callback(...args);
31 | cache.set(cacheKey, cacheValue);
32 | return cacheValue;
33 | };
34 |
35 | memo.clear = () => {
36 | cache = new Map();
37 | };
38 |
39 | memo.delete = (...args) => {
40 | const cacheKey = getCacheKey(...args);
41 | cache.delete(cacheKey);
42 | };
43 |
44 | memo.has = (...args) => {
45 | const cacheKey = getCacheKey(...args);
46 | return cache.has(cacheKey);
47 | };
48 |
49 | return memo;
50 | }
51 |
--------------------------------------------------------------------------------
/algorithms/promisify/promisify.js:
--------------------------------------------------------------------------------
1 | function promisify(callback) {
2 | return function (...args) {
3 | return new Promise((resolve, reject) => {
4 | const handleErrorAndValue = (error, value) => {
5 | if (error) reject(error);
6 | else resolve(value);
7 | };
8 |
9 | callback.call(this, ...args, handleErrorAndValue);
10 | });
11 | };
12 | }
13 |
14 | function adder(x, y, handleErrorAndValue) {
15 | const value = x + y;
16 | if (typeof value !== 'number') {
17 | const error = new Error('Not a number');
18 | handleErrorAndValue(error, null);
19 | } else {
20 | handleErrorAndValue(null, value);
21 | }
22 | }
23 |
24 | const promisifiedAdder = promisify(adder);
25 |
26 | promisifiedAdder(1, 2)
27 | .then(console.log) // This would log 3.
28 | .catch(console.error);
29 |
30 | promisifiedAdder(1, 'foobar').then(console.log).catch(console.error); // An error would be caught and logged.
31 |
--------------------------------------------------------------------------------
/algorithms/remove-characters/remove-characters.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/remove-characters
2 |
3 | function removeBs(input) {
4 | let output = [];
5 |
6 | for (let char of input) {
7 | if (char !== 'b') {
8 | output.push(char);
9 | }
10 | }
11 |
12 | return output.join('');
13 | }
14 |
15 | function removeACs(input) {
16 | let index = 0;
17 |
18 | while (index + 1 < input.length) {
19 | if (input.slice(index, index + 2) === 'ac') {
20 | input = input.slice(0, index) + input.slice(index + 2);
21 | index = 0;
22 | } else {
23 | index++;
24 | }
25 | }
26 |
27 | return input;
28 | }
29 |
30 | export function removeChars(input) {
31 | return removeACs(removeBs(input));
32 | }
33 |
--------------------------------------------------------------------------------
/algorithms/remove-characters/remove-characters.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { removeChars } from './remove-characters';
3 |
4 | describe('removeChars', () => {
5 | it('', () => {
6 | expect(removeChars('')).toEqual('');
7 | expect(removeChars('a')).toEqual('a');
8 | expect(removeChars('bbb')).toEqual('');
9 | expect(removeChars('ccc')).toEqual('ccc');
10 | expect(removeChars('ab')).toEqual('a');
11 | expect(removeChars('abc')).toEqual('');
12 | expect(removeChars('cabbaabcca')).toEqual('caa');
13 | expect(removeChars('aaaaba')).toEqual('aaaaa');
14 | expect(removeChars('abbbaaccbbaccab')).toEqual('a');
15 | expect(removeChars('cabaaaacccaacccbabaccaa')).toEqual('caaa');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/algorithms/repeated-chars/repeated-chars.js:
--------------------------------------------------------------------------------
1 | function getRepeated(string) {
2 | // {
3 | // h: {
4 | // start: 0
5 | // end: 0
6 | // },
7 | // l: {
8 | // start: 2
9 | // end: 8
10 | // },
11 | // }
12 |
13 | const charsPositions = new Map();
14 |
15 | for (let index = 0; index < string.length; index++) {
16 | let char = string[index];
17 |
18 | if (charsPositions.has(char)) {
19 | charsPositions.set(char, {
20 | start: charsPositions.get(char).start,
21 | end: index,
22 | });
23 | } else {
24 | charsPositions.set(char, {
25 | start: index,
26 | end: index,
27 | });
28 | }
29 | }
30 |
31 | const positions = [];
32 |
33 | for (let [_, { start, end }] of charsPositions) {
34 | if (start !== end) positions.push([start, end]);
35 | }
36 |
37 | return positions;
38 | }
39 |
40 | // ===== repeated chars =====
41 |
42 | const input = 'hellooooloo';
43 | const output = getRepeated(input);
44 |
45 | console.log(output); // [(2,3), (4,7), (9,10)]
46 |
--------------------------------------------------------------------------------
/algorithms/testing-framework/testing-framework.js:
--------------------------------------------------------------------------------
1 | function describe(testSuiteName, func) {
2 | console.log(`beginning test suite ${testSuiteName}`);
3 | try {
4 | func();
5 | console.log(`successfully completed test suite ${testSuiteName}`);
6 | } catch ({ testCaseName, errorMessage }) {
7 | console.error(
8 | `failed running test suite ${testSuiteName} on test case ${testCaseName} with error message ${errorMessage}`
9 | );
10 | }
11 | }
12 |
13 | function it(testCaseName, func) {
14 | console.log(`beginning test case ${testCaseName}`);
15 | try {
16 | func();
17 | console.log(`successfully completed test case ${testCaseName}`);
18 | } catch (errorMessage) {
19 | throw { testCaseName, errorMessage };
20 | }
21 | }
22 |
23 | function expect(actual) {
24 | return {
25 | toExist: () => {
26 | if (actual === null || actual === undefined) {
27 | throw `expected value to exist but got ${actual}`;
28 | }
29 | },
30 | toBe: (expected) => {
31 | if (actual !== expected) {
32 | throw `expected ${JSON.stringify(actual)} to be ${JSON.stringify(
33 | expected
34 | )}`;
35 | }
36 | },
37 | toBeType: (type) => {
38 | if (typeof actual !== type) {
39 | throw `expected ${JSON.stringify(
40 | actual
41 | )} to be of type ${type} but got ${typeof actual}`;
42 | }
43 | },
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/algorithms/throttle/throttle.js:
--------------------------------------------------------------------------------
1 | function throttle(callback, delay) {
2 | let timerID;
3 | let lastCalledTime = 0;
4 |
5 | const throttledFunction = function (...args) {
6 | const currentTime = Date.now();
7 | const timeSinceLastCall = currentTime - lastCalledTime;
8 | const delayRemaining = delay - timeSinceLastCall;
9 |
10 | if (delayRemaining <= 0) {
11 | lastCalledTime = currentTime;
12 | callback.apply(this, args);
13 | } else {
14 | clearTimeout(timerID);
15 | timerID = setTimeout(() => {
16 | lastCalledTime = Date.now();
17 | callback.apply(this, args);
18 | }, delayRemaining);
19 | }
20 | };
21 |
22 | throttledFunction.cancel = function () {
23 | clearTimeout(timerID);
24 | };
25 |
26 | return throttledFunction;
27 | }
28 |
--------------------------------------------------------------------------------
/algorithms/traverse-dom-level-by-level/traverse-dom-level-by-level.js:
--------------------------------------------------------------------------------
1 | // https://bigfrontend.dev/problem/Traverse-DOM-level-by-level
2 |
3 | export function flatten(root) {
4 | if (!root) return [];
5 |
6 | let result = [];
7 | let queue = [root];
8 |
9 | while (queue.length > 0) {
10 | let node = queue.shift();
11 | result.push(node);
12 |
13 | for (let child of node.children) {
14 | queue.push(child);
15 | }
16 | }
17 |
18 | return result;
19 | }
20 |
--------------------------------------------------------------------------------
/algorithms/traverse-dom-level-by-level/traverse-dom-level-by-level.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { flatten } from './traverse-dom-level-by-level';
3 |
4 | const createNode = (name, children = []) => ({
5 | name,
6 | children,
7 | });
8 |
9 | describe('flatten', () => {
10 | it('', () => {
11 | expect(flatten(null)).toEqual([]);
12 | });
13 |
14 | it('', () => {
15 | const button = createNode('button');
16 | const p = createNode('p', [button]);
17 | const div = createNode('div', [p]);
18 | const p2 = createNode('p');
19 | const div2 = createNode('div', [p2, div]);
20 | const img = createNode('img');
21 | const a = createNode('a', [img]);
22 | const p3 = createNode('p', [a]);
23 | const button1 = createNode('button');
24 | const p4 = createNode('p', [button1]);
25 | const root = createNode('div', [p4, p3, div2]);
26 |
27 | const tree = root;
28 |
29 | expect(flatten(tree)).toEqual([
30 | root,
31 | p4,
32 | p3,
33 | div2,
34 | button1,
35 | a,
36 | p2,
37 | div,
38 | img,
39 | p,
40 | button,
41 | ]);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/algorithms/trending-stocks/trending-stocks.js:
--------------------------------------------------------------------------------
1 | const SYMBOLS_API_BASE_URL =
2 | 'https://api.frontendexpert.io/api/fe/stock-symbols';
3 | const MARKET_CAPS_API_BASE_URL =
4 | 'https://api.frontendexpert.io/api/fe/stock-market-caps';
5 | const PRICES_API_BASE_URL = 'https://api.frontendexpert.io/api/fe/stock-prices';
6 |
7 | function getTopMarketCaps(marketCaps) {
8 | return marketCaps.sort(
9 | (cap1, cap2) => cap2['market-cap'] - cap1['market-cap']
10 | );
11 | }
12 |
13 | function symbolToMarketCap(topMarkets) {
14 | return topMarkets.reduce(
15 | (acc, market) => ({
16 | ...acc,
17 | [market['symbol']]: market['market-cap'],
18 | }),
19 | {}
20 | );
21 | }
22 |
23 | function symbolToName(symbols) {
24 | return symbols.reduce(
25 | (acc, symbol) => ({
26 | ...acc,
27 | [symbol['symbol']]: symbol['name'],
28 | }),
29 | {}
30 | );
31 | }
32 |
33 | async function trendingStocks(n) {
34 | const [marketCapsPromise, symbolsPromise] = await Promise.all([
35 | fetch(MARKET_CAPS_API_BASE_URL),
36 | fetch(SYMBOLS_API_BASE_URL),
37 | ]);
38 |
39 | const [marketCaps, symbols] = await Promise.all([
40 | marketCapsPromise.json(),
41 | symbolsPromise.json(),
42 | ]);
43 |
44 | const topMarketCaps = getTopMarketCaps(marketCaps);
45 | const nTopMarkets = topMarketCaps.slice(0, n);
46 | const topSymbols = nTopMarkets.map((market) => market['symbol']);
47 |
48 | const prices = await (
49 | await fetch(`${PRICES_API_BASE_URL}?symbols=${JSON.stringify(topSymbols)}`)
50 | ).json();
51 | const symbolToMarketCapMap = symbolToMarketCap(nTopMarkets);
52 | const symbolToNameMap = symbolToName(symbols);
53 |
54 | return prices.map((price) => ({
55 | ...price,
56 | 'market-cap': symbolToMarketCapMap[price['symbol']],
57 | name: symbolToNameMap[price['symbol']],
58 | }));
59 | }
60 |
--------------------------------------------------------------------------------
/architecture/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Frontend Architecture
4 |
5 | Research, studies, and practice on Frontend Architecture.
6 |
7 | ## Frontend Design & Architecture
8 |
9 | - [Practical frontend architecture](https://jaredgorski.org/writing/14-practical-frontend-architecture)
10 | - [How to structure frontend application](https://michalzalecki.com/elegant-frontend-architecture)
11 | - [Front-end JavaScript single page application architecture](https://marcobotto.com/blog/frontend-javascript-single-page-application-architecture)
12 | - [How to reevaluate your frontend architecture](https://increment.com/frontend/how-to-reevaluate-your-frontend-architecture)
13 | - [A different approach to frontend architecture](https://dev.to/huytaquoc/a-different-approach-to-frontend-architecture-38d4)
14 | - [Clean Architecture on Frontend](https://bespoyasov.me/blog/clean-architecture-on-frontend)
15 | - [Clean Architecture](https://www.youtube.com/watch?v=ThgqBecaq_w&ab_channel=DoNotMerge)
16 | - [How to Re-evaluate Your Frontend Architecture and Improve its Design?](https://www.simform.com/blog/frontend-architecture)
17 | - [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
18 | - [Who is a frontend architect and how to become one](https://www.youtube.com/watch?v=Aam2hcvrYEs&ab_channel=MaksimIvanov)
19 |
20 | ## Data Fetching
21 |
22 | - [Data Fetching Patterns in Single-Page Applications](https://martinfowler.com/articles/data-fetch-spa.html)
23 |
24 | ## Code Management & Strategy
25 |
26 | - [monorepo.tools](https://monorepo.tools)
27 | - [Monorepo & tools](https://www.youtube.com/watch?v=jUIxkryvWA0&list=PLakNactNC1dE8KLQ5zd3fQwu_yQHjTmR5&index=6)
28 |
29 | ## Software Architecture
30 |
31 | - [Fundamentals of Software Architecture](https://www.thoughtworks.com/insights/podcasts/technology-podcasts/fundamentals-software-architecture)
32 | - [Book notes: Fundamentals of Software Architecture](https://danlebrero.com/2021/11/17/fundamentals-of-software-architecture-summary)
33 |
34 | ## Case Studies
35 |
36 | - [Prototyping a Smoother Map](https://medium.com/google-design/google-maps-cb0326d165f5)
37 | - [Building the Google Photos Web UI](https://medium.com/google-design/google-photos-45b714dfbed1)
38 | - [About JIRA's front-end architecture](https://www.youtube.com/watch?v=CbHETl96qOk&ab_channel=HasgeekTV)
39 | - [Rendering like Butter – a Confluence Whiteboards Story](https://www.atlassian.com/engineering/rendering-like-butter-a-confluence-whiteboards-story)
40 |
41 | ## Infrastructure
42 |
43 | - [What is HTTP?](https://www.cloudflare.com/en-gb/learning/ddos/glossary/hypertext-transfer-protocol-http)
44 | - [HOW HTTPS WORKS](https://howhttps.works)
45 | - [Journey to HTTP/2](https://kamranahmed.info/blog/2016/08/13/http-in-depth)
46 | - [HTTP/3 From A To Z: Core Concepts](https://www.smashingmagazine.com/2021/08/http3-core-concepts-part1)
47 | - [What is DNS? | How DNS works](https://www.cloudflare.com/en-gb/learning/dns/what-is-dns)
48 | - [DNS and How does it work?](https://www.youtube.com/watch?v=Wj0od2ag5sk&ab_channel=theroadmap)
49 |
50 |
51 |
--------------------------------------------------------------------------------
/architecture/architecture/draft.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Architecture
4 |
5 | ## Storage
6 |
7 | - Cookies: The main method of local storage before HTML5, the size is only 4k, the HTTP request header will automatically bring cookies, and the compatibility is good
8 | - LocalStorage: a new feature of HTML5, persistent storage, even if the page is closed, it will not be cleared, stored in the form of key-value pairs, the size is 5M
9 | - SessionStorage: a new feature in HTML5, the operation and size are the same as localStorage, and the difference from localStorage is that sessionStorage is cleared when a tab (page) is closed, and sessionStorage between different tabs is not interoperable
10 | - IndexedDB: NoSQL database, analogous to MongoDB, uses key-value pairs for storage, operates the database asynchronously, supports transactions, and can store more than 250MB of storage space, but IndexedDB is limited by the same-origin policy
11 | - Web SQL: it is a relational database simulated on the browser. Developers can operate Web SQL through SQL statements. It is a set of independent specifications other than HTML5 and has poor compatibility.
12 |
13 | ## Infrastructure
14 |
15 | - Browser cache
16 | - DNS
17 | - HTTP, HTTPS, HTTP/2, HTTP/3
18 |
19 | ## IO
20 |
21 | - HTTP/Fetch request
22 | - Web Sockets
23 | - Service Workers
24 | - Server-Sent Events
25 |
26 | ## UI / State
27 |
28 | - State Management
29 | - App State Data Structure
30 | - Component Tree
31 |
32 | ## Scalability
33 |
34 | - State structure: fast access, fast update
35 | - Reduce request to backend, cache
36 | - Web performance: ship less JavaScript, bundle size optimization, runtime performance optimization
37 | - Image optimization: srcset for (responsive) images, intersection observer, download low-quality images and smoothly change to a better quality image
38 | - Background operation and caching with service workers
39 | - Code splitting and browser caching
40 |
41 |
42 |
--------------------------------------------------------------------------------
/architecture/monorepo/draft.md:
--------------------------------------------------------------------------------
1 | # Monorepos
2 |
3 | - A monorepo is a single repository containing multiple distinct projects, with well-defined relationships.
4 | - It's not a multirepo with no relationship or "code colocation"
5 |
--------------------------------------------------------------------------------
/images/tag-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/images/tag-input.png
--------------------------------------------------------------------------------
/interviews/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Interviews
4 |
5 | ## Draft
6 |
7 | - coding
8 | - algorithm
9 | - javascript
10 | - product building: e.g. Create an email Client like MS Outlook; Create a chat interface like MS teams; Create a Notification interface like MS teams
11 | - system design
12 |
13 | ## JavaScript
14 |
15 | - Explain JavaScript main thread and worker threads. How do you communicate between main thread and worker threads? How can worker threads impact main thread?
16 | - How web workers work
17 | - What causes memory leak in JavaScript and how to deal with them? How would you locate memory leaks in your code?
18 | - If there is an infinite loop in JavaScript code, what will happen to the code and what will the user experience?
19 | - If a JavaScript file is 10 MB, how much memory will it take up on user's computer when it's running?
20 | - fetch / promises
21 | - implement a promiseAll with the Promise
22 | - if any of the promises fail, it should reject everything
23 | - if every promise succeeds, it should return all the values resolved from the promises in an array
24 | - Coding: Fix a callback hell
25 | - write polyfill of promise
26 | - [resource](https://medium.com/swlh/implement-a-simple-promise-in-javascript-20c9705f197a)
27 | - write a polyfill of `promise.all`
28 | - [resource](https://zhisun.medium.com/implementing-promise-all-promise-race-promise-resolve-and-promise-reject-in-javascript-ddc624065554)
29 | - memoization
30 | - [resource](https://www.iamtk.co/writing-a-memoization-function-from-scratch)
31 | - closure
32 | - debounce
33 | - throttle
34 | - write an Event Emitter
35 | - write a `Pub-Sub class`
36 | - How the event loop works: tasks, microtasks
37 |
38 | ## CSS
39 |
40 | - What is the difference between rem and em
41 | - Explain vw and vh and how to use them.
42 |
43 | ## React
44 |
45 | - Components
46 | - Tabs
47 | - Accordion
48 | - Photo Gallery
49 | - Other possible components - [Refer to Bootstrap's list](https://getbootstrap.com/docs/4.0/components/)
50 | - Modal
51 | - Input (Address delivery)
52 | - Progress Bar
53 | - Apps
54 | - Sortable Data Table (with extensions for filtering)
55 | - TODO list
56 | - Kanban Board
57 | - Search filters
58 | - Games
59 | - Tic-tac-toe
60 | - Whack-a-mole
61 | - Wordle
62 | - Tetris (advanced)
63 | - Snake (advanced)
64 | - Fetch API in react
65 | - State Structure
66 | - Group related state
67 | - Avoid contradictions in state
68 | - Avoid redundant state
69 | - Avoid duplication in state
70 | - Avoid deeply nested state
71 |
72 | ## System Design
73 |
74 | - [Spotify Playlist — Frontend System Architecture](https://www.iamtk.co/series/crafting-frontend/spotify-playlist-frontend-system-architecture)
75 | - Web Performance considerations
76 |
77 | ## Resources
78 |
79 | ### FAANG
80 |
81 | - [My Google Front End Interview Questions](https://medium.com/codex/my-google-front-end-interview-questions-bca96925c16a)
82 | - [Google JavaScript Interview With A Frontend Engineer](https://www.youtube.com/watch?v=Rs7ARD5TCFU&ab_channel=Cl%C3%A9mentMihailescu)
83 | - [JavaScript interview with a Google engineer](https://www.youtube.com/watch?v=10WnvBk9sZc&t=166s&ab_channel=interviewing.io)
84 | - [Google Frontend Interview With A Frontend Expert](https://www.youtube.com/watch?v=ai1zmNO5Z3E&ab_channel=Cl%C3%A9mentMihailescu)
85 | - [Facebook / Meta front end engineer interview (questions, process, prep)](https://igotanoffer.com/blogs/tech/facebook-front-end-engineer-interview)
86 | - [8 Weeks Study Plan To Be A Frontend Engineer (MANNG edition) — Week 1](https://medium.com/@FrontendJirachi/8-weeks-study-plan-to-be-a-frontend-engineer-manng-edition-week-1-758699e61e8c)
87 | - [Software Engineer 2 UI Interview at Microsoft](https://dev.to/dhilipkmr/software-engineer-2-ui-interview-at-microsoft-1i0b)
88 |
89 | ### JavaScript
90 |
91 | - [JS mock interview - senior](https://www.youtube.com/watch?v=8eRVxE9PEF0)
92 | - [Promise Interview Questions](https://www.youtube.com/watch?v=3Hgac-wHiRE&ab_channel=SofiaGoyal)\*\*
93 | - [Implementing a simple Promise in Javascript](https://medium.com/swlh/implement-a-simple-promise-in-javascript-20c9705f197a)\*\*
94 |
95 | ### React
96 |
97 | - [Good practices for loading, error, and empty states in React](https://blog.whereisthemouse.com/good-practices-for-loading-error-and-empty-states-in-react)
98 | - [How to think about React state management](https://blog.whereisthemouse.com/how-to-think-about-react-state-management)
99 |
100 | ### Frontend System Design - Architecture
101 |
102 | - [Architecture for frontend - Hydration](https://www.youtube.com/watch?v=iR5T2HefqKk&t=12s&ab_channel=RyanCarniato)
103 | - [BigFrontEnd.dev](https://bigfrontend.dev)
104 | - [Cracking the Frontend System Design Interview](https://medhatdawoud.net/blog/cracking-frontend-system-design-interview)
105 | - [Framework for Front End System Design interviews - draft](https://pietropassarelli.com/front-end-system-design.html)
106 | - [Front End System Design Fundamentals](https://www.youtube.com/watch?v=NEzu4FD25KM)
107 | - [Front-End Engineer — System Design](https://www.youtube.com/c/FrontEndEngineer)
108 | - [Front-End Prep](https://www.notion.so/evgeniiray/Front-End-Preparation-a0ac842415a04ddf9319718ea6ba22a4)
109 | - [Front-end System Design — Develop Netflix from scratch](https://medium.com/itnext/front-end-system-design-develop-netflix-from-scratch-2bb65cb8be52)
110 | - [Front-End System Design Interview Guide](https://www.youtube.com/watch?v=5llb2fGKl9s&ab_channel=JSer)
111 | - [Frontend High Level System Design of Youtube](https://www.youtube.com/watch?v=QJe0cBjlgog&ab_channel=UncommonGeeks)
112 | - [Frontend Interview Handbook](https://www.frontendinterviewhandbook.com)
113 | - [Frontend Interview Preparation Resources](https://leetcode.com/discuss/interview-question/1074798/Frontend-Interview-Preparation-%281-4-years%29-Level-1-and-Level-2)
114 | - [Frontend Jirachi — System Design](https://www.youtube.com/channel/UCWeb5PgnIl3Dxv8lmMS0ApQ)
115 | - [Frontend System Design - youtube](https://www.youtube.com/watch?v=x9NgcwwFp7s&ab_channel=WebDevInterview)
116 | - [Frontend System Design — Client-Server API](https://www.youtube.com/watch?v=5kBI1LadXVA)
117 | - [Frontend System Design — Design Real-Time Updates](https://www.youtube.com/watch?v=8Uyka3fzXek)
118 | - [Frontend System Design — Part 1](https://hemdan.hashnode.dev/frontend-system-design-part-1)
119 | - [Frontend System Design — Part 2](https://hemdan.hashnode.dev/frontend-system-design-part-2)
120 | - [Frontend System Design — Web Storages](https://www.youtube.com/watch?v=CegvBL1L8qA)
121 | - [Frontend System Design Framework](https://medium.com/@wastecleaner/frontend-system-design-framework-5ba6e075b3b2)
122 | - [Frontend System Design interviews - the definitive guide](https://frontendmastery.com/posts/frontend-system-design-interview-guide)
123 | - [Frontend System Design](https://www.youtube.com/channel/UC6YpkaZsAcAvPNt4rLiS7dg)
124 | - [Frontend Vs. Backend System Design Interviews](https://www.zhenghao.io/posts/system-design-interviews)
125 | - [Great Frontend: prep for frontend interviews](https://www.greatfrontend.com)
126 | - [Heuristics for Frontend System Design Interviews](https://sherryhsu.medium.com/heuristics-for-frontend-system-design-interviews-a832f8d0e836)
127 | - [How to Ace Your Frontend System Design Interview](https://medium.com/@FrontendJirachi/how-to-ace-your-frontend-system-design-42c7b357416b)
128 | - [How to prepare your Frontend System Design Interview](https://www.youtube.com/watch?v=JhcW0fuR_ig&ab_channel=ICodeIt)
129 | - [The complete guide to the System Design Interview in 2022](https://www.educative.io/blog/complete-guide-system-design-interview)
130 | - [Uber - Front end - Phone screen](https://leetcode.com/discuss/interview-question/1064199/uber-front-end-phone-screen-reject)
131 |
132 |
133 |
--------------------------------------------------------------------------------
/interviews/system-design.md:
--------------------------------------------------------------------------------
1 | # System Design
2 |
3 | RADIO framework
4 |
5 | - **Requirements Exploration**: Understand the problem thoroughly and determine the scope by asking a number of clarifying questions.
6 | - **Architecture/High-level Design**: Identify the key components of the product and how they are related to each other.
7 | - **Data Model**: Describe the various data entities, the fields they contain and which component(s) they belong to.
8 | - **Interface Definition (API)**: Define the interface (API) between components in the product, functionality of each API, their parameters and responses.
9 | - **Optimizations and Deep Dive**: Discuss about possible optimization opportunities and specific areas of interest when building the product.- Requirements
10 |
11 | ## Requirements Exploration
12 |
13 | It's expected to clarify the scope and requirements needed in the discussion
14 |
15 | - Mindset: Treat your interview as product manager and engineering manager: asking question to clarify the problem
16 | - Behavior: Ask the questions and take notes
17 | - What's the feature: the define the scope and the problem | the the limits of the feature
18 | - What's the user flow we need to cover
19 | - Understand different requirements
20 | - Functional requirements: how will it work
21 | - Core functionality: Think about the core flow the user will use the product
22 | - High level estimation: data volumn, peak traffic
23 | - Non-functional requirements:
24 | - Different devices: desktop, mobile, tablets - think of SSR (can't use window, should use CSS media queries)
25 | - Which browsers should we support?
26 | - SEO: different content, SSR, fast (core web vitals - metrics - small bundles - lazy load)
27 | - Localized: how to use internationalization
28 | - UX: smooth experience (rendering page and navigation), animation, handle requests (loading, errors, success, try again)
29 |
30 | ## High-Level Architecture
31 |
32 | Identify the key components of the product and how they are related to each other
33 |
34 | - Separation of Concerns: draw the rectangles and the interactions between them
35 | - Client
36 | - User interface, design system components
37 | - "Controller" (performs the request, 'store' data in the client, provide the data to the UI)
38 | - Store: states and data from the server
39 | - Server: API (HTTP/WebSocket)
40 | - Interaction between components: show the interaction with arrows
41 | - Managing state / Data model
42 | - What are the states involved in the problem?
43 | - Where do we store the client state?
44 | - User behavior: e.g. Open dialog, click buttons, click a link, comment, create a post
45 | - Managing URL: depending on the url, it will fetch something else
46 | - Rendering Architecture
47 | - Client side rendering
48 | - Server side rendering
49 | - Streaming SSR
50 |
51 | ## Data Model
52 |
53 | Describe the various data entities, the fields they contain and which component(s) they belong to
54 |
55 | - Server data: usually from a database
56 | - Client data
57 | - Persistent data: it has to be sent to the server and saved into a database
58 | - Real database - request
59 | - Ephemeral data: temporary state that lasts for a short time
60 | - URL:
61 | - Good for current page's state, storing page number, filter settings
62 | - Very fragile, unsafe: visible through the URL
63 | - Cookies
64 | - Easy to use, easy to support.
65 | - Don't trigger a re-render for a component when changing the cookie. Should do extra work
66 | - Good for session management (user session, tracking user behavior, personalization - e.g. language preference)
67 | - Can increase HTTP overhead (storage capacity: 4kb per origin)
68 | - Local storage
69 | - Persistent storage but only for the specific device
70 | - Slow synchronous API
71 | - Only strings (encode, decode each time if data is not string)
72 | - Good for offline usage, saving drafts
73 | - Session storage
74 | - The session is finished when the tab is closed
75 | - Cache API
76 | - Storage inside service workers
77 | - Good for offline mode, cache of assets like HTML, CSS, and JS
78 | - Indexed DB: more complex DB
79 | - Large capacity, it's fast, available in service workers
80 | - Not a long term data storage (ITP - data deleted after 7 days)
81 | - Good for large amount of data, complex data
82 |
83 | ## Interface Definition (API)
84 |
85 | - API requests it should handle
86 | - HTTP methods: put, post, get
87 | - WebSockets
88 | - Basic payload
89 | - Data contract
90 |
91 | ## Interface Definition (UI)
92 |
93 | - UI / mockup for the problem
94 | - UI
95 | - Component Tree
96 | - User interaction / behavior
97 |
98 | ## Going in-depth
99 |
100 | - Components
101 | - Think about small reusable components
102 | - Think about if it could also be part of a design system
103 | - API
104 | - HTTP methods
105 | - API payload
106 | - headers: cookie / jwt / auth token
107 | - GraphQL / Rest API / BFF architecture / WebSockets / Long Poling / Server-Sent Event
108 | - Rendering
109 | - Partially render
110 | - Loading indicator
111 | - Loading Spinner or Skeleton
112 | - Error handling
113 | - Show the error in the content part
114 | - Show a dialog/modal with the error description and the possibility to retry the request
115 | - 401 - unauthorized: clean the cookies and redirect to the login page
116 | - 403 - forbidden: redirect to a forbidden page
117 |
118 | ## Going even deeper
119 |
120 | - Performance
121 | - In front end, performance typically refers to a few things - loading speed, how fast the UI responds to user interactions and memory space (heap) required by the component.
122 | - Network performance > Bundles
123 | - Small bundles / LazyLoading: vendor, page1, page2
124 | - Pre-fetching for other parts of the flow
125 | - Gzip / Brotli to compress the bundle and make it very small
126 | - Cache requests
127 | - Service workers/web workers
128 | - libraries like react-query / swr to get the data from cache and revalidate when it is stale
129 | - Infinite scroll / Long list: Sliding Window
130 | - Render the nodes that are only showing in the window
131 | - Page Stack: when clicking the back button | the bad part: the stack can be very big, we need to manage the stack size to not use too much memory in the browser
132 | - Small images/images size based on the device
133 | - Rendering performance > SSR
134 | - Loading speed - The less JavaScript the component contains, the less JavaScript the browser has to download to load the component and the lower the network request time. It's also important to modularize components and allow users to download only the necessary JavaScript modules needed for their use case.
135 | - Responsiveness to user interactions
136 | - If a user interaction results in displaying of data that has to be loaded over the network, there will be a delay between the user interaction and updating of the UI. Minimizing that delay or removing it entirely is the key to improving responsiveness.
137 | - JavaScript in a browser is single-threaded. The browser can only do execute one line of code at any one time. The less work (JavaScript executed, DOM updates) the component has to do when a user does something on the page, the faster the component can update the UI to respond to the changes.
138 | - Tracking events: postpone events and run them only on the browser's idle time
139 | - Memory space - The more memory your component takes up on the page, the slower the browser performs and the experience will feel sluggish/janky. If your component has to render hundreds/thousands of items (e.g. number of images in a carousel, number of items in a selector), memory space might become significant.
140 | - User Experience
141 | - UX might not fall squarely under engineering but good front end engineers have a good understanding of UX and build UI with great UX. There are too many UX practices to be aware of, but the most common ones/low hanging fruits are:
142 | - Reflect the state of the component to the user - If there's a pending background request, show a spinner. If there's an error, make sure to display it instead of silently failing.
143 | - Display an empty state if there are no items in a list, instead of not rendering anything.
144 | - Destructive actions should have a confirmation step, especially irreversible ones.
145 | - Disable interactive elements if they trigger an async request! Prevents double firing of events in the case of accidental double-clicking (possible for people with motor disabilities).
146 | - If there are search inputs involved, each keystroke should not fire a network request.
147 | - Handle extreme cases
148 | - Strings can be really long/short and your UI should not look weird in either case. For long strings, they can have their contents truncated and hidden behind a "View more" button.
149 | - If there are many items to display within a component, they shouldn't all be displayed on the screen at once and making the page extremely long/wide. Paginate the items or contain them within a maximum width/height container.
150 | - Keyboard friendliness - This involves making sure the component is keyboard-friendly
151 | - Add shortcuts to make the component more usable by keyboard-only users
152 | - Ensure that elements can be focused and tab order within the component is correct
153 | - Accessibility is part of UX but will be covered in a later section
154 | - SEO
155 | - Friendly URL
156 | - Index URL for the search with pre-selected filters: the user when clicking the link, it renders the search page with the pre-selected filters
157 | - Accessibility
158 |
--------------------------------------------------------------------------------
/javascript/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # JavaScript
4 |
5 | - [Promises](promises)
6 | - [Memoization](memoization)
7 | - [Event Emitter](event-emitter)
8 | - [Debounce](debounce)
9 | - [Throttle](throttle)
10 |
11 | ## Resources
12 |
13 | ### Promises
14 |
15 | - [What is the Difference Between Promises and Observables?](https://blog.bitsrc.io/promises-vs-observables-674f4bc8ca5e)
16 | - [6 Promise Problems that Front-end Engineers Should Master for Interviews](https://towardsdev.com/6-promise-problems-that-front-end-engineers-should-master-for-interviews-8281848d7721)
17 | - [Implementing a simple Promise in Javascript](https://medium.com/swlh/implement-a-simple-promise-in-javascript-20c9705f197a)
18 |
19 | ### Design
20 |
21 | - [S.O.L.I.D Principles with JS examples](https://medium.com/front-end-weekly/s-o-l-i-d-principles-with-js-examples-db95b44e82e)
22 |
23 | ### Under the hood
24 |
25 | - [How JavaScript Works Under the Hood, Explained for Dummies](https://javascript.plainenglish.io/how-javascript-works-under-the-hood-explained-for-dummies-216ce155183c)
26 |
27 |
28 |
--------------------------------------------------------------------------------
/javascript/create-dom/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Write a createDom function that takes in a required root parameter,
3 | * which is an object representation of a DOM tree's root node or a string
4 | * representation of a text node.
5 | *
6 | * If the root parameter is an object, then a DOM Element node is returned.
7 | * This object will have one required property: type, which corresponds to
8 | * the tag name of the element being created (e.g. "div"), as well as two
9 | * optional properties: children and attributes. If children exists, it will
10 | * be an array of objects in the same format as the root parameter.
11 | *
12 | * Each value in this array will be a child of the returned node,
13 | * in the order of the array. Additionally, if a child is a string instead
14 | * of an object, then that string should be used as text content.
15 | * If attributes exists, it will be an object, with each key corresponding
16 | * to an attribute name and each value corresponding to an attribute value.
17 | * These attributes are each attributes of the node.
18 | */
19 |
20 | function createDom(root) {
21 | if (typeof root === 'string') {
22 | return document.createTextNode(root);
23 | }
24 |
25 | const rootDOM = document.createElement(root.type);
26 |
27 | if (root.attributes) {
28 | for (const [type, value] of Object.entries(root.attributes)) {
29 | rootDOM.setAttribute(type, value);
30 | }
31 | }
32 |
33 | if (root.children) {
34 | for (const child of root.children) {
35 | rootDOM.appendChild(createDom(child));
36 | }
37 | }
38 |
39 | return rootDOM;
40 | }
41 |
--------------------------------------------------------------------------------
/javascript/debounce/README.md:
--------------------------------------------------------------------------------
1 | # Debounce
2 |
3 | - Setup a timer
4 | - Returns a function to be called (debounced function)
5 | - Use `setTimeout` to setup a timer
6 | - Clean the timer every time the function is called (with `clearTimeout`)
7 |
8 | [Example](index.js)
9 |
--------------------------------------------------------------------------------
/javascript/debounce/index.js:
--------------------------------------------------------------------------------
1 | function debounce(fn, timeout = 3000) {
2 | let timer;
3 |
4 | return (...args) => {
5 | clearTimeout(timer);
6 | timer = setTimeout(() => {
7 | fn(...args);
8 | }, timeout);
9 | };
10 | }
11 |
12 | function log(text) {
13 | console.log(text);
14 | }
15 |
16 | const debouncedLog = debounce(log);
17 |
--------------------------------------------------------------------------------
/javascript/deep-equal/index.js:
--------------------------------------------------------------------------------
1 | const Types = {
2 | string: 'string',
3 | boolean: 'boolean',
4 | number: 'number',
5 | array: 'array',
6 | object: 'object',
7 | NaN: 'NaN',
8 | null: 'null',
9 | undefined: 'undefined',
10 | };
11 |
12 | const primitives = [Types.string, Types.boolean, Types.number];
13 |
14 | function getType(value) {
15 | if (Array.isArray(value)) {
16 | return 'array';
17 | }
18 |
19 | if (typeof value === 'number' && isNaN(value)) {
20 | return Types.NaN;
21 | }
22 |
23 | if (primitives.includes(typeof value)) {
24 | return typeof value;
25 | }
26 |
27 | if (value === null) {
28 | return Types.null;
29 | }
30 |
31 | if (value === undefined) {
32 | return Types.undefined;
33 | }
34 |
35 | return 'object';
36 | }
37 |
38 | function deepEqual(valueOne, valueTwo) {
39 | const valueOneType = getType(valueOne);
40 | const valueTwoType = getType(valueTwo);
41 |
42 | if (valueOneType === Types.NaN && valueTwoType === Types.NaN) return true;
43 | if (valueOneType === Types.null && valueTwoType === Types.null) return true;
44 | if (valueOneType === Types.null || valueTwoType === Types.null) return false;
45 | if (valueOneType === Types.undefined && valueTwoType === Types.undefined)
46 | return true;
47 |
48 | if (primitives.includes(valueOneType)) return valueOne === valueTwo;
49 |
50 | if (valueOneType === Types.array && valueTwoType === Types.array) {
51 | if (valueOne.length !== valueTwo.length) return false;
52 |
53 | for (let index = 0; index < valueOne.length; index++) {
54 | if (!deepEqual(valueOne[index], valueTwo[index])) {
55 | return false;
56 | }
57 | }
58 |
59 | return true;
60 | }
61 |
62 | for (let key of Object.keys(valueOne)) {
63 | if (!valueTwo[key] || !deepEqual(valueOne[key], valueTwo[key])) {
64 | return false;
65 | }
66 | }
67 |
68 | for (let key of Object.keys(valueTwo)) {
69 | if (!valueOne[key] || !deepEqual(valueOne[key], valueTwo[key])) {
70 | return false;
71 | }
72 | }
73 |
74 | return true;
75 | }
76 |
77 | // get type of
78 | console.log(getType(null));
79 | console.log(getType(undefined));
80 | console.log(getType(NaN));
81 | console.log(getType(1));
82 | console.log(getType('a'));
83 | console.log(getType(true));
84 | console.log(getType([1, 2, 3]));
85 | console.log(getType({ hey: 1 }));
86 | console.log();
87 |
88 | // primitives
89 | console.log(deepEqual(null, null) === true);
90 | console.log(deepEqual(undefined, undefined) === true);
91 | console.log(deepEqual(NaN, NaN) === true);
92 | console.log(deepEqual(null, 1) === false);
93 | console.log(deepEqual(1, '1') === false);
94 | console.log(deepEqual(1, true) === false);
95 | console.log(deepEqual('a', true) === false);
96 | console.log(deepEqual(1, 2) === false);
97 | console.log(deepEqual(1, 1) === true);
98 | console.log(deepEqual('1', '2') === false);
99 | console.log(deepEqual('1', '1') === true);
100 | console.log(deepEqual(false, true) === false);
101 | console.log(deepEqual(true, true) === true);
102 | console.log();
103 |
104 | // array
105 | console.log(deepEqual([1, 2, 3], [1, 2, 3]) === true);
106 | console.log(deepEqual([1, 2], [1, 2, 3]) === false);
107 | console.log(deepEqual([1, 2, 3], [1, 2]) === false);
108 | console.log(deepEqual([1, 2, [1, 2]], [1, 2, [1, 2]]) === true);
109 | console.log(deepEqual([1, 2, [1, 2, 3]], [1, 2, [1, 2]]) === false);
110 | console.log(deepEqual([1, 2, [1, 2]], [1, 2, [1, 2, 3]]) === false);
111 | console.log(
112 | deepEqual([1, 2, { 1: [1, 2, 3] }], [1, 2, { 1: [1, 2, 3] }]) === true
113 | );
114 | console.log();
115 |
116 | // object
117 | console.log(deepEqual({}, {}) === true);
118 | console.log(deepEqual({ 1: 1 }, { 1: 1 }) === true);
119 | console.log(deepEqual({ 1: 1 }, { 1: 2 }) === false);
120 | console.log(deepEqual({ 2: 1 }, { 1: 1 }) === false);
121 | console.log(deepEqual({ 2: 1 }, { 1: 2 }) === false);
122 | console.log(deepEqual({ 2: [1, 2, 3] }, { 2: [1, 2, 3] }) === true);
123 | console.log(deepEqual({ 2: [1, 2, 3] }, { 2: [1, 2] }) === false);
124 | console.log(deepEqual({ 2: [1, 2] }, { 2: [1, 2, 3] }) === false);
125 | console.log(deepEqual({ 2: { 1: 1 } }, { 2: { 1: 1 } }) === true);
126 | console.log(deepEqual({ 2: { 1: 1 } }, { 2: { 1: 2 } }) === false);
127 | console.log(deepEqual({ 1: undefined, 2: 2 }, { 2: 2, 3: 3 }) === false);
128 |
--------------------------------------------------------------------------------
/javascript/event-emitter/README.md:
--------------------------------------------------------------------------------
1 | # Event Emitter
2 |
3 | [Implementation](index.js)
4 |
5 | ## API
6 |
7 | - `.on`: subscribe to an event
8 | - `.emit`: emit an event
9 | - `.off`: unsubscribe from an event
10 | - `.once`: subscribe an event to be emitted only one time
11 |
--------------------------------------------------------------------------------
/javascript/event-emitter/index.js:
--------------------------------------------------------------------------------
1 | export class EventEmitter {
2 | events = new Map();
3 |
4 | once(eventName, fn) {
5 | this._subscribe(eventName, fn, 'once');
6 | }
7 |
8 | on(eventName, fn) {
9 | this._subscribe(eventName, fn, 'on');
10 | }
11 |
12 | emit(eventName, ...args) {
13 | if (this.events.has(eventName)) {
14 | for (let fnObject of this.events.get(eventName)) {
15 | fnObject.fn(...args);
16 |
17 | if (fnObject.subscriber === 'once') {
18 | this.off(eventName, fnObject.fn);
19 | }
20 | }
21 | }
22 | }
23 |
24 | off(eventName, fn) {
25 | if (this.events.has(eventName)) {
26 | this.events.set(
27 | eventName,
28 | this.events.get(eventName).filter((event) => event.fn !== fn)
29 | );
30 | }
31 | }
32 |
33 | _subscribe(eventName, fn, type) {
34 | if (this.events.has(eventName)) {
35 | this.events.set(eventName, [
36 | ...this.events.get(eventName),
37 | { subscriber: type, fn },
38 | ]);
39 | } else {
40 | this.events.set(eventName, [{ subscriber: type, fn }]);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/javascript/event-emitter/tests/index.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from '../index.js';
2 |
3 | const eventEmitter = new EventEmitter();
4 |
5 | const fn1 = (param1, param2) => console.log('test 1', param1, param2);
6 |
7 | // subscribe the fn1 to the `test` event but it should run only once
8 | eventEmitter.once('test', fn1);
9 | eventEmitter.emit('test', 'param1', 'param2'); // log param1, param2
10 | eventEmitter.emit('test', 'param1', 'param2'); // doesn't log anything
11 |
12 | const fn2 = (param) => console.log('test 2', param);
13 |
14 | // subscribe the fn2 to the `test2` event and it can be called multiple times
15 | eventEmitter.on('test2', fn2);
16 | eventEmitter.emit('test2', 'param1'); // log param1
17 | eventEmitter.emit('test2', 'param2'); // log param2
18 |
19 | // unsubscribe the fn2 to the `test2` event so it shouldn't be called anymore
20 | eventEmitter.off('test2', fn2);
21 | eventEmitter.emit('test2', 'param1'); // doesn't log anything
22 |
--------------------------------------------------------------------------------
/javascript/memoization/README.md:
--------------------------------------------------------------------------------
1 | # Memoization
2 |
3 | - [memoize implementation](index.js)
4 |
5 | ## API
6 |
7 | Run the `javascript/memoization/benchmark/index.js` file to run the comparison between the actual function and its memoized version.
8 |
9 | ```bash
10 | node javascript/memoization/benchmark/index.js
11 | ```
12 |
13 | ### Running on the sum function
14 |
15 | The memoized version is `1.33%` faster than the pure version.
16 |
17 | ```bash
18 | ------- sum --------
19 | cold: 495.026ms
20 | cached: 371.011ms
21 | ------- // --------
22 | ```
23 |
24 | ### Running on the factorial function
25 |
26 | The memoized version is `298%` faster than the pure version.
27 |
28 | ```bash
29 | ------ factorial ------
30 | cold: 572.349ms
31 | cached: 1.919ms
32 | --------- // ---------
33 | ```
34 |
35 | ## Overview
36 |
37 | The API of the memoization function should be like this:
38 |
39 | ```javascript
40 | const memoizedFn **=** memoize(fn);
41 | ```
42 |
43 | The function receives a function as an input and returns the memoized function.
44 |
45 | In the first function call, it's still "cold", so every function call will be executed and then the output value will be cached for the subsequent calls.
46 |
47 | In any subsequent function call, the values are cached, so rather than executing the function body, it'll just return the cached value.
48 |
49 | Let's go to the implementation first and then do some benchmarks.
50 |
51 | The first thing is that the `memoize` function should receive a function as an input and return the memoized function. Simple steps:
52 |
53 | ```javascript
54 | function memoize(fn) {
55 | return fn;
56 | }
57 | ```
58 |
59 | So now we can just call the `memoize` function passing a new function and it will return the same input function.
60 |
61 | ```javascript
62 | const fn = (a, b) => a * b;
63 | const memoizedFn = memoize(fn);
64 |
65 | memoizedFn(1, 1); // 1 (first call: cold)
66 | memoizedFn(1, 1); // 1 (not memoized: it'll execute the function again)
67 | memoizedFn(1, 2); // 2 (first call: cold)
68 | ```
69 |
70 | To memoize the function, I will use the `Map` as a cache object. So every time the function is called, we can access the cache object using the arguments as a key and get the output.
71 |
72 | The cache object will look like this:
73 |
74 | ```javascript
75 | const cache = new Map();
76 |
77 | cache.set('1', 1);
78 | cache.get('1'); // 1
79 |
80 | cache.set('2', 2);
81 | cache.get('2', 2);
82 | ```
83 |
84 | The key of the cache object will be the arguments of the function and the value will be the output of the function call.
85 |
86 | So the algorithm is pretty simple:
87 |
88 | - get all the arguments and stringify them to build the key of the cache object
89 | - and verify in the cache if the key exists in it. If it does, return the output value
90 | - if it doesn't: call the function, store the output value in the cache, and return the output value
91 |
92 | Now implement each part:
93 |
94 | - get the arguments
95 |
96 | ```javascript
97 | (...args) => {};
98 | ```
99 |
100 | - stringify the arguments to build the key of the cache object
101 |
102 | ```javascript
103 | const key = JSON.stringify(args);
104 | ```
105 |
106 | - verify in the cache if the key exists in it. If it does, return the output value
107 |
108 | ```javascript
109 | if (cache.has(key)) {
110 | return cache.get(key);
111 | }
112 | ```
113 |
114 | - if it doesn't: call the function, store the output value in the cache
115 |
116 | ```javascript
117 | const result = fn(...args);
118 | cache.set(key, result);
119 | ```
120 |
121 | The final version:
122 |
123 | ```javascript
124 | function memoize(fn) {
125 | const cache = new Map();
126 |
127 | return (...args) => {
128 | const key = JSON.stringify(args);
129 |
130 | if (cache.has(key)) {
131 | return cache.get(key);
132 | }
133 |
134 | const result = fn(...args);
135 | cache.set(key, result);
136 |
137 | return result;
138 | };
139 | }
140 | ```
141 |
142 | To do the benchmark and make sure our memoization function works, let's see the example of `sum` and `factorial` functions.
143 |
144 | The sum is a pretty simple function and doesn't cost much, so we wouldn't see any significant improvements after caching the function calls.
145 |
146 | ```javascript
147 | function sum(a, b) {
148 | return a + b;
149 | }
150 | ```
151 |
152 | And now calling it:
153 |
154 | ```javascript
155 | const memoizedSum = memoize(sum);
156 |
157 | memoizedSum(1, 1); // 2 (not cached)
158 | memoizedSum(1, 1); // 2 (cached)
159 | ```
160 |
161 | To do a better comparison and make sure the memoization speed up the function calls, I created two simple helper functions to build the testing case.
162 |
163 | ```javascript
164 | function getNumbers(limit = 1000000) {
165 | let numbers = [];
166 |
167 | for (let i = 0; i < limit; i++) {
168 | numbers.push(i);
169 | }
170 |
171 | return numbers;
172 | }
173 |
174 | function testSum(label, numbers, callback) {
175 | console.time(label);
176 | for (let number of numbers) {
177 | callback(number, number);
178 | }
179 | console.timeEnd(label);
180 | }
181 | ```
182 |
183 | - `getNumbers` is a generator of all the numbers we want to test as inputs for the memoization function.
184 | - `testSum` is a function to test the execution time of a given callback function.
185 |
186 | So let's test it:
187 |
188 | ```javascript
189 | const numbers = getNumbers();
190 | ```
191 |
192 | Calling the `getNumbers`, we get an array of 1.000.000 numbers.
193 |
194 | ```javascript
195 | testSum('cold', numbers, memoizedSum);
196 | testSum('cached', numbers, memoizedSum);
197 | ```
198 |
199 | Calling the `testSum` passing the memoized function:
200 |
201 | - cold call
202 | - cached call
203 |
204 | Running in my machine (MacBook Pro (13-inch, M1, 2020), Chip Apple M1, Memory 16GB):
205 |
206 | ```bash
207 | ------- sum --------
208 | cold: 495.026ms
209 | cached: 371.011ms
210 | ------- // --------
211 | ```
212 |
213 | As I mentioned earlier, the `sum` function is a simple function therefore its execution doesn't cost that much, so we won't see a lot of performance improvements in the cached version.
214 |
215 | But now, let's see the `factorial` function being compared to its memoized version. As it's a somewhat more complex function, we'll probably see the memoized version sped up.
216 |
217 | ```javascript
218 | function factorial(number) {
219 | if (number < 0) return -1;
220 | if (number === 0) return 1;
221 | return number * factorial(number - 1);
222 | }
223 | ```
224 |
225 | Without a caching mechanism, we can implement the `factorial` function using the recursion technique.
226 |
227 | - if the number is smaller than zero: returns `-1`
228 | - if the number is zero: returns `1`
229 | - otherwise, return the number times the factorial of the `number - 1`
230 |
231 | ```javascript
232 | const memoizedFactorial = memoize(factorial);
233 | ```
234 |
235 | And this is the memoized version of the factorial. Let's call it and compare the cold and the cached versions.
236 |
237 | Similar to the `testSum` function, I created a `testFactorial` function to handle the testing.
238 |
239 | ```javascript
240 | function testFactorial(label, numbers, callback) {
241 | console.time(label);
242 | for (let number of numbers) {
243 | callback(number);
244 | }
245 | console.timeEnd(label);
246 | }
247 | ```
248 |
249 | It's very similar to the `testSum` function, the only difference is that the `callback` only receives one parameter.
250 |
251 | Running this two times:
252 |
253 | ```javascript
254 | testFactorial('cold', numbers, memoizedFactorial);
255 | testFactorial('cached', numbers, memoizedFactorial);
256 | ```
257 |
258 | We get this (running on a MacBook Pro (13-inch, M1, 2020), Chip Apple M1, Memory 16GB machine):
259 |
260 | ```bash
261 | ------ factorial ------
262 | cold: 572.349ms
263 | cached: 1.919ms
264 | --------- // ---------
265 | ```
266 |
--------------------------------------------------------------------------------
/javascript/memoization/benchmark/factorial.js:
--------------------------------------------------------------------------------
1 | import { memoize } from '../index.js';
2 |
3 | export function factorial(number) {
4 | if (number < 0) return -1;
5 | if (number === 0) return 1;
6 | return number * factorial(number - 1);
7 | }
8 |
9 | export const memoizedFactorial = memoize(factorial);
10 |
--------------------------------------------------------------------------------
/javascript/memoization/benchmark/index.js:
--------------------------------------------------------------------------------
1 | import { memoizedSum } from './sum.js';
2 | import { memoizedFactorial } from './factorial.js';
3 | import { testSum, testFactorial, getNumbers, start, end } from './test.js';
4 |
5 | let numbers = getNumbers();
6 |
7 | start('sum');
8 | testSum('cold', numbers, memoizedSum);
9 | testSum('cached', numbers, memoizedSum);
10 | end();
11 |
12 | numbers = getNumbers(10000);
13 |
14 | start('factorial');
15 | testFactorial('cold', numbers, memoizedFactorial);
16 | testFactorial('cached', numbers, memoizedFactorial);
17 | end();
18 |
--------------------------------------------------------------------------------
/javascript/memoization/benchmark/sum.js:
--------------------------------------------------------------------------------
1 | import { memoize } from '../index.js';
2 |
3 | export function sum(a, b) {
4 | return a + b;
5 | }
6 |
7 | export const memoizedSum = memoize(sum);
8 |
--------------------------------------------------------------------------------
/javascript/memoization/benchmark/test.js:
--------------------------------------------------------------------------------
1 | export function getNumbers(limit = 1000000) {
2 | let numbers = [];
3 |
4 | for (let i = 0; i < limit; i++) {
5 | numbers.push(i);
6 | }
7 |
8 | return numbers;
9 | }
10 |
11 | export function testSum(label, numbers, callback) {
12 | console.time(label);
13 | for (let number of numbers) {
14 | callback(number, number);
15 | }
16 | console.timeEnd(label);
17 | }
18 |
19 | export function testFactorial(label, numbers, callback) {
20 | console.time(label);
21 | for (let number of numbers) {
22 | callback(number);
23 | }
24 | console.timeEnd(label);
25 | }
26 |
27 | export const start = (label) => console.log(`-------- ${label} --------`);
28 | export const end = () => {
29 | console.log('-------- // --------');
30 | console.log();
31 | };
32 |
--------------------------------------------------------------------------------
/javascript/memoization/index.js:
--------------------------------------------------------------------------------
1 | export function memoize(fn) {
2 | const cache = new Map();
3 |
4 | return (...args) => {
5 | const key = JSON.stringify(args);
6 |
7 | if (cache.has(key)) {
8 | return cache.get(key);
9 | }
10 |
11 | const result = fn(...args);
12 | cache.set(key, result);
13 |
14 | return result;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/javascript/promises/README.md:
--------------------------------------------------------------------------------
1 | # Promise
2 |
3 | [Promise from scratch](from-scratch)
4 |
5 | ## Using Promises — Examples
6 |
7 | - [Example 1](examples/promise1.js)
8 | - [Example 2](examples/promise2.js)
9 | - [Example 3](examples/promise3.js)
10 |
11 | ## Use Cases
12 |
13 | - [Retry promise calls](use-cases/retries.js)
14 | - [Delay promise-based](use-cases/delay.js)
15 | - [Fetch promise-based](use-cases/fetch.js)
16 | - [Fetch Pokemons promise-based](use-cases/fetch-pokemons.js)
17 | - [Request promise-based](use-cases/request.js)
18 | - [Document ready promise-based](use-cases/document-ready.js)
19 |
--------------------------------------------------------------------------------
/javascript/promises/examples/promise1.js:
--------------------------------------------------------------------------------
1 | function job(state) {
2 | return new Promise(function (resolve, reject) {
3 | if (state) {
4 | resolve('success');
5 | } else {
6 | reject('error');
7 | }
8 | });
9 | }
10 |
11 | job(true)
12 | .then((data) => {
13 | console.log(data);
14 | return job(false);
15 | })
16 | .catch((error) => {
17 | console.log(error);
18 | return 'Error caught';
19 | })
20 | .then((data) => {
21 | console.log(data);
22 | return job(true);
23 | })
24 | .catch((error) => {
25 | console.log(error);
26 | });
27 |
28 | /**
29 | TLDR; logs `success`, `error`, `Error caught`
30 |
31 | Promise workflow:
32 | 1. Calls the job function and passes a `true` value
33 | 2. The job promise resolves returning a `success` data
34 | 3. Triggers the first `then` and logs the `success` data
35 | 4. Calls a new job promise and passes a `false` value
36 | 5. The job promise rejects returning an `error` data
37 | 6. Triggers the first `catch`, logs `error`, and return `Error caught`
38 | 7. Triggers the second `then`, logs `Error caught` and calls a new job
39 | 8. The job promise resolves returning a `success` data
40 | **/
41 |
--------------------------------------------------------------------------------
/javascript/promises/examples/promise2.js:
--------------------------------------------------------------------------------
1 | function job() {
2 | return new Promise(function (_, reject) {
3 | reject();
4 | });
5 | }
6 |
7 | job()
8 | .then(() => {
9 | console.log('Success 1');
10 | })
11 | .then(() => {
12 | console.log('Success 2');
13 | })
14 | .then(() => {
15 | console.log('Success 3');
16 | })
17 | .catch(() => {
18 | console.log('Error 1');
19 | })
20 | .then(() => {
21 | console.log('Success 4');
22 | });
23 |
24 | /**
25 | TLDR; logs `Error 1`, `Success 4`
26 |
27 | Promise workflow:
28 | 1. Calls the job function
29 | 2. The job promise rejects
30 | 3. Triggers the `catch`, logs `Error 1`
31 | 4. Triggers the last `then`, logs `Success 4`
32 | **/
33 |
--------------------------------------------------------------------------------
/javascript/promises/examples/promise3.js:
--------------------------------------------------------------------------------
1 | function job(state) {
2 | return new Promise(function (resolve, reject) {
3 | if (state) {
4 | resolve('success');
5 | } else {
6 | reject('error');
7 | }
8 | });
9 | }
10 |
11 | job(true)
12 | .then((data) => {
13 | console.log(data);
14 | return job(true);
15 | })
16 | .then((data) => {
17 | if (data !== 'victory') {
18 | throw 'Defeat';
19 | }
20 |
21 | return job(true);
22 | })
23 | .then((data) => {
24 | console.log(data);
25 | })
26 | .catch((error) => {
27 | console.log(error);
28 | return job(false);
29 | })
30 | .then((data) => {
31 | console.log(data);
32 | return job(true);
33 | })
34 | .catch((error) => {
35 | console.log(error);
36 | return 'Error caught';
37 | })
38 | .then((data) => {
39 | console.log(data);
40 | return new Error('test');
41 | })
42 | .then((data) => {
43 | console.log('Success:', data.message);
44 | })
45 | .catch((data) => {
46 | console.log('Error:', data.message);
47 | });
48 |
49 | /**
50 | TLDR; logs `success`, `Defeat`, `error`, `Error caught`, `Success: test`
51 |
52 | Promise workflow:
53 | 1. Calls the job function and passes a `true` value
54 | 2. The job promise resolves returning a `success` data
55 | 3. Triggers the first `then` and logs the `success` data
56 | 4. Calls a new job promise and passes a `true` value
57 | 5. The job promise resolves returning a `success` data
58 | 6. Triggers the second `then`, data is different than 'victory' ('success') and throws the `Defeat` error
59 | 7. Triggers the first `catch`, logs `Defeat` and calls a new job passing a `false` value
60 | 8. Triggers the second `catch`, logs `error` and return string `Error caught`
61 | 9. Triggers the fifth `then`, logs `Error caught`, and return a new Error
62 | 10. Triggers the sixth `then`, logs the `Success: test`
63 | **/
64 |
--------------------------------------------------------------------------------
/javascript/promises/from-scratch/README.md:
--------------------------------------------------------------------------------
1 | # Promise — From Scratch
2 |
3 | - [Promise from scratch](promise.js)
4 | - [Promise.all from scratch](promiseAll.js)
5 |
6 | ## API
7 |
8 | - .all()
9 | - .allSettled()
10 | - .any()
11 | - .prototype.catch()
12 | - .prototype.finally()
13 | - .race()
14 | - .reject()
15 | - .resolve()
16 | - .prototype.then()
17 |
--------------------------------------------------------------------------------
/javascript/promises/from-scratch/promise.js:
--------------------------------------------------------------------------------
1 | class Promis {
2 | Statuses = {
3 | Pending: 'pending',
4 | Fulfilled: 'fulfilled',
5 | Rejected: 'rejected',
6 | };
7 |
8 | constructor(resolver) {
9 | this.status = this.Statuses.Pending;
10 | this.data = null;
11 | this.onFulfillmentCallbacks = [];
12 | this.onRejectionCallbacks = [];
13 |
14 | try {
15 | resolver(
16 | (data) => this.resolve(data),
17 | (error) => this.reject(error)
18 | );
19 | } catch (error) {
20 | this.reject(error);
21 | }
22 | }
23 |
24 | then(onFulfillment, onRejection) {
25 | return new Promis((resolve, reject) => {
26 | if (this.status === this.Statuses.Fulfilled) {
27 | try {
28 | const onFulfillmentReturnedData = onFulfillment(this.data);
29 |
30 | if (onFulfillmentReturnedData instanceof Promis) {
31 | onFulfillmentReturnedData.then(resolve, reject);
32 | } else {
33 | resolve(onFulfillmentReturnedData);
34 | }
35 | } catch (err) {
36 | reject(err);
37 | }
38 | }
39 |
40 | if (this.status === this.Statuses.Rejected) {
41 | try {
42 | const onRejectionReturnedData = onRejection(this.data);
43 |
44 | if (onRejectionReturnedData instanceof Promis) {
45 | onRejectionReturnedData.then(resolve, reject);
46 | } else {
47 | reject(onRejectionReturnedData);
48 | }
49 | } catch (err) {
50 | reject(err);
51 | }
52 | }
53 |
54 | if (this.status === this.Statuses.Pending) {
55 | this.onFulfillmentCallbacks.push(() => {
56 | try {
57 | const onFulfillmentReturnedData = onFulfillment(this.data);
58 |
59 | if (onFulfillmentReturnedData instanceof Promis) {
60 | onFulfillmentReturnedData.then(resolve, reject);
61 | } else {
62 | resolve(onFulfillmentReturnedData);
63 | }
64 | } catch (err) {
65 | reject(err);
66 | }
67 | });
68 |
69 | this.onRejectionCallbacks.push(() => {
70 | try {
71 | const onRejectionReturnedData = onRejection(this.data);
72 |
73 | if (onRejectionReturnedData instanceof Promis) {
74 | onRejectionReturnedData.then(resolve, reject);
75 | } else {
76 | reject(onRejectionReturnedData);
77 | }
78 | } catch (err) {
79 | reject(err);
80 | }
81 | });
82 | }
83 | });
84 | }
85 |
86 | resolve(data) {
87 | if (this.status === this.Statuses.Pending) {
88 | this.status = this.Statuses.Fulfilled;
89 | this.data = data;
90 |
91 | this.onFulfillmentCallbacks.forEach((onFulfillment) =>
92 | onFulfillment(data)
93 | );
94 | }
95 | }
96 |
97 | reject(data) {
98 | if (this.status === this.Statuses.Pending) {
99 | this.status = this.Statuses.Rejected;
100 | this.data = data;
101 |
102 | this.onRejectionCallbacks.forEach((onRejection) => onRejection(data));
103 | }
104 | }
105 |
106 | static all() {}
107 | static allSettled() {}
108 | static any() {}
109 | static race() {}
110 | static reject() {}
111 | static resolve() {}
112 |
113 | catch() {}
114 | finally() {}
115 | }
116 |
117 | // ============ // ============
118 | // Testing the Promis class.
119 |
120 | const getPromis = (condition) =>
121 | new Promis((resolve, reject) => {
122 | if (condition) {
123 | resolve('success');
124 | } else {
125 | reject('error');
126 | }
127 | });
128 |
129 | // ============ // ============
130 | // Promis: resolving a promise
131 | console.log('==== Promis: resolving a promise ====');
132 |
133 | const promis1 = getPromis(true);
134 |
135 | promis1.then(console.log);
136 |
137 | console.log('==== // ====\n');
138 |
139 | // ============ // ============
140 | // Promis: rejecting a promise
141 | console.log('==== Promis: rejecting a promise ====');
142 |
143 | const promis2 = getPromis(false);
144 |
145 | promis2.then(console.log, console.log);
146 |
147 | console.log('==== // ====\n');
148 |
149 | // ============ // ============
150 | // wait promise-based function
151 | console.log('==== Promis: wait promise-based function ====');
152 |
153 | const wait = (ms) =>
154 | new Promis((resolve) => {
155 | setTimeout(() => {
156 | resolve(`done after ${ms}ms`);
157 | }, ms);
158 | });
159 |
160 | wait(1000).then((data) => {
161 | console.log(data);
162 | });
163 |
164 | // ============ // ============
165 |
166 | // Promis: resolving a promise
167 | console.log('==== Promis: chaining .then ====');
168 |
169 | const promis3 = getPromis(true);
170 |
171 | promis3
172 | .then((data) => {
173 | console.log(data);
174 | return 'another success';
175 | })
176 | .then(console.log);
177 |
178 | console.log('==== // ====\n');
179 |
180 | // ============ // ============
181 |
182 | // Promis: resolving a promise
183 | console.log('==== Promis: chaining .then for rejection ====');
184 |
185 | const promis4 = getPromis(true);
186 |
187 | promis4
188 | .then((data) => {
189 | console.log(data);
190 | throw 'error';
191 | })
192 | .then(() => {}, console.log);
193 |
194 | console.log('==== // ====\n');
195 |
196 | // ============ // ============
197 |
198 | // Promis: resolving a promise
199 | console.log('==== Promis: chaining .then for rejection ====');
200 |
201 | const promis5 = getPromis(true);
202 |
203 | promis5
204 | .then((data) => {
205 | console.log(data);
206 | return wait(1000);
207 | })
208 | .then(console.log);
209 |
210 | console.log('==== // ====\n');
211 |
--------------------------------------------------------------------------------
/javascript/promises/from-scratch/promiseAll.js:
--------------------------------------------------------------------------------
1 | function promiseAll(promises) {
2 | let result = [];
3 | let promisesCount = 0;
4 |
5 | return new Promise((resolve, reject) => {
6 | for (let promise of promises) {
7 | promise
8 | .then((data) => {
9 | result.push(data);
10 | promisesCount++;
11 |
12 | if (promisesCount === promises.length) {
13 | resolve(result);
14 | }
15 | })
16 | .catch(reject);
17 | }
18 | });
19 | }
20 |
21 | let promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
22 |
23 | promiseAll(promises).then(console.log); // [1, 2, 3]
24 |
25 | promises = [
26 | Promise.resolve(1),
27 | Promise.resolve(2),
28 | Promise.reject('Error'),
29 | Promise.resolve(3),
30 | Promise.resolve(4),
31 | ];
32 |
33 | promiseAll(promises).catch(console.log); // Error
34 |
35 | const asyncPromise = new Promise((resolve) => {
36 | setTimeout(() => resolve(4), 100);
37 | });
38 |
39 | promises = [
40 | Promise.resolve(1),
41 | Promise.resolve(2),
42 | Promise.resolve(3),
43 | asyncPromise,
44 | ];
45 |
46 | promiseAll(promises).then(console.log); // [1, 2, 3, 4]
47 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/delay.js:
--------------------------------------------------------------------------------
1 | const callback = () => {
2 | console.log(2);
3 | console.log(3);
4 | };
5 |
6 | console.log(1);
7 |
8 | const delay = (time) => {
9 | return new Promise((resolve, reject) => {
10 | if (isNaN(time)) {
11 | reject(new Error('delay requires a valid number'));
12 | }
13 |
14 | setTimeout(resolve, time);
15 | });
16 | };
17 |
18 | delay(1000)
19 | .then(callback)
20 | .catch((err) => console.error(err));
21 |
22 | delay('not a number')
23 | .then(callback)
24 | .catch((err) => console.error(err));
25 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/document-ready.js:
--------------------------------------------------------------------------------
1 | const resolveBody = () => {
2 | const body = document.querySelector('body');
3 | body.innerHTML = 'Resolved!';
4 | };
5 |
6 | const callback = (resolve) => {
7 | document.addEventListener('readystatechange', function () {
8 | if (document.readyState != 'loading') {
9 | resolve();
10 | }
11 | });
12 | };
13 |
14 | const ready = () => new Promise(callback);
15 |
16 | ready().then(resolveBody);
17 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/fetch-pokemons.js:
--------------------------------------------------------------------------------
1 | const baseUrl = 'https://pokeapi.co/api/v2/pokemon/';
2 |
3 | const pokemonsUrls = [
4 | `${baseUrl}pikachu`,
5 | `${baseUrl}mew`,
6 | `${baseUrl}charmander`,
7 | ];
8 |
9 | const getPokemon = (url) =>
10 | fetch(url)
11 | .then((response) => response.json())
12 | .then((pokemon) => ({ id: pokemon.order, name: pokemon.name }));
13 |
14 | const fetchPokemons = pokemonsUrls.map(getPokemon);
15 |
16 | Promise.all(fetchPokemons).then((pokemons) => {
17 | console.log('pokemons', pokemons);
18 | });
19 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/fetch.js:
--------------------------------------------------------------------------------
1 | const json = {
2 | id: 123,
3 | name: 'TK',
4 | email: 'leandrotk100@gmail.com',
5 | posts: [
6 | {
7 | title: 'Python From Zero to Hero',
8 | contents: '...',
9 | },
10 | {
11 | title: 'FP Concepts',
12 | contents: '...',
13 | },
14 | ],
15 | };
16 |
17 | const fetchUser = () =>
18 | new Promise((resolve, _) => {
19 | resolve(json);
20 | });
21 |
22 | const get = (property) => (json) => {
23 | const value = json[property];
24 | console.log(value);
25 | };
26 |
27 | fetchUser().then(get('id')); // console.log(`123`)
28 |
29 | fetchUser().then(get('name')); // console.log(`TK`)
30 |
31 | fetchUser().then(get('posts')); // console.log(`[...]`)
32 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/request.js:
--------------------------------------------------------------------------------
1 | function request(url) {
2 | const request = new XMLHttpRequest();
3 |
4 | return new Promise((resolve, reject) => {
5 | request.onreadystatechange = () => {
6 | if (request.readyState !== 4) return;
7 |
8 | if (request.status >= 200 && request.status < 300) {
9 | resolve(request.response);
10 | } else {
11 | reject({
12 | status: request.status,
13 | statusText: request.statusText,
14 | });
15 | }
16 | };
17 |
18 | request.onerror = () => {
19 | reject();
20 | };
21 |
22 | request.open('GET', url, true);
23 | request.send();
24 | });
25 | }
26 |
27 | request('https://pokeapi.co/api/v2/pokemon').then(JSON.parse).then(console.log);
28 |
--------------------------------------------------------------------------------
/javascript/promises/use-cases/retries.js:
--------------------------------------------------------------------------------
1 | /* Implement a retryAsync function that will call an asynchronous function and then
2 | * recall it again on failure until a max number of retries has been reached.
3 | * The function receives the asynchronous function and the maximum number of retries
4 | * The function should return the result of the async function and throw an error
5 | * after the maximum number of retries
6 | */
7 |
8 | const retryAsync = async (asyncFunc, maxRetries = 5) => {
9 | try {
10 | return await asyncFunc();
11 | } catch (error) {
12 | if (maxRetries > 0) {
13 | return await retryAsync(asyncFunc, maxRetries - 1);
14 | } else {
15 | throw new Error('error');
16 | }
17 | }
18 | };
19 |
20 | const main = async () => {
21 | const myPromise = () =>
22 | new Promise((resolve, reject) => {
23 | setTimeout(() => {
24 | reject('Promise Failed');
25 | }, 3000);
26 | });
27 |
28 | const res = await retryAsync(myPromise, 3);
29 | return res;
30 | };
31 |
32 | main();
33 |
--------------------------------------------------------------------------------
/javascript/throttle/README.md:
--------------------------------------------------------------------------------
1 | # Throttle
2 |
3 | [Example](index.js)
4 |
--------------------------------------------------------------------------------
/javascript/throttle/index.js:
--------------------------------------------------------------------------------
1 | function throttle(fn, time = 3000) {
2 | let withinFunctionCall = false;
3 |
4 | return (...args) => {
5 | if (withinFunctionCall) {
6 | return;
7 | }
8 |
9 | withinFunctionCall = true;
10 |
11 | setTimeout(() => {
12 | fn(...args);
13 | withinFunctionCall = false;
14 | }, time);
15 | };
16 | }
17 |
18 | function log(text) {
19 | console.log(text);
20 | }
21 |
22 | const throttledLog = throttle(log);
23 |
24 | throttledLog('test');
25 | throttledLog('test');
26 |
--------------------------------------------------------------------------------
/javascript/update-timer/index.js:
--------------------------------------------------------------------------------
1 | function updateTimer(isoDate, timerInfo = {}) {
2 | let date = new Date(isoDate);
3 | let seconds = date.getSeconds();
4 | let minutes = date.getMinutes();
5 | let hours = date.getHours();
6 |
7 | setInterval(() => {
8 | timerInfo.seconds = seconds;
9 | timerInfo.minutes = minutes;
10 | timerInfo.hours = hours;
11 |
12 | seconds++;
13 |
14 | if (seconds >= 60) {
15 | seconds = seconds % 60;
16 | minutes++;
17 |
18 | if (minutes >= 60) {
19 | minutes = minutes % 60;
20 | hours++;
21 |
22 | if (hours >= 24) {
23 | hours = 0;
24 | }
25 | }
26 | }
27 | }, 1000);
28 | }
29 |
30 | let timerInfo = {
31 | seconds: 0,
32 | minutes: 0,
33 | hours: 0,
34 | };
35 |
36 | updateTimer('2022-07-01T12:59:51-03:00', timerInfo);
37 |
38 | console.log(timerInfo);
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crafting-frontend",
3 | "version": "1.0.0",
4 | "scripts": {},
5 | "type": "module",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/imteekay/crafting-frontend.git"
9 | },
10 | "author": "TK",
11 | "license": "MIT"
12 | }
13 |
--------------------------------------------------------------------------------
/react/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/react/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/react/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.15.1
2 |
--------------------------------------------------------------------------------
/react/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/react/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # React
4 |
5 | Research, studies, and practice on ReactJS.
6 |
7 | ## Basic concepts
8 |
9 | - State / Props
10 | - [Update parent's state](/react/pages/parent-state/index.js)
11 | - [Fetch API](/react/pages/fetch/index.js)
12 | - [Handle Form submit](/react/pages/form)
13 | - [Context API](/react/pages/context/index.js)
14 | - [Event handlers](/react/pages/form)
15 |
16 | ## UI / Component
17 |
18 | - Form
19 | - Modal
20 | - Tabs
21 | - Input
22 | - [Tag Input](/react/pages/tag-input)
23 | - [Progress Bar](/react/pages/progress-bar)
24 |
25 | ## Apps
26 |
27 | - Sortable Data Table
28 | - [TODO list](/react/pages/todo/index.js)
29 | - Kanban Board
30 | - Search filters
31 | - [Nested Comments](/react/pages/nested-comments/index.js): [doc](/react/docs/nested-comments.md)
32 |
33 |
34 |
--------------------------------------------------------------------------------
/react/docs/nested-comments.md:
--------------------------------------------------------------------------------
1 | # Nested Comments
2 |
3 | ## Comments Engine / Comment thread
4 |
5 | - display a list of comments
6 | - add a new comment
7 | - reply to any existing comment (should support n-level Nested Replies to the comments)
8 |
9 | ## Design & Implementation
10 |
11 | - Map to render a list of comments
12 | - Comment component: UI of the comment
13 | - Replies component: list of replies for a given component
14 | - Input and button to handle new comment
15 | - Input and button to handle new reply
16 | - Context API + `useState` to handle state
17 | - Replies render Replies: recursion to render nested replies
18 | - Recursion to add new replies
19 |
--------------------------------------------------------------------------------
/react/hooks/useFetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | Write a useFetch custom hook that takes in a required url as a string or URL object. This parameter should be directly passed to the native JavaScript fetch function.
3 |
4 | Calling useFetch in a component should make a fetch request when an instance of the component is mounted. Additionally, a new request should be issued on any render where the url has changed.
5 |
6 | The useFetch function should return an object with three keys:
7 |
8 | responseJSON: The JSON response from the most recent call to fetch. If no response has been received yet or the most recent request resulted in an error, this should be null.
9 | isLoading: When a fetch request is issued, this should be set to true, and set to false when the response comes back or an error is thrown.
10 | error: If the most recent call to fetch threw an error or retrieving the JSON from the most recent response threw an error, the error should be saved in this value, otherwise it should be null.
11 | In the event that the url changes before the previous fetch request returns, the response from that previous request should not be used in order to prevent a race condition.
12 |
13 | ```js
14 | function Fetcher() {
15 | const { responseJSON, isLoading, error } = useFetch(url);
16 | const [url, setUrl] = useState('');
17 | return (
18 | <>
19 | setUrl(e.target.value)} />
20 | {
21 | error ?
Error: {error}
:
22 | isLoading ? Loading...
:
23 | Response: {responseJSON}
24 | }
25 | >
26 | );
27 | }
28 | ```
29 | */
30 |
31 | function useFetch(url) {
32 | const [responseJSON, setResponseJSON] = useState(null);
33 | const [isLoading, setIsLoading] = useState(false);
34 | const [error, setError] = useState(null);
35 |
36 | useEffect(() => {
37 | const controller = new AbortController();
38 | const { signal } = controller;
39 |
40 | const request = async () => {
41 | try {
42 | setIsLoading(true);
43 | const response = await fetch(url, { signal });
44 | setResponseJSON(await response.json());
45 | setError(null);
46 | } catch (err) {
47 | setError(err);
48 | setResponseJSON(null);
49 | } finally {
50 | setIsLoading(false);
51 | }
52 | };
53 |
54 | request();
55 |
56 | return () => {
57 | controller.abort();
58 | };
59 | }, [url]);
60 |
61 | return {
62 | responseJSON,
63 | error,
64 | isLoading,
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/react/hooks/useInterval.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | function useInterval(callback, delay) {
4 | const callbackRef = useRef();
5 |
6 | useEffect(() => {
7 | callbackRef.current = callback;
8 | }, [callback]);
9 |
10 | useEffect(() => {
11 | const interval = setInterval(() => {
12 | callbackRef.current();
13 | }, delay);
14 |
15 | if ([null, undefined].includes(delay)) {
16 | clearInterval(interval);
17 | }
18 |
19 | return () => {
20 | clearInterval(interval);
21 | };
22 | }, [delay]);
23 | }
24 |
--------------------------------------------------------------------------------
/react/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Write a useLocalStorage custom hook that takes in a required key as a string, and an optional initialValue.
3 |
4 | * Calling useLocalStorage in a component should save the initialValue in localStorage at the given key when the component first mounts. If a value already exists at that key, the initialValue parameter should be ignored.
5 | * The useLocalStorage function should return an array with the current value as the first element and a setter function as the second element. The setter function should take in a new value as a parameter and update localStorage at the original key.
6 | * When the setter function is called, the component should re-render, just as it would when a standard piece of state is updated.
7 | * Any value added to localStorage should first be passed to JSON.stringify. When reading the value from localStorage, JSON.parse should be used to parse the original value.
8 | * For simplicity, you can asssume the key parameter will not change between renders.
9 |
10 | * Sample Usage
11 |
12 | ```js
13 | function SaveValues() {
14 | const [value, setValue] = useLocalStorage('name', '');
15 | return setValue(e.target.value)} />;
16 | }
17 | ```
18 | */
19 |
20 | function useLocalStorage(key, initialValue) {
21 | const [value, setValue] = useState();
22 | const localStorageValue = JSON.parse(localStorage.getItem(key));
23 |
24 | useEffect(() => {
25 | if ([null, undefined].includes(localStorageValue)) {
26 | localStorage.setItem(key, JSON.stringify(initialValue));
27 | setValue(initialValue);
28 | } else {
29 | setValue(localStorageValue);
30 | }
31 | }, []);
32 |
33 | const handleSetValue = (newValue) => {
34 | setValue(newValue);
35 | localStorage.setItem(key, JSON.stringify(newValue));
36 | };
37 |
38 | return [value, handleSetValue];
39 | }
40 |
--------------------------------------------------------------------------------
/react/hooks/useMap.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | function useMap(initialValue) {
4 | const [map, setMap] = useState(new Map(initialValue));
5 |
6 | const set = useCallback((key, value) => {
7 | setMap((prevMap) => {
8 | const updatedMap = new Map(prevMap);
9 | updatedMap.set(key, value);
10 | return updatedMap;
11 | });
12 | }, []);
13 |
14 | const del = useCallback((key) => {
15 | setMap((prevMap) => {
16 | const updatedMap = new Map(prevMap);
17 | updatedMap.delete(key);
18 | return updatedMap;
19 | });
20 | }, []);
21 |
22 | const clear = useCallback(() => {
23 | setMap((prevMap) => {
24 | const updatedMap = new Map(prevMap);
25 | updatedMap.clear();
26 | return updatedMap;
27 | });
28 | }, []);
29 |
30 | return {
31 | map,
32 | set,
33 | delete: del,
34 | clear,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/react/hooks/useStateWithHistory.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | function useStateWithHistory(initialState) {
4 | const [history, setHistory] = useState(
5 | [null, undefined].includes(initialState) ? [] : [initialState]
6 | );
7 | const [value, setValue] = useState(initialState);
8 | const [index, setIndex] = useState(0);
9 |
10 | const updateHistory = (newValue) => {
11 | setHistory([...history, newValue]);
12 | setValue(newValue);
13 | setIndex(history.length);
14 | };
15 |
16 | const goBack = () => {
17 | if (index > 0) {
18 | setValue(history[index - 1]);
19 | setIndex(index - 1);
20 | }
21 | };
22 |
23 | const goForward = () => {
24 | if (index < history.length - 1) {
25 | setValue(history[index + 1]);
26 | setIndex(index + 1);
27 | }
28 | };
29 |
30 | return [value, updateHistory, goBack, goForward, history];
31 | }
32 |
--------------------------------------------------------------------------------
/react/hooks/useWindowSize.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | function useWindowSize() {
4 | const [height, setHeight] = useState(window.innerHeight);
5 | const [width, setWidth] = useState(window.innerWidth);
6 |
7 | useEffect(() => {
8 | const handleResize = () => {
9 | setHeight(window.innerHeight);
10 | setWidth(window.innerWidth);
11 | };
12 |
13 | window.addEventListener('resize', handleResize);
14 |
15 | return () => {
16 | window.removeEventListener('resize', handleResize);
17 | };
18 | }, []);
19 |
20 | return { width, height };
21 | }
22 |
--------------------------------------------------------------------------------
/react/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "11.0.1",
13 | "react": "17.0.2",
14 | "react-dom": "17.0.2"
15 | },
16 | "devDependencies": {
17 | "eslint": "7.32.0",
18 | "eslint-config-next": "11.0.1",
19 | "prettier": "^2.3.2"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/react/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 |
3 | function MyApp({ Component, pageProps }) {
4 | return ;
5 | }
6 |
7 | export default MyApp;
8 |
--------------------------------------------------------------------------------
/react/pages/context/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useContext, createContext } from 'react';
2 |
3 | const CountContext = createContext();
4 |
5 | const Counter = () => {
6 | const [count, increment] = useState(0);
7 |
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | const Count = () => {
17 | const { count } = useContext(CountContext);
18 | return Count: {count}
;
19 | };
20 |
21 | const IncrementButton = () => {
22 | const { count, increment } = useContext(CountContext);
23 | return increment(count + 1)}>Increment ;
24 | };
25 |
26 | export default Counter;
27 |
--------------------------------------------------------------------------------
/react/pages/fetch/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const Home = () => {
4 | const [pokemon, setPokemon] = useState({ abilities: [] });
5 | const [isLoading, setIsLoading] = useState(false);
6 | const [hasError, setHasError] = useState(false);
7 |
8 | useEffect(() => {
9 | const fetchPokemon = async () => {
10 | try {
11 | setHasError(false);
12 | setIsLoading(true);
13 | const response = await fetch('https://pokeapi.co/api/v2/pokemon/ditto');
14 | const data = await response.json();
15 | setPokemon(data);
16 | } catch (error) {
17 | setHasError(true);
18 | }
19 |
20 | setIsLoading(false);
21 | };
22 |
23 | fetchPokemon();
24 | }, []);
25 |
26 | if (isLoading) return loading...
;
27 | if (hasError) return error
;
28 |
29 | return (
30 | <>
31 | Welcome to the `fetch`!
32 | name: {pokemon.name}
33 | id: {pokemon.id}
34 | ability:
35 |
36 | {pokemon.abilities.map(({ ability }, id) => (
37 | {ability.name}
38 | ))}
39 |
40 | >
41 | );
42 | };
43 |
44 | export default Home;
45 |
--------------------------------------------------------------------------------
/react/pages/form/README.md:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 | ## Form elements
4 |
5 | - Input
6 | - Form
7 | - Label
8 | - Select
9 | - Button
10 |
11 | ## Event Handlers
12 |
13 | - onSubmit
14 | - onClick
15 | - onChange
16 | - onKeyDown
17 |
--------------------------------------------------------------------------------
/react/pages/form/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const countriesToVisit = [
4 | {
5 | name: 'Japan',
6 | value: 'japan',
7 | },
8 | {
9 | name: 'Canada',
10 | value: 'canada',
11 | },
12 | {
13 | name: 'Korea',
14 | value: 'korea',
15 | },
16 | {
17 | name: 'Netherlands',
18 | value: 'netherlands',
19 | },
20 | ];
21 |
22 | const Form = () => {
23 | const [name, setName] = useState('');
24 | const [phone, setPhone] = useState('');
25 | const [country, setCountry] = useState('');
26 |
27 | const onSubmitForm = (event) => {
28 | event.preventDefault();
29 | console.log('submit all data', name, phone, country);
30 | };
31 |
32 | const handleName = (e) => setName(e.target.value);
33 | const handlePhone = (e) => setPhone(e.target.value);
34 | const handleCountry = (e) => setCountry(e.target.value);
35 |
36 | return (
37 | <>
38 |
62 |
63 | name: {name}
64 | phone: {phone}
65 | country: {country}
66 | >
67 | );
68 | };
69 |
70 | export const Home = () => (
71 | <>
72 | Welcome to the form playground!
73 |
74 | >
75 | );
76 |
77 | export default Home;
78 |
--------------------------------------------------------------------------------
/react/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import Image from 'next/image';
3 |
4 | export default function Home() {
5 | return (
6 |
7 | React
8 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/react/pages/nested-comments/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | Comments Engine / Comment thread,
3 |
4 | Develop a Comments Engine with the following features
5 | - display a list of comments ✅
6 | - add a new comment ✅
7 | - reply to any existing comment (should support n-level Nested Replies to the comments) ✅
8 | - delete a comment (All children's comments are deleted if a parent is deleted) ✅
9 | */
10 |
11 | import { useState, createContext, useContext } from 'react';
12 |
13 | const initialComments = [
14 | {
15 | text: 'comment',
16 | author: 'TK',
17 | edited: false,
18 | replies: [
19 | {
20 | text: 'reply',
21 | author: 'TK',
22 | edited: false,
23 | replies: [
24 | {
25 | text: 'nested reply',
26 | author: 'TK',
27 | edited: false,
28 | replies: [],
29 | },
30 | ],
31 | },
32 | ],
33 | },
34 | ];
35 |
36 | const CommentsContext = createContext();
37 |
38 | const CommentsProvider = ({ children, initialComments }) => {
39 | const [comments, setComments] = useState(initialComments);
40 | const [comment, setComment] = useState();
41 |
42 | const addNewReply = (comments, ids) => {
43 | if (ids.length === 0) {
44 | return [...comments, comment];
45 | }
46 |
47 | const id = ids.shift();
48 | comments[id].replies = addNewReply(comments[id].replies, ids);
49 | return [...comments];
50 | };
51 |
52 | const removeReply = (comments, ids, index) => {
53 | if (ids.length === 0) {
54 | return comments.filter((_, id) => id !== index);
55 | }
56 |
57 | const id = ids.shift();
58 | comments[id].replies = removeReply(comments[id].replies, ids, index);
59 | return [...comments];
60 | };
61 |
62 | const handeCommentChange = (event) => {
63 | setComment({
64 | text: event.target.value,
65 | author: 'TK',
66 | edited: false,
67 | replies: [],
68 | });
69 | };
70 |
71 | const handleCommentAddition =
72 | (ids = []) =>
73 | () => {
74 | setComments(addNewReply(comments, ids));
75 | };
76 |
77 | const handleCommentDeletion = (ids, index) => () => {
78 | setComments(removeReply(comments, ids, index));
79 | };
80 |
81 | const providerValue = {
82 | comments,
83 | handeCommentChange,
84 | handleCommentAddition,
85 | handleCommentDeletion,
86 | };
87 |
88 | return (
89 |
90 | {children}
91 |
92 | );
93 | };
94 |
95 | const CommentWrapper = ({ children }) => (
96 |
97 | {children}
98 |
99 | );
100 |
101 | const CommentTextWrapper = ({ children }) => (
102 | {children}
103 | );
104 |
105 | const CommentText = ({ children }) => (
106 | {children}
107 | );
108 |
109 | const DeleteCommentButton = ({ ids, index }) => {
110 | const { handleCommentDeletion } = useContext(CommentsContext);
111 | return X ;
112 | };
113 |
114 | const AddComment = ({ ids }) => {
115 | const [isReplyInputOpen, setIsReplyInputOpen] = useState(false);
116 | const { handeCommentChange, handleCommentAddition } =
117 | useContext(CommentsContext);
118 |
119 | const handleReplyInputOpen = () => setIsReplyInputOpen(!isReplyInputOpen);
120 |
121 | return (
122 | <>
123 |
124 | Reply
125 |
126 | {isReplyInputOpen ? (
127 | <>
128 |
129 | add comment
130 | >
131 | ) : null}
132 | >
133 | );
134 | };
135 |
136 | const Edited = ({ edited }) =>
137 | edited ? ✅
: null;
138 |
139 | const Comment = ({ text, author, edited, replies, index, ids }) => (
140 |
141 |
142 |
143 | {author}: {text}
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | );
152 |
153 | const Comments = ({ comments, ids = [] }) =>
154 | comments.map((comment, index) => (
155 |
164 | ));
165 |
166 | const Wrapper = () => {
167 | const { comments } = useContext(CommentsContext);
168 | return ;
169 | };
170 |
171 | const Page = () => (
172 |
173 |
174 |
175 | );
176 |
177 | export default Page;
178 |
--------------------------------------------------------------------------------
/react/pages/parent-state/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const Child = ({ updateState }) => (
4 | updateState((state) => state + 1)}>+
5 | );
6 |
7 | const Parent = () => {
8 | const [state, setState] = useState(0);
9 |
10 | return (
11 |
12 | {state}
13 |
14 |
15 | );
16 | };
17 |
18 | export default Parent;
19 |
--------------------------------------------------------------------------------
/react/pages/phone-input/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | export default function PhoneInput() {
4 | const [phoneNumber, setPhoneNumber] = useState('');
5 | const isPhoneNumberComplete = phoneNumber.length === 14;
6 |
7 | const formatAddingNumber = (key) => {
8 | if (phoneNumber.length === 0) {
9 | return `(${key}`;
10 | }
11 |
12 | if (phoneNumber.length === 4) {
13 | return `${phoneNumber}) ${key}`;
14 | }
15 |
16 | if (phoneNumber.length === 9) {
17 | return `${phoneNumber}-${key}`;
18 | }
19 |
20 | if (isPhoneNumberComplete) {
21 | return phoneNumber;
22 | }
23 |
24 | return phoneNumber + key;
25 | };
26 |
27 | const formatRemovingNumber = () => {
28 | if (phoneNumber.length === 2) {
29 | return '';
30 | }
31 |
32 | if (phoneNumber.length === 7) {
33 | return phoneNumber.slice(0, phoneNumber.length - 3);
34 | }
35 |
36 | if (phoneNumber.length === 11) {
37 | return phoneNumber.slice(0, phoneNumber.length - 2);
38 | }
39 |
40 | return phoneNumber.slice(0, phoneNumber.length - 1);
41 | };
42 |
43 | const handleOnKeyDown = (e) => {
44 | const isNumber = /^[0-9]$/i.test(e.key);
45 |
46 | if (isNumber) {
47 | setPhoneNumber(formatAddingNumber(e.key));
48 | }
49 |
50 | if (['Backspace', 'Delete'].includes(e.key)) {
51 | setPhoneNumber(formatRemovingNumber());
52 | }
53 | };
54 |
55 | const handleOnClick = (e) => {
56 | setPhoneNumber('');
57 | };
58 |
59 | return (
60 | <>
61 |
67 |
68 | Submit
69 |
70 | >
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/react/pages/progress-bar/README.md:
--------------------------------------------------------------------------------
1 | # Progress Bar
2 |
3 | - show the bar with the progress loading
4 | - show the percentage
5 |
6 | ## ProgressBar
7 |
8 | - API: `loading` - the percentage
9 | - The wrapper for the progress bar: border, height
10 | - Loading bar with background color and dynamic width
11 |
--------------------------------------------------------------------------------
/react/pages/progress-bar/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const Wrapper = ({ children }) => (
4 |
5 | {children}
6 |
7 | );
8 |
9 | const LoadingBar = ({ width }) => (
10 |
11 | );
12 |
13 | const ProgressBar = ({ loading }) => (
14 |
15 |
16 |
17 | );
18 |
19 | const Page = () => {
20 | const [loading, setLoading] = useState(0);
21 |
22 | useEffect(() => {
23 | setTimeout(() => {
24 | if (loading < 100) {
25 | setLoading(loading + 1);
26 | }
27 | }, 20);
28 | }, [loading]);
29 |
30 | return (
31 |
32 |
Progress Bar: {loading}%
33 |
34 |
35 | );
36 | };
37 |
38 | export default Page;
39 |
--------------------------------------------------------------------------------
/react/pages/question-list/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | Question List
3 |
4 | You're given a CSS file for the FrontendExpert question list, and you need to implement the component using React.
5 |
6 | When the component initially mounts, it should make two API requests:
7 |
8 | https://api.frontendexpert.io/api/fe/questions returns a list of all of the questions as JSON in this format:
9 | [
10 | {
11 | "category": "HTML",
12 | "id": "sign-up-form",
13 | "name": "Sign-Up Form"
14 | },
15 | ...
16 | ]
17 | https://api.frontendexpert.io/api/fe/submissions returns a list of the user's most recent submissions as JSON in this format:
18 | [
19 | {
20 | "questionId": "blog-post",
21 | "status": "CORRECT"
22 | },
23 | ...
24 | ]
25 | The component should render a fragment containing all of the categories. Each category is a div with a heading and one or more question divs. Each category div should have a category CSS class, and each question should have a question CSS class.
26 |
27 | The category heading is an h2 with the text of the name of the category and how many correct submissions there are for questions in that category (correct questions have the "CORRECT" status). For example, if 1 out of 5 CSS questions have a "CORRECT" status, the category heading would read CSS 1 / 5.
28 |
29 | The question divs should first contain another div for the status. This status div should have the CSS class of status and a CSS class based on the current status. If the question exists in the submissions API output, that status should be converted to lowercase, any _'s should be replaced with a -, and the resulting string should be used as a CSS class. For example, if a submission status is PARTIALLY_CORRECT, the complete CSS class of the status div would be status partially-correct. If there is no status in the submissions response, the status class should be status unattempted.
30 |
31 | After the status div, each question should also contain an h3 with the title of the question.
32 |
33 | The complete output of a category might look something like this:
34 |
35 |
36 |
CSS 1 / 5
37 |
38 |
39 |
Rainbow Circles
40 |
41 |
45 |
46 |
47 | Your component has already been rendered to the DOM inside of a #root div directly in the body with the CSS imported.
48 | */
49 |
50 | import React, { useState, useEffect } from 'react';
51 |
52 | const QUESTIONS_API_BASE_URL = 'https://api.frontendexpert.io/api/fe/questions';
53 | const SUBMISSIONS_API_BASE_URL =
54 | 'https://api.frontendexpert.io/api/fe/submissions';
55 |
56 | const getStatusCSSClass = (status) => {
57 | switch (status) {
58 | case 'CORRECT':
59 | return 'correct';
60 | case 'PARTIALLY_CORRECT':
61 | return 'partially-correct';
62 | case 'INCORRECT':
63 | return 'incorrect';
64 | default:
65 | return 'unattempted';
66 | }
67 | };
68 |
69 | const buildPosts = (questions, submissions) => {
70 | const posts = {};
71 | const submissionsStatus = {};
72 |
73 | for (const submission of submissions) {
74 | submissionsStatus[submission['questionId']] = submission['status'];
75 | }
76 |
77 | for (const question of questions) {
78 | if (posts[question['category']]) {
79 | posts[question['category']].list.push({
80 | id: question['id'],
81 | label: question['name'],
82 | status: getStatusCSSClass(submissionsStatus[question['id']]),
83 | });
84 | } else {
85 | posts[question['category']] = {
86 | list: [
87 | {
88 | id: question['id'],
89 | label: question['name'],
90 | status: getStatusCSSClass(submissionsStatus[question['id']]),
91 | },
92 | ],
93 | };
94 | }
95 | }
96 |
97 | for (const [categoryName, category] of Object.entries(posts)) {
98 | const total = category.list.length;
99 | const correct = category.list.filter(
100 | (post) => post.status === 'correct'
101 | ).length;
102 | posts[categoryName].progress = `${correct} / ${total}`;
103 | }
104 |
105 | return posts;
106 | };
107 |
108 | export default function QuestionList() {
109 | const [posts, setPosts] = useState({});
110 | const [isLoading, setIsLoading] = useState(false);
111 |
112 | useEffect(() => {
113 | const requestQuestions = async () => {
114 | const response = await fetch(QUESTIONS_API_BASE_URL);
115 | return await response.json();
116 | };
117 |
118 | const requestSubmissions = async () => {
119 | const response = await fetch(SUBMISSIONS_API_BASE_URL);
120 | return await response.json();
121 | };
122 |
123 | const request = async () => {
124 | setIsLoading(true);
125 |
126 | try {
127 | const [questionsPromise, submissionsPromise] = await Promise.all([
128 | requestQuestions,
129 | requestSubmissions,
130 | ]);
131 | const posts = buildPosts(
132 | await questionsPromise(),
133 | await submissionsPromise()
134 | );
135 | setPosts(posts);
136 | } catch (error) {
137 | console.error(error);
138 | } finally {
139 | setIsLoading(false);
140 | }
141 | };
142 |
143 | request();
144 | }, []);
145 |
146 | if (isLoading) return null;
147 |
148 | return (
149 | <>
150 | {Object.entries(posts).map(([category, categoryList]) => (
151 |
152 |
153 | {category} - {categoryList.progress}
154 |
155 | {categoryList.list.map((post) => (
156 |
157 |
158 |
{post.label}
159 |
160 | ))}
161 |
162 | ))}
163 | >
164 | );
165 | }
166 |
--------------------------------------------------------------------------------
/react/pages/tag-input/README.md:
--------------------------------------------------------------------------------
1 | # Tag Input
2 |
3 | Design, implement and style a Tag input component.
4 |
5 | The user types in an input field and on “Enter” a tag of whatever they typed is created.
6 |
7 | ## Requirements:
8 |
9 | - the tags must be removable
10 | - the input cannot accept duplicate values (case insensitive)
11 | - The CSS must match the design they gave
12 |
13 | ## Bonus:
14 |
15 | - Add an Autocomplete feature with the provided JSON list.
16 |
17 | ## Implementation
18 |
19 | ### UI
20 |
21 | 
22 |
23 | - `TagInput`: the main component
24 | - `Input`: the input tag
25 | - `Tags`: the component to hold the list of tags
26 | - `Tag`: the tag component
27 | - `Button`: the close button
28 | - `Wrapper`: the tag input wrapper component
29 |
30 | ## Behavior
31 |
32 | - Close button: remove the tag from the list of tags
33 | - Enter: add the tag to the list of tags
34 | - Tags items: the state of tags
35 |
36 | ## Data model
37 |
38 | ```
39 | {
40 | 'test1': 'Test1',
41 | 'test2': 'Test2',
42 | 'test3': 'Test3',
43 | }
44 | ```
45 |
46 | With a hashmap:
47 |
48 | - `O(1)` to add a new tag
49 | - `O(1)` to remove a tag
50 |
51 | ## Event handler
52 |
53 | - `onClick`: for close button
54 | - `onChange`: for input state
55 | - `onKeyDown`: for the enter keyword
56 |
--------------------------------------------------------------------------------
/react/pages/tag-input/index.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from 'react';
2 |
3 | const TagInputContext = createContext();
4 |
5 | const isObjEmpty = (object) => Object.keys(object).length === 0;
6 |
7 | const Button = ({ tagId }) => {
8 | const { removeTag } = useContext(TagInputContext);
9 |
10 | return (
11 | removeTag(tagId)} style={{ marginLeft: '4px' }}>
12 | X
13 |
14 | );
15 | };
16 |
17 | const Label = ({ children }) => {children} ;
18 |
19 | const Tag = ({ tagId, tag }) => (
20 |
23 | {tag}
24 |
25 |
26 | );
27 |
28 | const Tags = () => {
29 | const { allTags } = useContext(TagInputContext);
30 |
31 | return (
32 |
33 | {allTags.map(([tagId, tag]) => (
34 |
35 | ))}
36 |
37 | );
38 | };
39 |
40 | const Input = () => {
41 | const [newTag, setNewTag] = useState('');
42 | const { addTag } = useContext(TagInputContext);
43 |
44 | const updateTag = (event) => {
45 | setNewTag(event.target.value);
46 | };
47 |
48 | const handleEnter = (event) => {
49 | if (event.key === 'Enter') {
50 | addTag(newTag);
51 | setNewTag('');
52 | }
53 | };
54 |
55 | return ;
56 | };
57 |
58 | const Wrapper = ({ children, defaultTags }) => {
59 | const [tags, setTags] = useState(defaultTags);
60 | const isEmpty = isObjEmpty(tags);
61 | const hasGap = !isEmpty;
62 |
63 | const removeTag = (tag) => {
64 | const { [tag]: _, ...updatedTags } = tags;
65 | setTags(updatedTags);
66 | };
67 |
68 | const allTags = Object.entries(tags);
69 | const hasTag = (tag) => (tags[tag] ? true : false);
70 |
71 | const addTag = (newTag) =>
72 | !hasTag(newTag) &&
73 | setTags({
74 | ...tags,
75 | [newTag]: newTag,
76 | });
77 |
78 | const value = {
79 | allTags,
80 | addTag,
81 | removeTag,
82 | };
83 |
84 | return (
85 |
86 |
95 | {children}
96 |
97 |
98 | );
99 | };
100 |
101 | const TagInput = ({ defaultTags }) => (
102 |
103 |
104 |
105 |
106 | );
107 |
108 | const defaultTags = { tag1: 'tag1', tag2: 'tag2' };
109 |
110 | const Page = () => (
111 |
112 |
113 |
114 | );
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/react/pages/tip-calculator/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | export default function TipCalculator() {
4 | const [bill, setBill] = useState(50);
5 | const [tipPercentage, setTipPercentage] = useState(18);
6 | const [numberOfPeople, setNumberOfPeople] = useState(1);
7 |
8 | const presentPrice = (value) =>
9 | bill !== '' && tipPercentage !== '' ? `$${value.toFixed(2)}` : '-';
10 |
11 | const calculateTotalTip = () => (bill * tipPercentage) / 100;
12 |
13 | const calculateTipPerPerson = () => totalTip / numberOfPeople;
14 |
15 | const totalTip = calculateTotalTip();
16 | const tipPerPerson = calculateTipPerPerson();
17 |
18 | return (
19 | <>
20 | Bill
21 | setBill(e.target.value)}
26 | />
27 | Tip Percentage
28 | setTipPercentage(e.target.value)}
33 | />
34 | Number of People
35 | setNumberOfPeople(e.target.value)}
40 | />
41 |
42 | Total Tip: {presentPrice(totalTip)}
43 | Tip Per Person: {presentPrice(tipPerPerson)}
44 | >
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/react/pages/todo/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useHash = () => {
4 | const [hash, setHash] = useState(0);
5 |
6 | const generateHash = () => {
7 | setHash(hash + 1);
8 | return hash;
9 | };
10 |
11 | return { generateHash };
12 | };
13 |
14 | const Categories = {
15 | All: 'ALL',
16 | Active: 'ACTIVE',
17 | Completed: 'COMPLETED',
18 | };
19 |
20 | const filterByCategory = (todos, category) => {
21 | switch (category) {
22 | case Categories.All:
23 | return todos;
24 | case Categories.Active:
25 | return todos.filter((todo) => !todo.checked);
26 | case Categories.Completed:
27 | return todos.filter((todo) => todo.checked);
28 | default:
29 | return todos;
30 | }
31 | };
32 |
33 | const useTodo = () => {
34 | const [todos, setTodos] = useState([]);
35 | const [todoTitle, setTodoTitle] = useState('');
36 | const [category, setCategory] = useState(Categories.All);
37 | const { generateHash } = useHash();
38 |
39 | const handleTodoChange = (event) => setTodoTitle(event.target.value);
40 |
41 | const handleAddTodo = () => {
42 | setTodos([
43 | ...todos,
44 | { id: generateHash(), title: todoTitle, checked: false },
45 | ]);
46 | };
47 |
48 | const handleDeleteTodo = (id) => () => {
49 | setTodos(todos.filter((todo) => todo.id !== id));
50 | };
51 |
52 | const handleTodoCheck = (id) => () => {
53 | setTodos(
54 | todos.map((todo) =>
55 | todo.id === id ? { ...todo, checked: !todo.checked } : todo
56 | )
57 | );
58 | };
59 |
60 | const handleCategory = (category) => () => {
61 | setCategory(category);
62 | };
63 |
64 | return {
65 | todos,
66 | todoTitle,
67 | category,
68 | handleTodoChange,
69 | handleAddTodo,
70 | handleDeleteTodo,
71 | handleTodoCheck,
72 | handleCategory,
73 | };
74 | };
75 |
76 | const useSearch = () => {
77 | const [searchTerm, setSearchTerm] = useState('');
78 | const handleSearchInput = (event) => {
79 | setSearchTerm(event.target.value);
80 | };
81 |
82 | const search = (todos, searchTerm) =>
83 | todos.filter(({ title }) =>
84 | title.toLowerCase().includes(searchTerm.toLowerCase())
85 | );
86 |
87 | return { searchTerm, search, handleSearchInput };
88 | };
89 |
90 | const Todo = () => {
91 | const { searchTerm, search, handleSearchInput } = useSearch();
92 | const {
93 | todos,
94 | todoTitle,
95 | category,
96 | handleTodoChange,
97 | handleAddTodo,
98 | handleDeleteTodo,
99 | handleTodoCheck,
100 | handleCategory,
101 | } = useTodo();
102 |
103 | const filteredByCategories = filterByCategory(todos, category);
104 | const filteredTodos = search(filteredByCategories, searchTerm);
105 |
106 | return (
107 | <>
108 | ToDo
109 |
110 |
111 | add todo
112 |
113 |
114 | Search
115 |
116 |
117 |
118 |
119 | All
120 | Active
121 |
122 | Completed
123 |
124 |
125 |
126 |
139 | >
140 | );
141 | };
142 |
143 | export default Todo;
144 |
--------------------------------------------------------------------------------
/react/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/react/public/favicon.ico
--------------------------------------------------------------------------------
/react/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/react/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/testing/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Testing
4 |
5 | Research, studies, and practice on Frontend Testing
6 |
7 | - [Testable Frontend: The Good, The Bad And The Flaky](https://www.smashingmagazine.com/2022/07/testable-frontend-architecture)
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui-challenges/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # UI Challenges
4 |
5 | UI Challenges is my training lab to practice and improve my HTML & CSS skills
6 |
7 | ## Challenges
8 |
9 | | UI Challenge | Solution | Code |
10 | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------- |
11 | | [Pokemon Card](https://dribbble.com/shots/4619445-Charmeleon) | [:bulb:](https://imteekay.github.io/crafting-frontend/ui-challenges/pokemon-card) | [:computer:](./pokemon-card) |
12 | | [Smooth Scrollable List](https://css-tricks.com/css-only-carousel) | [:bulb:](https://imteekay.github.io/crafting-frontend/ui-challenges/smooth-scrollable-list) | [:computer:](./smooth-scrollable-list) |
13 | | [Read more content](https://www.youtube.com/watch?v=kQW-MXriUIU) | [:bulb:](https://imteekay.github.io/crafting-frontend/ui-challenges/read-more) | [:computer:](./read-more) |
14 | | [Year Progress Bar](https://hugovk.github.io/year-progress-bar) | [:bulb:](https://imteekay.github.io/crafting-frontend/ui-challenges/year-progress-bar) | [:computer:](./year-progress-bar) |
15 |
16 | ## Copying the template to create a new challenge
17 |
18 | ```bash
19 | mkdir pokemon && cp template/index.html pokemon && cp template/style.css pokemon
20 | ```
21 |
22 | \*`pokemon` is the new folder for the challenge por example
23 |
24 | ## Request a Challenge | Challenge me
25 |
26 | Feel free to send me new UI Challenges by [creating a new issue](https://github.com/imteekay/ui-challenges/issues/new) in this project.
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/README.md:
--------------------------------------------------------------------------------
1 | # Pokemon Card
2 |
3 | A UI implementation for a pokemon card
4 |
5 | ## Challenge
6 |
7 | Transform this [Dribble Design](https://dribbble.com/shots/4619445-Charmeleon) into a UI Card.
8 |
9 | ## Inspiration
10 |
11 | 
12 |
13 | ## Implementation
14 |
15 | 
16 |
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/images/charmeleon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/pokemon-card/images/charmeleon.png
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/images/fire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/pokemon-card/images/fire.png
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/images/pokemon-challenge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/pokemon-card/images/pokemon-challenge.png
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/images/ui-charmeleon-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/pokemon-card/images/ui-charmeleon-card.png
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pokemon
5 |
6 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
CHARMELEON
22 |
23 |
24 |
25 |
Height
26 |
1.09M
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Abilities
34 |
Blaze
35 |
36 |
37 |
38 |
39 |
Weight
40 |
19.9kg
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ui-challenges/pokemon-card/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 30px auto;
3 | font-family: "Fira Sans", sans-serif;
4 | }
5 |
6 | .pokemon-card {
7 | width: 350px;
8 | margin: auto;
9 | padding: 25px;
10 | position: relative;
11 | -webkit-box-shadow: -2px 2px 5px -1px rgba(0, 0, 0, 0.6);
12 | -moz-box-shadow: -2px 2px 5px -1px rgba(0, 0, 0, 0.6);
13 | box-shadow: -2px 2px 5px -1px rgba(0, 0, 0, 0.6);
14 | }
15 |
16 | svg {
17 | display: inline-block;
18 | position: absolute;
19 | top: 0;
20 | left: 0;
21 | z-index: -1;
22 | }
23 |
24 | .pokemon-card .pokemon-image {
25 | margin: 40px auto 0;
26 | display: block;
27 | }
28 |
29 | .pokemon-card h1 {
30 | text-align: center;
31 | font-weight: 700;
32 | letter-spacing: 1px;
33 | margin-bottom: 40px;
34 | font-size: 34px;
35 | }
36 |
37 | .characteristics {
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | }
42 |
43 | .characteristics .middle-characteristics {
44 | margin-left: 30px;
45 | margin-right: 30px;
46 | }
47 |
48 | .characteristics .pokemon-type {
49 | width: 55px;
50 | margin: 0 auto 20px;
51 | display: block;
52 | border-radius: 50px;
53 | padding: 4px;
54 | border: 1px solid;
55 | }
56 |
57 | .characteristics .height,
58 | .characteristics .abilities,
59 | .characteristics .weight {
60 | text-align: center;
61 | }
62 |
63 | .characteristics .height h2,
64 | .characteristics .abilities h2,
65 | .characteristics .weight h2 {
66 | font-weight: normal;
67 | font-size: 20px;
68 | margin: 0;
69 | }
70 |
71 | .characteristics .height p,
72 | .characteristics .abilities p,
73 | .characteristics .weight p {
74 | font-weight: 700;
75 | font-size: 35px;
76 | margin: 0;
77 | }
78 |
--------------------------------------------------------------------------------
/ui-challenges/read-more/README.md:
--------------------------------------------------------------------------------
1 | # Read More
2 |
3 | An implementation of the "Read More" UI for articles.
4 |
5 | - [demo](https://leandrotk.github.io/ui-challenges/read-more/)
6 |
7 | https://user-images.githubusercontent.com/5835798/141539398-1297b8cb-e8d6-4e53-b448-aef8deb33477.mov
8 |
--------------------------------------------------------------------------------
/ui-challenges/read-more/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Read More
5 |
6 |
7 |
8 |
9 |
10 |
Title of an article
11 |
12 |
13 |
14 |
15 |
20 |
21 | PATRICK T. FALLON/AFP via Getty Glen M. de Vries
22 |
23 |
24 |
25 |
26 | A Blue Origin crew member who recently joined
27 | William Shatner
30 | on his
31 | journey to space
35 | is one of two people who died after a plane crashed in New Jersey on
36 | Thursday, according to authorities.
37 |
38 |
39 | In a statement to PEOPLE, the New Jersey State Police (NJSP)
40 | confirmed that the fatal incident unfolded just before 3 p.m. in
41 | Hampton Township in Sussex County.
42 |
43 |
44 | State police said their troopers responded at 2:50 p.m. after
45 | receiving a report of a small aircraft crash and discovered two
46 | fatalities at the scene.
47 |
48 |
49 | According to
50 | an online incident report
54 | from the Federal Aviation Administration (FAA), the two victims, a
55 | pilot and passenger, were the only people on board at the time.
56 |
57 |
58 | They were later identified as Glen M. de Vries, 49, of New
59 | York, New York, and Thomas P. Fischer, 54, of Hopatcong, New Jersey,
60 | NJSP confirmed.
61 |
62 |
63 | The FAA has not identified who the pilot of the aircraft was.
64 | According to the Fischer Aviation website , de Vries began his private pilot training with Fischer in
68 | February 2016.
69 |
70 |
71 | RELATED:
73 | 'Doting' Father and Daughter, 14, Identified as Victims of Ga.
76 | Plane Crash That Also Killed 2 Others
79 |
80 |
81 | de Vries was one of four crew members aboard the New Shepard
82 | rocket for the NS-18 mission last month — which he called his "oldest, and lifelong enduring, dream
86 | according to a social media post. The trip was made possible
87 | by Jeff Bezos '
88 | aerospace company Blue Origin. In addition to being a
89 | Carnegie Melon University trustee, he co-founded of Medidata
90 | Solutions, a tech company that develops software as a service,
91 | per their website .
94 |
95 |
96 | "We are devastated to hear of the sudden passing of Glen de Vries,"
97 | Blue Origin
98 | wrote
101 | in a statement shared on social media. "He brought so much life and
102 | energy to the entire Blue Origin team and to his fellow crewmates.
103 | His passion for aviation, his charitable work, and his dedication to
104 | his craft will long be revered and admired.
105 |
106 |
107 | "The world lost a visionary," Nadia M. Bracken, who works at
108 | Medidata Solutions,
109 | wrote
113 | in another tribute on Twitter. "May his legacy of innovation in the
114 | life sciences industry live on. #innovator #restinpeace #spaceman"
115 |
116 |
146 |
147 |
148 | Fischer was also an avid air traveler, working as the owner and
149 | second-generation chief flight instructor at Fischer Aviation in New
150 | Jersey,
151 | according to their website .
154 |
155 |
156 | In their report, the FAA stated that the single-engine Cessna 172
157 | plane was "destroyed" in a heavily wooded area but the cause remains
158 | unknown. The FAA will be the lead investigating agency, according to
159 | NJSP.
160 |
161 |
162 | Never miss a story — sign up for PEOPLE's free weekly newsletter
170 | to get the biggest news of the week delivered to your inbox
171 | every Friday.
174 |
175 |
176 | RELATED:
178 | William Shatner Flies to Space, Becoming the Oldest Person to
181 | Ever Leave Earth: 'Life-Changing'
184 |
185 |
186 | A 1994 graduate of Carnegie Mellon University with a degree in
187 | molecular biology and genetics, de Vries had an obsession with
188 | aerospace aviation since he was young, per
189 | a profile on the university's website .
193 |
194 |
195 | Over the years, he read every book he could about rockets, aircraft
196 | and spaceships before training to pilot his own single-engine plane,
197 | his profile stated.
198 |
199 |
200 | RELATED VIDEO: William Shatner and Blue Origin Passengers Float
202 | In Space
204 |
205 |
206 | William Shatner and Blue Origin Passengers Float In Space
207 |
208 |
209 | The "Star Trek" Actor and other crew members float in the space
210 | capsule as it reached apogee.
211 |
212 |
213 |
214 | In October, de Vries' longstanding dream of going to space finally
215 | came true when he
216 | blasted into space
220 | with Shatner, as well as Audrey Powers, Blue Origin's vice president
221 | of mission and flight operations, and fellow crew member and
222 | co-founder of Planet Labs, Chris Boshuizen.
223 |
224 |
225 | Speaking to Carnegie Mellon after the journey, de Vries said, "I
226 | honestly don't think anybody could go to space and not want to go to
227 | space more, so I would love to again."
228 |
229 |
230 | "I think I've taken that perspective [of a heightened sense of time]
231 | back down with me to our planet, and into my relationships," he
232 | added. "The passage of time, just like the resources on Earth, feels
233 | more precious with expanded perspective."
234 |
235 |
236 |
237 | Read more
238 |
245 |
249 |
250 |
251 |
252 |
253 |
254 |
266 |
267 |
268 |
--------------------------------------------------------------------------------
/ui-challenges/read-more/style.css:
--------------------------------------------------------------------------------
1 | .content-wrapper {
2 | height: 70vh;
3 | overflow: hidden;
4 | backdrop-filter: blur(5px);
5 | }
6 |
7 | .fade-bottom {
8 | background: linear-gradient(
9 | to bottom,
10 | rgba(255, 255, 255, 0) 0%,
11 | rgba(var(--f23, 255, 255, 255), 1) 100%
12 | );
13 | bottom: 0;
14 | height: 60px;
15 | left: 0;
16 | position: absolute;
17 | width: 100%;
18 | }
19 |
20 | .read-more {
21 | margin-top: 8px;
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | gap: 8px;
26 | cursor: pointer;
27 | }
28 |
--------------------------------------------------------------------------------
/ui-challenges/smooth-scrollable-list/README.md:
--------------------------------------------------------------------------------
1 | # Smooth Scrollable List
2 |
3 | A UI implementation of a smooth scrollable list: carousel of items
4 |
5 | - [demo](https://leandrotk.github.io/ui-challenges/smooth-scrollable-list/)
6 |
7 | https://user-images.githubusercontent.com/5835798/140621151-6496b6f3-1886-4d22-8921-6ed54887f573.mov
8 |
--------------------------------------------------------------------------------
/ui-challenges/smooth-scrollable-list/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Smooth Scrollable List
5 |
6 |
7 |
8 |
9 |
10 | Smooth Scrollable List
11 |
12 |
13 |
Item 1
14 |
Item 2
15 |
Item 3
16 |
Item 4
17 |
Item 5
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ui-challenges/smooth-scrollable-list/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Arial';
3 | padding: 1rem;
4 | background-color: #f7f7f7;
5 | }
6 |
7 | .item1 {
8 | background-color: chartreuse;
9 | }
10 |
11 | .item2 {
12 | background-color: cyan;
13 | }
14 |
15 | .item3 {
16 | background-color: lightcoral;
17 | }
18 |
19 | .item4 {
20 | background-color: slateblue;
21 | }
22 |
23 | .item5 {
24 | background-color: slategray;
25 | }
26 |
27 | /* smooth scrollable list style */
28 |
29 | .section {
30 | display: flex;
31 | gap: 20px;
32 | max-width: 450px;
33 | overflow-x: auto;
34 | scroll-snap-type: x mandatory;
35 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
36 | -webkit-overflow-scrolling: touch;
37 | scroll-padding: 1rem;
38 | padding: 1rem;
39 | margin: 3rem auto 0;
40 | border-radius: 5px;
41 | }
42 |
43 | .section__item {
44 | display: flex;
45 | flex: 0 0 300px;
46 | width: 30px;
47 | height: 150px;
48 | scroll-snap-align: start;
49 | }
50 |
--------------------------------------------------------------------------------
/ui-challenges/template/README.md:
--------------------------------------------------------------------------------
1 | # UI Challenge Name
2 |
3 | A UI implementation for a [name the challenge]
4 |
5 | ## Challenge
6 |
7 | Transform this Dribble Design into a UI Card.
8 |
9 | ## Inspiration
10 |
11 | 
12 |
13 | ## Implementation
14 |
15 | 
16 |
--------------------------------------------------------------------------------
/ui-challenges/template/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Template Title
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ui-challenges/template/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/template/style.css
--------------------------------------------------------------------------------
/ui-challenges/year-progress-bar/README.md:
--------------------------------------------------------------------------------
1 | # Year Progress Bar
2 |
3 | A UI implementation for the Year Progrss Bar.
4 |
5 | ## Challenge
6 |
7 | Build a progress representing the percentage % of the year.
8 |
9 | ## Implementation
10 |
11 | 
12 |
--------------------------------------------------------------------------------
/ui-challenges/year-progress-bar/images/year-progress-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imteekay/crafting-frontend/aa35826636738ca3430471fc5572e804bfd47ed2/ui-challenges/year-progress-bar/images/year-progress-bar.png
--------------------------------------------------------------------------------
/ui-challenges/year-progress-bar/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | Year Progress Bar
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ui-challenges/year-progress-bar/index.js:
--------------------------------------------------------------------------------
1 | const currentProgress = document.getElementById('current-progress');
2 | const percentageProgress = document.getElementById('percentage-progress');
3 |
4 | const today = new Date();
5 | const year = today.getFullYear();
6 |
7 | const isLeapYear = (year) =>
8 | (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
9 |
10 | const getFebruaryDays = (year) => (isLeapYear(year) ? 29 : 28);
11 |
12 | const daysPerMonth = {
13 | 1: 31,
14 | 2: getFebruaryDays(year),
15 | 3: 31,
16 | 4: 30,
17 | 5: 31,
18 | 6: 30,
19 | 7: 31,
20 | 8: 31,
21 | 9: 30,
22 | 10: 31,
23 | 11: 30,
24 | 12: 31,
25 | };
26 |
27 | const countYearDays = (daysPerMonth) =>
28 | Object.values(daysPerMonth).reduce((acc, curr) => acc + curr);
29 |
30 | const days = countYearDays(daysPerMonth);
31 |
32 | const firstDate = new Date(`${year}-01-01`);
33 | const lastDate = new Date(`${year}-12-31`);
34 |
35 | const toDays = (milliseconds) =>
36 | Math.ceil(milliseconds / (1000 * 60 * 60 * 24));
37 |
38 | const remainingMilliseconds = lastDate.getTime() - today.getTime();
39 | const passedMilliseconds = today.getTime() - firstDate.getTime();
40 |
41 | const passedDays = toDays(passedMilliseconds);
42 | const toPercetage = (passedDays, days) => (passedDays / days) * 100;
43 |
44 | const percentage = Math.floor(toPercetage(passedDays, days));
45 | const stringPercentage = `${percentage}%`;
46 |
47 | currentProgress.style.width = stringPercentage;
48 | percentageProgress.textContent = stringPercentage;
49 |
--------------------------------------------------------------------------------
/ui-challenges/year-progress-bar/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | }
5 |
6 | .container {
7 | height: 100%;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | flex-direction: column;
12 | }
13 |
14 | .year-progress-bar {
15 | border: 1px solid;
16 | height: 30px;
17 | width: 400px;
18 | position: relative;
19 | }
20 |
21 | .current-progress {
22 | content: '\A';
23 | position: absolute;
24 | background: black;
25 | top: 0;
26 | bottom: 0;
27 | left: 0;
28 | }
29 |
--------------------------------------------------------------------------------