├── .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 ; 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 | 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 |
39 | 43 | 44 | 48 | 49 | 59 | 60 | 61 |
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 ; 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 | 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 | 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 | 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 |
42 |
43 |

Navbar

44 |
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 | ![tag-input](/images/tag-input.png) 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 | 14 | ); 15 | }; 16 | 17 | const Label = ({ children }) => {children}; 18 | 19 | const Tag = ({ tagId, tag }) => ( 20 | 23 | 24 | 112 | 113 |

114 | Search 115 | 116 |

117 | 118 |
119 | 120 | 121 | 124 |
125 | 126 |
    127 | {filteredTodos.map(({ id, title, checked }) => ( 128 |
  • 129 | 134 | {title} 135 | 136 |
  • 137 | ))} 138 |
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 | ![alt text](https://github.com/leandrotk/ui-challenges/blob/master/pokemon-card/images/pokemon-challenge.png "UI Challenge Inspiration") 12 | 13 | ## Implementation 14 | 15 | ![alt text](https://github.com/leandrotk/ui-challenges/blob/master/pokemon-card/images/ui-charmeleon-card.png "UI Implementation") 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 | ![alt text](https://github.com/leandrotk/ui-challenges/blob/master/pokemon-card/images/pokemon-challenge.png 'UI Challenge Inspiration') 12 | 13 | ## Implementation 14 | 15 | ![alt text](https://github.com/leandrotk/ui-challenges/blob/master/pokemon-card/images/ui-charmeleon-card.png 'UI Implementation') 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 | ![](./images/year-progress-bar.png) 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 |
15 |

20%

16 |
17 |
18 |
19 |
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 | --------------------------------------------------------------------------------