├── .DS_Store ├── .gitignore ├── 01.custom-video-player ├── index.html ├── scripts │ ├── index.js │ └── video.js ├── static │ ├── custom-video-player-ss.jpg │ ├── poster.jpg │ └── video.mp4 └── styles │ └── styles.css ├── 02.lovely-movies ├── index.html ├── scripts │ ├── index.js │ └── services.js ├── static │ └── lovely-movies-ss.jpg └── styles │ └── styles.css ├── 03.notes-app ├── index.html ├── scripts │ ├── index.js │ ├── note-dom-helper.js │ └── note.js ├── static │ └── notes-app-ss.jpg └── styles │ └── styles.css ├── 04.othello-board-game ├── index.html ├── scripts │ ├── board.js │ ├── cell.js │ └── index.js └── static │ └── othello-board-game-ss.jpg ├── 05.quiz-app ├── index.html ├── scripts │ ├── elements-helper.js │ ├── index.js │ └── quiz.js ├── static │ └── quiz-app-ss.jpg └── styles │ └── styles.css ├── 06.simple-range-slider ├── index.html ├── scripts │ ├── index.js │ └── simple-range-slider.js ├── static │ └── simple-range-slider-ss.jpg └── styles │ └── styles.css ├── 07.web-chat-app ├── components │ ├── active-chat.js │ ├── app-brand.js │ ├── authed-user.js │ ├── chat-box.js │ ├── chat-list-item.js │ ├── chat-message.js │ ├── chats-list.js │ ├── component.js │ └── new-message.js ├── index.html ├── scripts │ ├── chat-app.js │ ├── data-factory.js │ ├── index.js │ └── recorder.js ├── static │ ├── chat-box-bg.png │ ├── chat-placeholder.svg │ └── mic.svg └── styles │ └── styles.css ├── 08.canvas-wallpaper ├── index.html ├── scripts │ ├── circle.js │ └── index.js └── styles │ └── styles.css ├── 09.split-screen ├── index.html ├── scripts │ └── main.js ├── static │ └── img │ │ ├── bg-bw.jpg │ │ └── bg-colored.jpg └── style │ └── style.css ├── 10.responsive-font-size ├── index.html └── style │ └── style.css ├── 11.css-escape-loading-animation ├── index.html └── styles │ └── styles.css ├── 12.image-slider-3d ├── index.html ├── scripts │ └── index.js ├── static │ └── images │ │ ├── img1.png │ │ ├── img10.png │ │ ├── img2.png │ │ ├── img3.png │ │ ├── img4.jpg │ │ ├── img5.jpg │ │ ├── img6.jpg │ │ ├── img7.jpg │ │ ├── img8.png │ │ └── img9.png └── styles │ └── styles.css └── readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | .idea 4 | .idea/* 5 | project-template 6 | parcel 7 | -------------------------------------------------------------------------------- /01.custom-video-player/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom Video Player 8 | 9 | 10 |
11 |

Custom Video Player

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /01.custom-video-player/scripts/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const video = new Video({ 4 | wrapperID: "video-wrapper", 5 | videoSrc: "static/video.mp4", 6 | posterSrc: "static/poster.jpg", 7 | absolute: true, 8 | hideControlsOnPlay: true, 9 | progressColor: "white" 10 | }); 11 | 12 | console.log(video); 13 | -------------------------------------------------------------------------------- /01.custom-video-player/static/custom-video-player-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/01.custom-video-player/static/custom-video-player-ss.jpg -------------------------------------------------------------------------------- /01.custom-video-player/static/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/01.custom-video-player/static/poster.jpg -------------------------------------------------------------------------------- /01.custom-video-player/static/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/01.custom-video-player/static/video.mp4 -------------------------------------------------------------------------------- /01.custom-video-player/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .m-video { 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | --progress-color: red; 13 | } 14 | 15 | .m-video video { 16 | width: 100%; 17 | max-width: 100%; 18 | } 19 | 20 | .m-video video::-webkit-media-controls-enclosure, 21 | .m-video video::-webkit-media-controls { 22 | display: none !important; 23 | } 24 | 25 | .m-video input[type="range"] { 26 | height: 4px; 27 | -webkit-appearance: none; 28 | outline: none; 29 | background: transparent; 30 | overflow: hidden; 31 | cursor: pointer; 32 | border-radius: 4px; 33 | } 34 | 35 | .m-video .v-controls { 36 | background: rgba(0, 0, 0, .6); 37 | padding: .5rem; 38 | color: #fff; 39 | display: flex; 40 | flex-direction: column; 41 | flex-wrap: nowrap; 42 | width: 100%; 43 | } 44 | 45 | .m-video .v-controls.--fs-abs, 46 | .m-video .v-controls.--absolute { 47 | position: absolute; 48 | width: 100%; 49 | left: 0; 50 | bottom: 0; 51 | } 52 | 53 | .m-video.playing .v-controls.--absolute.--auto-hide, 54 | .m-video .v-controls:not(.--absolute).--fs-abs { 55 | opacity: 0; 56 | transition: .3s; 57 | } 58 | 59 | .m-video .v-controls:hover.--absolute.--auto-hide, 60 | .m-video .v-controls:hover.--fs-abs { 61 | opacity: 1; 62 | } 63 | 64 | 65 | .m-video .v-controls svg { 66 | width: 24px; 67 | height: 24px; 68 | fill: none; 69 | stroke: #fff; 70 | stroke-width: 2; 71 | stroke-linecap: round; 72 | stroke-linejoin: round; 73 | } 74 | 75 | .m-video button { 76 | outline: none; 77 | background: transparent; 78 | border: none; 79 | cursor: pointer; 80 | width: 30px; 81 | height: 30px; 82 | padding: 0; 83 | margin: 0; 84 | display: inline-flex; 85 | justify-content: center; 86 | align-items: center; 87 | box-shadow: none; 88 | } 89 | 90 | .m-video button:focus, 91 | .m-video button::-moz-focus-inner { 92 | border: 0; 93 | outline: none; 94 | } 95 | 96 | /* Slider in Chrome, Firefox, and Opera */ 97 | .m-video input[type="range"]::-webkit-slider-runnable-track { 98 | background: rgba(255, 255, 255, 0.6); 99 | height: 4px; 100 | border-radius: 4px; 101 | border: 0; 102 | width: 100%; 103 | } 104 | 105 | .m-video input[type="range"]::-moz-range-track { 106 | background: rgba(255, 255, 255, 0.6); 107 | height: 4px; 108 | border-radius: 4px; 109 | border: 0; 110 | width: 100%; 111 | } 112 | 113 | .m-video input[type="range"]::-ms-track { 114 | background: rgba(255, 255, 255, 0.6); 115 | height: 4px; 116 | border-radius: 4px; 117 | border: 0; 118 | width: 100%; 119 | } 120 | 121 | .m-video input[type="range"]::-ms-fill-lower { 122 | background: var(--progress-color, red); 123 | } 124 | 125 | .m-video input[type="range"]::-moz-range-progress { 126 | background: var(--progress-color, red); 127 | } 128 | 129 | .m-video input[type="range"]::-webkit-slider-thumb { 130 | -webkit-appearance: none; 131 | background: var(--progress-color, red); 132 | width: 4px; 133 | height: 4px; 134 | margin-top: 0; 135 | border-radius: 50%; 136 | cursor: pointer; 137 | } 138 | 139 | .m-video input[type="range"]::-moz-range-thumb { 140 | -moz-appearance: none; 141 | background: var(--progress-color, red); 142 | width: 4px; 143 | height: 4px; 144 | margin-top: 0; 145 | border-radius: 50%; 146 | cursor: pointer; 147 | } 148 | 149 | .m-video input[type="range"]::-ms-thumb { 150 | background: var(--progress-color, red); 151 | width: 4px; 152 | height: 4px; 153 | margin-top: 0; 154 | border-radius: 50%; 155 | cursor: pointer; 156 | } 157 | 158 | .m-video input[type="range"]::-webkit-slider-thumb { 159 | box-shadow: -1200px 0 0 1200px var(--progress-color, red); 160 | outline: none; 161 | border: 0; 162 | } 163 | 164 | .m-video .v-controls__btns { 165 | display: flex; 166 | flex-wrap: nowrap; 167 | justify-content: space-between; 168 | align-items: center; 169 | } 170 | 171 | .m-video .v-controls__btns .v-controls__btns__sound { 172 | width: 30%; 173 | flex: 0 0 30%; 174 | display: flex; 175 | justify-content: flex-start; 176 | align-items: center; 177 | } 178 | 179 | .m-video .v-controls__btns .v-controls__btns__sound input[type="range"] { 180 | max-width: 90px; 181 | } 182 | 183 | .m-video .v-controls__btns .v-controls__btns_play { 184 | width: 40%; 185 | flex: 0 0 40%; 186 | display: flex; 187 | justify-content: center; 188 | align-items: center; 189 | } 190 | 191 | .m-video .v-controls__btns .v-controls__btns_fs { 192 | width: 30%; 193 | flex: 0 0 30%; 194 | display: flex; 195 | justify-content: flex-end; 196 | } 197 | 198 | .m-video .v-controls .v-controls__timing { 199 | display: flex; 200 | justify-content: center; 201 | align-items: center; 202 | margin-top: .5rem; 203 | } 204 | 205 | .m-video .v-controls .v-controls__timing input[type="range"] { 206 | width: 100%; 207 | margin: 0 1rem; 208 | } 209 | 210 | .m-video .v-controls .v-controls__timing span { 211 | text-align: center; 212 | width: 58px; 213 | display: inline-block; 214 | } 215 | -------------------------------------------------------------------------------- /02.lovely-movies/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Lovely Movies 9 | 10 | 11 | 12 |
13 |

Lovely Movies

14 | 19 |
20 |
21 |
22 |

Result for "Lovely

23 | 24 |
25 | 26 |
27 |
28 | × 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /02.lovely-movies/scripts/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const resultWrapper = document.getElementById("result-wrapper"); 4 | const detailsWrapper = document.getElementById("id-details-wrapper"); 5 | const searchInput = document.getElementById("search"); 6 | const searchTrendSpan = document.getElementById("search-trend"); 7 | const pageNumberSpan = document.getElementById("page-number"); 8 | const nextBtn = document.getElementById("next-btn"); 9 | 10 | let SEARCH_DEBOUNCE_FLAG = null; 11 | let CURRENT_PAGE = 1; 12 | 13 | window.onload = function onLoadDone() { 14 | document.body.classList.add("loaded"); 15 | }; 16 | 17 | document.addEventListener("DOMContentLoaded", () => { 18 | 19 | if (!resultWrapper) 20 | throw new Error("Result wrapper is not exist"); 21 | 22 | if (!detailsWrapper) 23 | throw new Error("Details wrapper is not exist"); 24 | 25 | // init movies list 26 | initialMovieList(); 27 | 28 | initListeners(); 29 | }); 30 | 31 | window.onresize = function () { 32 | calcItemsSize(); 33 | }; 34 | 35 | function initialMovieList() { 36 | getMovies("Lovely") 37 | .then(({movies = [], totalResult = 0}) => { 38 | movies.map(generateMovieItem) 39 | }); 40 | 41 | } 42 | 43 | function initListeners() { 44 | detailsWrapper.querySelector(".movie-details__close") 45 | .addEventListener("click", closeDetailsSection); 46 | 47 | searchInput.addEventListener("input", searchInMovies); 48 | 49 | nextBtn.addEventListener("click", nextBtnClickHandler) 50 | } 51 | 52 | function searchInMovies(e) { 53 | if (SEARCH_DEBOUNCE_FLAG) 54 | clearTimeout(SEARCH_DEBOUNCE_FLAG); 55 | SEARCH_DEBOUNCE_FLAG = setTimeout(() => { 56 | 57 | let trend = e.target.value; 58 | if (!trend) trend = "Lovely"; 59 | 60 | // search with a trend less than 3 chars cause an error on omdbapi 61 | if (trend.length < 3) 62 | return; 63 | 64 | // reset page number on each search 65 | CURRENT_PAGE = 1; 66 | 67 | trend = trend.trim() 68 | 69 | getMoviesAndParse(trend, CURRENT_PAGE) 70 | 71 | }, 300) 72 | } 73 | 74 | function nextBtnClickHandler() { 75 | 76 | let trend = searchInput.value; 77 | if (!trend) trend = "Lovely"; 78 | 79 | // search with a trend less than 3 chars cause an error on omdbapi 80 | if (trend.length < 3) 81 | return; 82 | 83 | getMoviesAndParse(trend, ++CURRENT_PAGE) 84 | } 85 | 86 | function getMoviesAndParse(trend, page) { 87 | 88 | resultWrapper.innerHTML = ''; 89 | 90 | // update search-trend span 91 | searchTrendSpan.innerText = trend.length < 10 ? trend : trend.substr(0, 8) + '...'; 92 | nextBtn.style.display = "none"; 93 | pageNumberSpan.innerText = ''; 94 | 95 | // handle search 96 | getMovies(trend, page) 97 | .then(({movies = [], totalResults = 0}) => { 98 | 99 | if ((page * 10) < +totalResults) { 100 | pageNumberSpan.innerText = `| Page: ${page}`; 101 | nextBtn.style.display = "inline-block"; 102 | } 103 | 104 | if (movies.length) 105 | movies.map(generateMovieItem); 106 | else 107 | generateNoContentPlaceholder(); 108 | 109 | // scroll to top after fetch 110 | window.scrollTo({ 111 | top: 0, 112 | behavior: "smooth" 113 | }) 114 | }); 115 | } 116 | 117 | function generateMovieItem(item) { 118 | let movieElm = document.createElement("div"); 119 | movieElm.setAttribute("data-imdbid", item.imdbID); 120 | movieElm.classList.add("movie-item"); 121 | 122 | movieElm.addEventListener("click", handleMovieItemClick); 123 | 124 | movieElm.innerHTML = ` 125 |
127 |

${item.Title}

128 |
129 | ${item.Year} 130 |  -  131 | 132 | IMDB 133 |
`; 134 | 135 | resultWrapper.append(movieElm) 136 | } 137 | 138 | function generateNoContentPlaceholder() { 139 | let placeholderElm = document.createElement("p"); 140 | placeholderElm.classList.add("no-content-placeholder"); 141 | 142 | placeholderElm.innerText = `Movies not found.`; 143 | 144 | resultWrapper.append(placeholderElm) 145 | } 146 | 147 | function handleMovieItemClick(e) { 148 | const movieItem = e.target.closest(".movie-item"); 149 | const movieItemID = movieItem.getAttribute("data-imdbid"); 150 | 151 | // handle class toggle 152 | removeDetailsClassFromItems(); 153 | movieItem.classList.add("--in-details"); 154 | 155 | // get movie from api 156 | getSingleMovie(movieItemID) 157 | .then(movieObj => { 158 | showMovieInDetails(movieObj, movieItem) 159 | }) 160 | } 161 | 162 | function calcItemsSize() { 163 | let columnsCount = Math.floor(resultWrapper.offsetWidth / 200) || 1; 164 | document.body.style.setProperty("--poster-height", (resultWrapper.offsetWidth / columnsCount) + "px"); 165 | document.body.style.setProperty("--result-grid-column", columnsCount.toString()); 166 | } 167 | 168 | function removeDetailsClassFromItems() { 169 | document.querySelectorAll(".movie-item").forEach(mi => { 170 | mi.classList.remove("--in-details"); 171 | }); 172 | } 173 | 174 | function closeDetailsSection() { 175 | detailsWrapper.classList.remove("--visible"); 176 | removeDetailsClassFromItems(); 177 | calcItemsSize(); 178 | } 179 | 180 | function showMovieInDetails(movieObj, targetItem) { 181 | if (!detailsWrapper.classList.contains("--visible")) { 182 | detailsWrapper.classList.add("--visible"); 183 | } 184 | 185 | calcItemsSize(); 186 | 187 | // scroll to target movie element 188 | setTimeout(() => { 189 | window.scrollTo({ 190 | top: targetItem.offsetTop - 20, 191 | behavior: 'smooth' 192 | }) 193 | }, 50); 194 | 195 | let detailsElm = detailsWrapper.querySelector(".movie-details__inner"); 196 | if (!detailsElm) 197 | detailsElm = document.createElement("div"); 198 | 199 | detailsElm.classList.add("movie-details__inner"); 200 | 201 | if (!movieObj.Poster || movieObj.Poster === "N/A") 202 | detailsElm.classList.add("--no-poster"); 203 | else 204 | detailsElm.classList.remove("--no-poster"); 205 | 206 | detailsElm.innerHTML = ` 207 | 208 |
210 |
211 |

${movieObj.Title}

212 | ${movieObj.imdbRating} / 10 213 |
214 |
215 | Released:${movieObj.Released} -  216 | Runtime:${movieObj.Runtime} 217 |
218 |
Genre:${movieObj.Genre}
219 |
220 | Director:${movieObj.Director} -  221 | Writer:${movieObj.Writer} 222 |
223 |
224 | Country:${movieObj.Country} -  225 | Language:${movieObj.Language} 226 |
227 |
228 | Actors:${movieObj.Actors} 229 |
230 |
231 | Summary: 232 |

Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.

233 |
`; 234 | 235 | detailsWrapper.append(detailsElm); 236 | } 237 | -------------------------------------------------------------------------------- /02.lovely-movies/scripts/services.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const API_URL = "https://www.omdbapi.com/"; 4 | const API_KEY = "3ef4ab9e"; 5 | 6 | /** 7 | * Fetch movies from omdbapi 8 | * 9 | * @param trend 10 | * @param page 11 | * @returns {Promise} 12 | */ 13 | function getMovies(trend, page = 1) { 14 | return new Promise((resolve, reject) => { 15 | 16 | // start global loading... 17 | startLoading(); 18 | 19 | let URL = `${API_URL}?apiKey=${API_KEY}&s=${trend}&page=${page}`; 20 | fetch(URL) 21 | .then(response => response.json()) 22 | .then(res => { 23 | resolve({movies: res.Search || [], totalResults: res.totalResults || 0}) 24 | }) 25 | .catch(err => { 26 | reject(err) 27 | }) 28 | .finally(() => { 29 | // stop global loading 30 | stopLoading() 31 | }) 32 | }); 33 | } 34 | 35 | /** 36 | * Fetch single movie by ID from omdbapi 37 | * 38 | * @param id 39 | * @returns {Promise} 40 | */ 41 | function getSingleMovie(id) { 42 | return new Promise((resolve, reject) => { 43 | 44 | // start global loading... 45 | startLoading(); 46 | 47 | let URL = `${API_URL}?apiKey=${API_KEY}&i=${id}`; 48 | fetch(URL) 49 | .then(response => response.json()) 50 | .then(res => { 51 | resolve(res) 52 | }) 53 | .catch(err => { 54 | reject(err) 55 | }) 56 | .finally(() => { 57 | // stop global loading 58 | stopLoading() 59 | }) 60 | }); 61 | } 62 | 63 | function startLoading() { 64 | document.body.classList.add("loading") 65 | } 66 | 67 | function stopLoading() { 68 | document.body.classList.remove("loading") 69 | } 70 | -------------------------------------------------------------------------------- /02.lovely-movies/static/lovely-movies-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/02.lovely-movies/static/lovely-movies-ss.jpg -------------------------------------------------------------------------------- /02.lovely-movies/styles/styles.css: -------------------------------------------------------------------------------- 1 | /** Simple Reset - START */ 2 | html { 3 | box-sizing: border-box; 4 | font-size: 16px; 5 | overflow-x: hidden; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, h1, h2, h3, h4, h5, h6, p, ol, ul { 13 | margin: 0; 14 | padding: 0; 15 | font-weight: normal; 16 | } 17 | 18 | ol, ul { 19 | list-style: none; 20 | } 21 | 22 | img { 23 | max-width: 100%; 24 | height: auto; 25 | } 26 | 27 | 28 | /** Global Styles - START */ 29 | body { 30 | margin: 0; 31 | padding: 0; 32 | opacity: 0; 33 | transition: .8s; 34 | background: rgb(33, 34, 38); 35 | background: linear-gradient(180deg, rgba(33, 34, 38, 1) 0%, rgba(33, 37, 49, 1) 100%) no-repeat; 36 | color: #f4f4f4; 37 | min-height: 100vh; 38 | font-family: 'Lato', sans-serif; 39 | --header-height: 70px; 40 | --footer-height: 0px; 41 | --result-grid-column: 5; 42 | --poster-height: calc(100vw / 4); 43 | } 44 | 45 | body.loaded { 46 | opacity: 1; 47 | } 48 | 49 | body .loader { 50 | opacity: 0; 51 | visibility: hidden; 52 | } 53 | 54 | body.loading .loader { 55 | opacity: 1; 56 | visibility: visible; 57 | } 58 | 59 | /** Animations - START **/ 60 | @keyframes loader-anim { 61 | to { 62 | transform: rotate(360deg); 63 | } 64 | } 65 | 66 | /** Header - START **/ 67 | .main-header { 68 | padding: 1rem; 69 | background: rgb(0, 0, 0); 70 | background: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, rgba(4, 4, 4, 1) 100%) no-repeat; 71 | display: flex; 72 | justify-content: flex-start; 73 | align-items: center; 74 | box-shadow: 0 1px 10px 2px rgba(0, 0, 0, 0.3); 75 | height: 70px; 76 | position: sticky; 77 | top: 0; 78 | z-index: 1; 79 | } 80 | 81 | .main-header__branding { 82 | flex: 0 0 160px; 83 | font-size: 24px; 84 | } 85 | 86 | .main-header__search { 87 | width: calc(100% - 320px); 88 | flex: 0 0 calc(100% - 320px); 89 | text-align: center; 90 | position: relative; 91 | } 92 | 93 | .main-header__search .loader { 94 | position: absolute; 95 | top: calc(50% - 6px); 96 | margin-left: 4px; 97 | width: 12px; 98 | height: 12px; 99 | border-radius: 50%; 100 | border: 2px solid #888; 101 | border-left-color: transparent; 102 | animation: loader-anim 1s linear infinite; 103 | } 104 | 105 | .main-header__search input[type="search"] { 106 | background: #262626; 107 | min-width: 240px; 108 | padding: .5rem 1.3rem; 109 | border-radius: 5px; 110 | border: none; 111 | outline: none; 112 | color: #999; 113 | font-size: 14px; 114 | } 115 | 116 | .main-header__search input[type="search"]::-webkit-search-cancel-button { 117 | cursor: pointer; 118 | } 119 | 120 | 121 | /** Main Content - START **/ 122 | main.content { 123 | padding: 2rem 1rem 0 1rem; 124 | display: flex; 125 | min-height: calc(100% - var(--header-height, 70px) - var(--footer-height, 0px)); 126 | position: relative; 127 | } 128 | 129 | .content__result { 130 | width: 100%; 131 | display: inline-block; 132 | position: relative; 133 | } 134 | 135 | .content__result .loader { 136 | position: absolute; 137 | display: inline-block; 138 | margin: 2rem 1rem; 139 | width: 36px; 140 | height: 36px; 141 | border-radius: 50%; 142 | border: 2px solid #888; 143 | border-left-color: transparent; 144 | animation: loader-anim 1s linear infinite; 145 | } 146 | 147 | .content__result__next-btn { 148 | background: #d5475d; 149 | border: none; 150 | color: #f4f4f4; 151 | outline: none; 152 | padding: .3rem .85rem; 153 | border-radius: 3px; 154 | font-size: 16px; 155 | cursor: pointer; 156 | margin-bottom: 1rem; 157 | } 158 | 159 | .content__result__list { 160 | display: flex; 161 | flex-wrap: wrap; 162 | justify-content: flex-start; 163 | align-items: flex-start; 164 | margin-top: 1.7rem; 165 | } 166 | 167 | .movie-details { 168 | display: none; 169 | width: 0; 170 | position: sticky; 171 | top: 0; 172 | height: 100vh; 173 | overscroll-behavior: contain; 174 | overflow-x: hidden; 175 | overflow-y: auto; 176 | margin-right: -1rem; 177 | margin-left: 1rem; 178 | margin-top: 48px; 179 | padding: 1rem; 180 | background: #040404; 181 | box-shadow: 0 1px 10px 2px rgba(0, 0, 0, 0.3); 182 | } 183 | 184 | .movie-details.--visible { 185 | width: 50%; 186 | flex: 0 0 50%; 187 | display: block; 188 | } 189 | 190 | .movie-details__close { 191 | width: 32px; 192 | height: 32px; 193 | display: flex; 194 | justify-content: center; 195 | align-items: center; 196 | cursor: pointer; 197 | background: rgba(255, 255, 255, 0.1); 198 | border-radius: 50%; 199 | color: #fff; 200 | font-size: 18px; 201 | position: absolute; 202 | left: 1rem; 203 | top: 1rem; 204 | z-index: 100; 205 | transition: .3s; 206 | } 207 | 208 | .movie-details__close:hover { 209 | background: rgba(255, 255, 255, 0.2); 210 | 211 | } 212 | 213 | .movie-details__inner.--no-poster figure { 214 | display: none; 215 | } 216 | 217 | .movie-details__inner .loader { 218 | position: absolute; 219 | left: 0; 220 | top: 0; 221 | width: 100%; 222 | height: 100%; 223 | background: #000000b3; 224 | z-index: 100; 225 | display: flex; 226 | justify-content: center; 227 | align-items: center; 228 | } 229 | 230 | .movie-details__inner .loader:after { 231 | content: "Loading..."; 232 | display: block; 233 | text-align: center; 234 | position: absolute; 235 | top: 10%; 236 | } 237 | 238 | .movie-details__poster { 239 | overflow: hidden; 240 | width: calc(100% + 2rem); 241 | height: 50vh; 242 | background-color: #313030; 243 | background-repeat: no-repeat; 244 | background-position: center; 245 | background-size: contain; 246 | margin-top: -1rem; 247 | margin-left: -1rem; 248 | } 249 | 250 | .movie-details__title { 251 | margin: 1rem 0; 252 | } 253 | 254 | .movie-details__inner.--no-poster .movie-details__title { 255 | margin-top: 2rem; 256 | } 257 | 258 | .movie-details__title h2 { 259 | display: inline-block; 260 | } 261 | 262 | .movie-details__rating { 263 | display: inline-block; 264 | background: rgba(255, 255, 255, 0.2); 265 | padding: 2px 6px; 266 | font-size: 14px; 267 | border-radius: 3px; 268 | margin-left: 1rem; 269 | font-weight: bold; 270 | color: #e4ae17; 271 | } 272 | 273 | .movie-details__meta { 274 | display: block; 275 | margin-bottom: 1rem; 276 | color: #999999; 277 | line-height: 22px; 278 | } 279 | 280 | .movie-details__meta .--label { 281 | font-weight: bold; 282 | color: #aaa; 283 | margin-right: .2rem; 284 | } 285 | 286 | .no-content-placeholder { 287 | display: block; 288 | width: 100%; 289 | color: #999; 290 | font-size: 18px; 291 | } 292 | 293 | /** Movie Item - START **/ 294 | .movie-item { 295 | width: calc((100% - (var(--result-grid-column, 2) * .5rem)) / var(--result-grid-column, 3)); 296 | flex: 0 0 calc((100% - (var(--result-grid-column, 2) * 0.5rem)) / var(--result-grid-column, 3)); 297 | margin: 0 .5rem 1.5rem 0; 298 | cursor: pointer; 299 | padding: .5rem; 300 | } 301 | 302 | .movie-item:not(.--in-details):hover { 303 | transition: all .3s; 304 | transform: translateY(-3px); 305 | } 306 | 307 | .movie-item.--in-details { 308 | background: #37466d; 309 | border-radius: 5px; 310 | } 311 | 312 | .movie-item:nth-child(5) { 313 | margin-right: 0; 314 | } 315 | 316 | .movie-item__poster { 317 | box-shadow: 0 1px 10px 2px rgba(0, 0, 0, 0.3); 318 | border-radius: 5px; 319 | overflow: hidden; 320 | width: 100%; 321 | height: var(--poster-height, 200px); 322 | margin: 0; 323 | background-color: #313030; 324 | background-repeat: no-repeat; 325 | background-position: center; 326 | background-size: cover; 327 | position: relative; 328 | } 329 | 330 | .movie-item__poster:before { 331 | content: "Poster Not Found!"; 332 | display: flex; 333 | justify-content: center; 334 | align-items: center; 335 | color: #999999; 336 | height: 100%; 337 | font-size: 14px; 338 | opacity: .5; 339 | font-weight: normal; 340 | } 341 | 342 | .movie-item__title { 343 | font-size: 18px; 344 | margin: .6rem 0 .3rem; 345 | } 346 | 347 | .movie-item__meta { 348 | display: block; 349 | font-size: 14px; 350 | color: #999; 351 | } 352 | 353 | .movie-item__meta__imdb-link a { 354 | display: inline-block; 355 | color: #999; 356 | text-decoration: none; 357 | } 358 | 359 | .movie-item__meta__imdb-link a:hover { 360 | text-decoration: underline; 361 | } 362 | -------------------------------------------------------------------------------- /03.notes-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Notes App 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 | 27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 | 46 | 57 |
58 |
59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 | 69 | 70 | 71 |
72 |
73 | 74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /03.notes-app/scripts/index.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | "use strict"; 3 | const categoriesList = document.getElementById("categories-list"); 4 | const notesList = document.getElementById("notes-list"); 5 | const categoryAddEditForm = document.getElementById("add-category-form"); 6 | const noteAddEditForm = document.getElementById("note-add-edit-form"); 7 | const searchInput = document.getElementById("search-input"); 8 | const newNoteBtn = document.getElementById("new-note-btn"); 9 | const showAllNotesBtn = document.getElementById("show-all-notes"); 10 | const _note_app = new Note(); 11 | 12 | const alertBox = new AlertBox({}); 13 | 14 | function filterNotesList(trend = '') { 15 | const result = _note_app.filterNotes(trend); 16 | 17 | notesList.innerHTML = ''; 18 | // show result in list 19 | result.map(note => { 20 | notesList.appendChild(note.el) 21 | }); 22 | 23 | if (!result.length) 24 | notesList.innerHTML = `

Notes not found.

` 25 | } 26 | 27 | function handleCategoryAddUpdate(e) { 28 | e.preventDefault(); 29 | 30 | // find title input and get the value 31 | const titleInput = e.target.querySelector("input"); 32 | 33 | if (!titleInput.value) { 34 | alertBox.show({header: "Category Adding Error", message: "Enter a valid title.", buttonText: "OK!"}); 35 | titleInput.focus(); 36 | return 37 | } 38 | 39 | if (!_note_app.updatingCategoryID) { 40 | // unique id for each cat 41 | const id = new Date().getTime(); 42 | const newCat = new CategoryItem({title: titleInput.value.trim(), id}); 43 | 44 | // add new category to app 45 | _note_app.addCategory(newCat); 46 | 47 | } else { 48 | // update existing category 49 | _note_app.updateCategory(titleInput.value.trim()); 50 | } 51 | 52 | // reset input 53 | titleInput.value = ""; 54 | } 55 | 56 | function handleCategoryItemClick(e) { 57 | e.preventDefault(); 58 | 59 | const item = e.target.closest(".category-item"); 60 | if (!item) 61 | return; 62 | 63 | const targetItemID = item.getAttribute("data-cat-id"); 64 | 65 | const targetCategory = _note_app.getCategoryByID(targetItemID); 66 | if (!targetCategory) { 67 | alertBox.show({header: "Category Select Error", message: "Target category not found.", buttonText: "OK!"}); 68 | return; 69 | } 70 | 71 | // control item removing 72 | if (e.target.tagName === "BUTTON") { 73 | e.preventDefault(); 74 | const action = e.target.getAttribute("data-action"); 75 | if (action === "remove") { 76 | _note_app.removeCategory(targetItemID); 77 | 78 | } else if (action === "edit") { 79 | const catEditInput = categoryAddEditForm.querySelector("input"); 80 | _note_app.updatingCategoryID = targetCategory.data.id; 81 | catEditInput.value = targetCategory.data.title; 82 | catEditInput.focus(); 83 | } 84 | 85 | } else { 86 | // update selected category 87 | _note_app.selectedCategory = targetCategory; 88 | } 89 | 90 | filterNotesList() 91 | } 92 | 93 | function handleNoteAddUpdate(e) { 94 | e.preventDefault(); 95 | 96 | const titleInput = e.target.querySelector("input"); 97 | const contentArea = e.target.querySelector("textarea"); 98 | 99 | if (!titleInput.value) { 100 | alertBox.show({header: "Note Saving Error", message: "Enter a valid title.", buttonText: "OK!"}); 101 | titleInput.focus(); 102 | return 103 | } 104 | 105 | if (!_note_app.selectedCategory) { 106 | alertBox.show({header: "Note Saving Error", message: "Select a category first.", buttonText: "OK!"}); 107 | return; 108 | } 109 | 110 | const noteObj = { 111 | title: titleInput.value.trim(), 112 | content: contentArea.value.trim(), 113 | category: _note_app.selectedCategory.data.id // set category on note 114 | }; 115 | 116 | if (!_note_app.selectedNote) { 117 | // unique id for each note 118 | noteObj.id = new Date().getTime(); 119 | noteObj.created_at = new Date().getTime(); 120 | const newNote = new NoteItem(noteObj); 121 | 122 | // add new category to app 123 | _note_app.addNote(newNote); 124 | 125 | } else { 126 | noteObj.updated_at = new Date().getTime(); 127 | // update existing category 128 | _note_app.updateNote(noteObj); 129 | } 130 | 131 | } 132 | 133 | function handleNoteItemClick(e) { 134 | e.preventDefault(); 135 | 136 | const item = e.target.closest(".note-item"); 137 | if (!item) 138 | return; 139 | 140 | const targetItemID = item.getAttribute("data-note-id"); 141 | 142 | const targetNote = _note_app.getNoteById(targetItemID); 143 | if (!targetNote) { 144 | alertBox.show({header: "Note Select Error", message: "Target note not found.", buttonText: "OK!"}); 145 | return; 146 | } 147 | 148 | // control item removing 149 | if (e.target.tagName === "BUTTON") { 150 | e.preventDefault(); 151 | const action = e.target.getAttribute("data-action"); 152 | if (action === "remove") { 153 | _note_app.removeNote(targetItemID); 154 | } 155 | 156 | } else { 157 | // set selected notes values to the editor form 158 | const noteTitle = noteAddEditForm.querySelector("input"); 159 | const noteContent = noteAddEditForm.querySelector("textarea"); 160 | noteTitle.value = targetNote.data.title; 161 | noteContent.value = targetNote.data.content; 162 | 163 | // update selected category 164 | _note_app.selectedNote = targetNote 165 | } 166 | 167 | } 168 | 169 | function handleSearchInNotes(e) { 170 | filterNotesList(e.target.value) 171 | } 172 | 173 | function handleNewNoteBtnClick(e) { 174 | _note_app.selectedNote = null; 175 | const noteTitle = noteAddEditForm.querySelector("input"); 176 | const noteContent = noteAddEditForm.querySelector("textarea"); 177 | noteTitle.value = ''; 178 | noteContent.value = ''; 179 | noteTitle.focus(); 180 | 181 | } 182 | 183 | function handleShowAllNotesBtnClick(e) { 184 | _note_app.selectedCategory = null; 185 | filterNotesList() 186 | } 187 | 188 | function initListeners() { 189 | // add listener for category adding 190 | categoryAddEditForm.addEventListener("submit", handleCategoryAddUpdate); 191 | categoriesList.addEventListener("click", handleCategoryItemClick); 192 | 193 | // add listener to note form 194 | noteAddEditForm.addEventListener("submit", handleNoteAddUpdate); 195 | notesList.addEventListener("click", handleNoteItemClick); 196 | 197 | // add listener to control search 198 | searchInput.addEventListener("input", handleSearchInNotes); 199 | 200 | newNoteBtn.addEventListener("click", handleNewNoteBtnClick); 201 | 202 | showAllNotesBtn.addEventListener("click", handleShowAllNotesBtnClick); 203 | } 204 | 205 | initListeners(); 206 | 207 | })(); 208 | -------------------------------------------------------------------------------- /03.notes-app/scripts/note.js: -------------------------------------------------------------------------------- 1 | const LOCAL_NOTES_KEY = "APP_NOTES"; 2 | const LOCAL_CATS_KEY = "APP_CATS"; 3 | 4 | class Note { 5 | 6 | constructor() { 7 | this.notes = []; 8 | this.categories = []; 9 | 10 | this.updatingCategoryID = null; 11 | this._selectedCategory = null; 12 | this._selectedNote = null; 13 | 14 | this.loadLocalData(); 15 | } 16 | 17 | loadLocalData() { 18 | const localCats = localStorage.getItem(LOCAL_CATS_KEY); 19 | if (localCats && this._isValidJsonString(localCats)) { 20 | const cats = JSON.parse(localCats); 21 | if (Array.isArray(cats)) 22 | this.categories = cats.map(cat => new CategoryItem(cat)); 23 | } 24 | const localNotes = localStorage.getItem(LOCAL_NOTES_KEY); 25 | if (localNotes && this._isValidJsonString(localNotes)) { 26 | const notes = JSON.parse(localNotes); 27 | if (Array.isArray(notes)) 28 | this.notes = notes.map(note => new NoteItem(note)); 29 | } 30 | } 31 | 32 | /** 33 | * Save data to local storage 34 | */ 35 | saveData() { 36 | const cats = JSON.stringify(this.categories.map(cat => cat.data)); 37 | const notes = JSON.stringify(this.notes.map(note => note.data)); 38 | 39 | localStorage.setItem(LOCAL_CATS_KEY, cats); 40 | localStorage.setItem(LOCAL_NOTES_KEY, notes); 41 | } 42 | 43 | _isValidJsonString(JSONString) { 44 | try { 45 | JSON.parse(JSONString); 46 | return true 47 | } catch (e) { 48 | return false 49 | } 50 | } 51 | 52 | addNote(note) { 53 | if (!(note instanceof NoteItem)) 54 | throw new Error(`Expecting NoteItem instance but received ${typeof note}`); 55 | 56 | this.notes.push(note); 57 | this.selectedNote = note; 58 | // save changes to local storage 59 | this.saveData(); 60 | } 61 | 62 | updateNote(details = {}) { 63 | if (!this.selectedNote) 64 | return; 65 | 66 | this.notes = this.notes.map(note => { 67 | if (note.data.id === parseInt(this.selectedNote.data.id)) { 68 | note.data = { 69 | ...details, 70 | created_at: this.selectedNote.data.created_at, 71 | id: this.selectedNote.data.id 72 | }; 73 | note.update(details); 74 | } 75 | return note; 76 | }); 77 | 78 | // save changes to local storage 79 | this.saveData(); 80 | } 81 | 82 | removeNote(id) { 83 | this.notes = this.notes.filter(note => { 84 | if (note.data.id === parseInt(id)) 85 | note.removeElement(); 86 | 87 | return note.data.id !== parseInt(id) 88 | }); 89 | // save changes to local storage 90 | this.saveData(); 91 | } 92 | 93 | filterNotes(trend) { 94 | let result = this.notes 95 | .filter(note => note.data.title.toLowerCase().indexOf(trend.toLowerCase()) > -1 96 | || note.data.content.toLowerCase().indexOf(trend.toLowerCase()) > -1); 97 | 98 | // filter by selected category 99 | if (this.selectedCategory) 100 | result = result.filter(note => note.data.category === this.selectedCategory.data.id); 101 | 102 | return result; 103 | } 104 | 105 | addCategory(category) { 106 | if (!(category instanceof CategoryItem)) 107 | throw new Error(`Expecting CategoryElement instance but received ${typeof category}`); 108 | 109 | this.categories.push(category); 110 | // save changes to local storage 111 | this.saveData(); 112 | } 113 | 114 | updateCategory(title) { 115 | if (!this.updatingCategoryID) 116 | return; 117 | 118 | this.categories = this.categories.map(cat => { 119 | if (cat.data.id === parseInt(this.updatingCategoryID)) { 120 | cat.data.title = title; 121 | cat.update(title) 122 | } 123 | return cat; 124 | }); 125 | 126 | this.updatingCategoryID = null; 127 | // save changes to local storage 128 | this.saveData(); 129 | } 130 | 131 | getCategoryByID(id) { 132 | return this.categories.find(cat => cat.data.id === parseInt(id)) 133 | } 134 | 135 | getNoteById(id) { 136 | return this.notes.find(cat => cat.data.id === parseInt(id)) 137 | } 138 | 139 | removeCategory(id) { 140 | this.categories = this.categories.filter(cat => { 141 | if (cat.data.id === parseInt(id)) { 142 | // remove selectedCategory too 143 | if (this.selectedCategory && this.selectedCategory.data.id === parseInt(id)) 144 | this.selectedCategory = null; 145 | 146 | cat.removeElement(); 147 | } 148 | 149 | return cat.data.id !== parseInt(id) 150 | }); 151 | 152 | // save changes to local storage 153 | this.saveData(); 154 | } 155 | 156 | findCategory(id) { 157 | return this.categories.find(cat => cat.data.id === parseInt(id)) 158 | } 159 | 160 | get selectedCategory() { 161 | return this._selectedCategory; 162 | } 163 | 164 | set selectedCategory(category) { 165 | 166 | if (!category) { 167 | if (this._selectedCategory) 168 | this._selectedCategory.deselect(); 169 | this._selectedCategory = null; 170 | return; 171 | } 172 | 173 | if (!(category instanceof CategoryItem)) 174 | throw new Error(`Expecting CategoryElement instance but received ${typeof category}`); 175 | 176 | // call select method of CategoryItem 177 | // to set its checked status as true manually 178 | category.select(); 179 | this._selectedCategory = category; 180 | } 181 | 182 | get selectedNote() { 183 | return this._selectedNote; 184 | } 185 | 186 | set selectedNote(note) { 187 | 188 | // clear note selection 189 | if (!note) { 190 | if (this._selectedNote) 191 | this._selectedNote.deselect(); 192 | this._selectedNote = null; 193 | return 194 | } 195 | 196 | if (!(note instanceof NoteItem)) 197 | throw new Error(`Expecting NoteElement instance but received ${typeof note}`); 198 | 199 | // call select method of NoteItem 200 | // to set its checked status as true manually 201 | note.select(); 202 | this._selectedNote = note; 203 | 204 | // update selected category according to the selected note 205 | if (note.data.category) { 206 | const noteCategory = this.findCategory(note.data.category); 207 | if (noteCategory) { 208 | this.selectedCategory = noteCategory 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /03.notes-app/static/notes-app-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/03.notes-app/static/notes-app-ss.jpg -------------------------------------------------------------------------------- /03.notes-app/styles/styles.css: -------------------------------------------------------------------------------- 1 | /** Simple Reset - START */ 2 | html { 3 | box-sizing: border-box; 4 | font-size: 16px; 5 | overflow-x: hidden; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, h1, h2, h3, h4, h5, h6, p, ol, ul { 13 | margin: 0; 14 | padding: 0; 15 | font-weight: normal; 16 | } 17 | 18 | ol, ul { 19 | list-style: none; 20 | } 21 | 22 | img { 23 | max-width: 100%; 24 | height: auto; 25 | } 26 | 27 | /** Global - START */ 28 | 29 | body { 30 | font-family: 'Lato', sans-serif; 31 | } 32 | 33 | #note-app { 34 | position: absolute; 35 | width: 100%; 36 | height: 100%; 37 | background: #fbfbf9; 38 | display: flex; 39 | flex-direction: column; 40 | } 41 | 42 | #note-app main { 43 | position: relative; 44 | 45 | flex-grow: 1; 46 | display: grid; 47 | grid-template-columns: 200px 250px auto; 48 | grid-template-rows: calc(100vh - 3em); 49 | grid-template-areas: "categories notes editor"; 50 | } 51 | 52 | .custom-radio { 53 | position: relative; 54 | } 55 | 56 | .custom-radio:hover { 57 | cursor: pointer; 58 | } 59 | 60 | .custom-radio input[type="radio"] { 61 | display: none; 62 | } 63 | 64 | .custom-radio label { 65 | cursor: pointer; 66 | display: block; 67 | padding: .5em 2.5em .5em .75em; 68 | } 69 | 70 | .custom-radio:hover label { 71 | background-color: rgba(239, 187, 0, 0.08); 72 | } 73 | 74 | .custom-radio input[type="radio"]:checked ~ label { 75 | background-color: #f5eea9; 76 | } 77 | 78 | .input-field { 79 | background-color: transparent; 80 | display: flex; 81 | justify-content: flex-start; 82 | align-items: center; 83 | } 84 | 85 | .input-field input { 86 | background-color: transparent; 87 | outline: none; 88 | box-shadow: none; 89 | padding: .4em; 90 | border: 1px solid #d0d0d0; 91 | border-radius: .3em; 92 | max-width: 100%; 93 | } 94 | 95 | .input-field input:focus { 96 | background-color: #e2e0e0; 97 | } 98 | 99 | .icon-button { 100 | background-color: #efefef; 101 | width: 1.75em; 102 | border-radius: .3em; 103 | outline: none; 104 | cursor: pointer; 105 | display: inline-flex; 106 | padding: .2em; 107 | box-shadow: none; 108 | border: 1px solid #d0d0d0; 109 | } 110 | 111 | .icon-button > svg { 112 | pointer-events: none; 113 | stroke: #949494; 114 | width: 100%; 115 | stroke-width: 1.7; 116 | } 117 | 118 | .icon-button:hover { 119 | background-color: #e0e0e0; 120 | } 121 | 122 | .content-placeholder { 123 | font-size: 16px; 124 | color: #aaa; 125 | } 126 | 127 | /** Toolbox - START */ 128 | .toolbox { 129 | height: 3em; 130 | display: flex; 131 | justify-content: space-between; 132 | padding: .5rem 1rem; 133 | border-bottom: 2px solid #d0d0d0; 134 | background: #f5f5f5; 135 | } 136 | 137 | #add-category-form { 138 | display: flex; 139 | margin-right: 1.7em; 140 | } 141 | 142 | #add-category-form .input-field { 143 | max-width: 140px; 144 | } 145 | 146 | #add-category-form button { 147 | width: 2em; 148 | padding: 0 .3em; 149 | margin-left: .4em; 150 | } 151 | 152 | .toolbox__btn { 153 | width: 26px; 154 | margin-left: .5rem; 155 | } 156 | 157 | /** Categories List - START */ 158 | #categories-list { 159 | grid-area: categories; 160 | overflow-y: auto; 161 | border-right: 1px solid #e0e0e0; 162 | padding: .25em; 163 | } 164 | 165 | .category-item { 166 | border-bottom: 1px solid #dfdfdf; 167 | } 168 | 169 | .category-item .icon-button { 170 | position: absolute; 171 | top: 50%; 172 | right: .5em; 173 | transform: translateY(-50%); 174 | visibility: hidden; 175 | opacity: 0; 176 | transition: all .1s ease-in; 177 | } 178 | 179 | .category-item .icon-button:nth-of-type(2) { 180 | right: 2.5em; 181 | } 182 | 183 | .category-item:hover .icon-button { 184 | visibility: visible; 185 | opacity: 1; 186 | } 187 | 188 | /** Notes List - START */ 189 | #notes-list { 190 | grid-area: notes; 191 | overflow-y: auto; 192 | border-right: 1px solid #e0e0e0; 193 | padding: .25em; 194 | } 195 | 196 | .note-item { 197 | border-bottom: 1px solid #dfdfdf; 198 | } 199 | 200 | .note-item .icon-button { 201 | position: absolute; 202 | top: 50%; 203 | right: .5em; 204 | transform: translateY(-50%); 205 | visibility: hidden; 206 | opacity: 0; 207 | transition: all .1s ease-in; 208 | } 209 | 210 | .note-item:hover .icon-button { 211 | visibility: visible; 212 | opacity: 1; 213 | } 214 | 215 | .note-item__title { 216 | display: block; 217 | margin-bottom: .3em; 218 | } 219 | 220 | .note-item__subtitle { 221 | display: block; 222 | color: #888; 223 | font-size: 14px; 224 | } 225 | 226 | .note-item__datetime { 227 | display: block; 228 | color: #888; 229 | font-size: 12px; 230 | margin-top: .3em; 231 | } 232 | 233 | /** Editor - START */ 234 | #note-editor { 235 | grid-area: editor; 236 | } 237 | 238 | #note-add-edit-form { 239 | display: flex; 240 | flex-direction: column; 241 | height: 100%; 242 | } 243 | 244 | #note-add-edit-form input, 245 | #note-add-edit-form textarea { 246 | border: none; 247 | border-bottom: 1px solid #e0e0e0; 248 | background-color: transparent; 249 | padding: 1em; 250 | box-shadow: none; 251 | outline: none; 252 | } 253 | 254 | #note-add-edit-form input { 255 | font-size: 16px; 256 | font-family: 'Lato', sans-serif; 257 | } 258 | 259 | #note-add-edit-form textarea { 260 | flex-grow: 1; 261 | min-width: 100%; 262 | max-width: 100%; 263 | font-size: 16px; 264 | line-height: 24px; 265 | font-family: 'Lato', sans-serif; 266 | } 267 | 268 | #note-add-edit-form textarea:focus, 269 | #note-add-edit-form input:focus { 270 | background-color: #f5f5f5; 271 | } 272 | 273 | #note-add-edit-form button { 274 | background: #e0e0e0; 275 | box-shadow: none; 276 | border: none; 277 | padding: 1em; 278 | border-radius: .3em; 279 | cursor: pointer; 280 | } 281 | 282 | #note-add-edit-form button:hover { 283 | background-color: #d5d5d5; 284 | } 285 | 286 | 287 | /** Alert Box - START */ 288 | .alert-box { 289 | position: fixed; 290 | width: 100%; 291 | height: 100%; 292 | z-index: 5; 293 | background: rgba(0, 0, 0, 0.5); 294 | display: flex; 295 | justify-content: center; 296 | align-items: center; 297 | padding: 1em; 298 | visibility: hidden; 299 | opacity: 0; 300 | transition: all .1s ease-in; 301 | } 302 | 303 | .alert-box.visible { 304 | visibility: visible; 305 | opacity: 1; 306 | } 307 | 308 | .alert-box__inner { 309 | padding: 1em; 310 | background: #fff; 311 | border-radius: .5em; 312 | width: 100%; 313 | max-width: 360px; 314 | } 315 | 316 | .alert-box__header { 317 | margin-bottom: 1rem; 318 | font-size: 16px; 319 | font-weight: bold; 320 | text-align: left; 321 | } 322 | 323 | .alert-box__message { 324 | margin: 1rem 0; 325 | text-align: left; 326 | font-size: 16px; 327 | color: #888; 328 | } 329 | 330 | .alert-box__ok { 331 | float: right; 332 | background: #e0e0e0; 333 | box-shadow: none; 334 | border: none; 335 | padding: .5em 1em; 336 | border-radius: .3em; 337 | cursor: pointer; 338 | } 339 | 340 | .alert-box__ok:hover { 341 | background-color: #d5d5d5; 342 | } 343 | -------------------------------------------------------------------------------- /04.othello-board-game/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Othello Board Game 7 | 8 | 9 |
10 |
11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /04.othello-board-game/scripts/cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Cell class representing a cell of game board 5 | * 6 | * @class 7 | */ 8 | class Cell { 9 | 10 | /** 11 | * Cell creator 12 | * 13 | * @param {number} x - Coordinate on X axis of board 14 | * @param {number} y - Coordinate on Y axis of board 15 | * @param {string|null} owner - Specify the owner on cell (black/white) 16 | */ 17 | constructor(x, y, owner = null) { 18 | this.x = x; 19 | this.y = y; 20 | this._owner = owner; 21 | } 22 | 23 | /** 24 | * return coordinate of cell as [x,y] 25 | * 26 | * @returns {number[]} - First index is X and second is Y 27 | */ 28 | pos() { 29 | return [this.x, this.y] 30 | } 31 | 32 | /** 33 | * Getter for cell owner 34 | * @returns {string} 35 | */ 36 | get owner() { 37 | return this._owner; 38 | } 39 | 40 | /** 41 | * Setter for cell owner 42 | * @param {string} owner - Value of cell owner (black/white) 43 | */ 44 | set owner(owner) { 45 | this._owner = owner; 46 | } 47 | 48 | /** 49 | * Possible around neighbors of a cell will return as coordinates 50 | * 51 | * @returns {number[]} 52 | */ 53 | neighbors() { 54 | return [ 55 | [this.x - 1, this.y - 1], [this.x - 1, this.y], [this.x - 1, this.y + 1], 56 | [this.x, this.y - 1], [this.x, this.y + 1], 57 | [this.x + 1, this.y - 1], [this.x + 1, this.y], [this.x + 1, this.y + 1], 58 | ]; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /04.othello-board-game/scripts/index.js: -------------------------------------------------------------------------------- 1 | class OthelloGame { 2 | 3 | defaultOptions = { 4 | width: 8, 5 | cellMaxWidth: 80, 6 | selector: "othello", 7 | cellBGColor: "#43a047", 8 | }; 9 | 10 | constructor(options = {}) { 11 | if (options && typeof options === "object") 12 | this.options = {...this.defaultOptions, ...options}; 13 | 14 | this.board = new Board(this.options.width); 15 | 16 | this.initGame(); 17 | } 18 | 19 | initGame() { 20 | const {selector = "othella"} = this.options; 21 | 22 | this.elm = document.getElementById(selector); 23 | 24 | if (!this.elm) 25 | throw new Error(`Element with id ${selector} not found.`); 26 | 27 | for (let y = 0; y < this.options.width; y++) { 28 | const boardRowElm = this.createRowElm(y); 29 | for (let x = 0; x < this.options.width; x++) { 30 | 31 | const cell = this.board.getCell(x, y); 32 | 33 | const boardCellElm = this.createCellElm(x, y); 34 | boardCellElm.appendChild(this.createNutElm(cell.owner, this.board.isValidMove(x, y), this.board.turn)); 35 | 36 | boardRowElm.appendChild(boardCellElm) 37 | } 38 | 39 | this.elm.appendChild(boardRowElm) 40 | } 41 | 42 | } 43 | 44 | reRender() { 45 | 46 | for (let y = 0; y < this.options.width; y++) { 47 | for (let x = 0; x < this.options.width; x++) { 48 | 49 | const cell = this.board.getCell(x, y); 50 | 51 | const boardCellElm = document.getElementById(`og-board-cell-${x}-${y}`); 52 | boardCellElm.innerHTML = ""; 53 | boardCellElm.appendChild(this.createNutElm(cell.owner, this.board.isValidMove(x, y), this.board.turn)); 54 | 55 | } 56 | } 57 | 58 | } 59 | 60 | createNutElm(type, validMove = false, turn) { 61 | const nut = document.createElement("span"); 62 | nut.style.backgroundColor = type; 63 | nut.style.display = "inline-block"; 64 | nut.style.width = `${Math.floor(this.options.cellMaxWidth * .8)}px`; 65 | nut.style.height = `${Math.floor(this.options.cellMaxWidth * .8)}px`; 66 | nut.style.borderRadius = '100%'; 67 | 68 | if (validMove) { 69 | nut.style.border = `1px dashed #${turn === "white" ? "eee" : "444"}`; 70 | } 71 | 72 | return nut; 73 | } 74 | 75 | createRowElm(y) { 76 | const elm = document.createElement("div"); 77 | elm.style.display = "flex"; 78 | elm.setAttribute("class", `og-board-row row-${y}`); 79 | elm.setAttribute("id", `og-board-row-${y}`); 80 | 81 | return elm; 82 | } 83 | 84 | handleCellClick(x, y) { 85 | return () => { 86 | 87 | if (this.board.finished) { 88 | const result = this.board.gameResult(); 89 | document.getElementById("result") 90 | .innerHTML = `

Winner is ${result.winner} with ${result[result.winner]} nuts.

`; 91 | return; 92 | } 93 | 94 | this.board.placeNutTo(x, y); 95 | this.reRender(); 96 | } 97 | } 98 | 99 | createCellElm(x, y) { 100 | const elm = document.createElement("div"); 101 | elm.setAttribute("class", `og-cell cell-${x}-${y}`); 102 | elm.setAttribute("id", `og-board-cell-${x}-${y}`); 103 | 104 | elm.style.backgroundColor = this.options.cellBGColor; 105 | elm.style.border = "1px solid #d5d5d5"; 106 | elm.style.width = `${this.options.cellMaxWidth}px`; 107 | elm.style.height = `${this.options.cellMaxWidth}px`; 108 | elm.style.display = "flex"; 109 | elm.style.justifyContent = "center"; 110 | elm.style.alignItems = "center"; 111 | 112 | elm.onclick = this.handleCellClick(x, y); 113 | return elm; 114 | } 115 | 116 | } 117 | 118 | const OG = new OthelloGame(); 119 | -------------------------------------------------------------------------------- /04.othello-board-game/static/othello-board-game-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/04.othello-board-game/static/othello-board-game-ss.jpg -------------------------------------------------------------------------------- /05.quiz-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple Quiz 9 | 10 | 11 |
12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 |

23 |
24 | Question Count:-- 25 |
26 |
27 | Time:-- 28 |
29 | 30 |
31 | 34 |
35 |
36 | 37 |
38 |
39 | Question 0/0 40 | 00:00 41 |
42 |
43 |

44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 54 |
55 |
56 | 57 | 59 |
60 |
61 | 62 | 64 |
65 |
66 | 67 |
68 | 71 | 74 |
75 |
76 | 77 |
78 |

0

79 | Your Score 80 | 81 |
82 | 83 |
84 |
85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /05.quiz-app/scripts/elements-helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class QuizElementsHelper { 4 | /** 5 | * 6 | * @param app {Element} - the element of the whole app 7 | * @param quizCard {Element} - the wrapper of the quiz details 8 | * @param questionCard {Element} - the wrapper of the questions card 9 | * @param resultCard {Element} - the wrapper of the result card 10 | * @param quiz {Quiz} - an instance of the Quiz class 11 | */ 12 | constructor(app, quizCard, questionCard, resultCard, quiz) { 13 | this.app = app; 14 | this.quiz = quiz; 15 | this.quizCard = quizCard; 16 | this.questionCard = questionCard; 17 | this.resultCard = resultCard; 18 | 19 | // find & assign elements 20 | this.assignElements(); 21 | 22 | // initialize the listeners 23 | this.initListeners(); 24 | 25 | // show quiz details card 26 | this.showQuizCard(); 27 | } 28 | 29 | /** 30 | * find the inner elements of each card and assign to it. 31 | */ 32 | assignElements() { 33 | // Quiz Card Elements 34 | this.quizCard.startBtn = this.quizCard.querySelector( 35 | ".quiz-details__start-btn" 36 | ); 37 | this.quizCard.titleElm = this.quizCard.querySelector( 38 | ".quiz-details__title" 39 | ); 40 | this.quizCard.descriptionElm = this.quizCard.querySelector( 41 | ".quiz-details__description" 42 | ); 43 | this.quizCard.metaQCElm = this.quizCard.querySelector( 44 | ".quiz-details__meta.--qc strong" 45 | ); 46 | this.quizCard.metaTimeElm = this.quizCard.querySelector( 47 | ".quiz-details__meta.--t strong" 48 | ); 49 | 50 | // Question Card Elements 51 | this.questionCard.progressRemainingTimeElm = document.querySelector( 52 | ".questions-card__remaining-time" 53 | ); 54 | this.questionCard.progressQuestionCountElm = document.querySelector( 55 | ".questions-card__q-count" 56 | ); 57 | this.questionCard.progressbarElm = document.querySelector( 58 | ".questions-card__progress .--value" 59 | ); 60 | this.questionCard.questionTitleElm = document.getElementById( 61 | "question-title" 62 | ); 63 | this.questionCard.optionOneElm = document.querySelector( 64 | "#option-one ~ label" 65 | ); 66 | this.questionCard.optionTwoElm = document.querySelector( 67 | "#option-two ~ label" 68 | ); 69 | this.questionCard.optionThreeElm = document.querySelector( 70 | "#option-three ~ label" 71 | ); 72 | this.questionCard.optionFourElm = document.querySelector( 73 | "#option-four ~ label" 74 | ); 75 | this.questionCard.nextBtn = this.app.querySelector("#next-btn"); 76 | this.questionCard.stopBtn = this.app.querySelector("#stop-btn"); 77 | 78 | // Result Card Elements 79 | this.resultCard.gotoHome = this.resultCard.querySelector("#go-to-home"); 80 | this.resultCard.scoreElm = this.resultCard.querySelector("#score"); 81 | } 82 | 83 | /** 84 | * initialize the required listeners of the elements 85 | */ 86 | initListeners() { 87 | this.quizCard.startBtn.addEventListener( 88 | "click", 89 | this.showQuestionsCard.bind(this) 90 | ); 91 | this.questionCard.nextBtn.addEventListener( 92 | "click", 93 | this.nextBtnHandler.bind(this) 94 | ); 95 | this.questionCard.stopBtn.addEventListener( 96 | "click", 97 | this.stopBtnHandler.bind(this) 98 | ); 99 | this.resultCard.gotoHome.addEventListener( 100 | "click", 101 | this.hideResultCard.bind(this) 102 | ); 103 | } 104 | 105 | /** 106 | * Show the details card of the quiz 107 | */ 108 | showQuizCard() { 109 | this.quizCard.titleElm.innerText = this.quiz.title; 110 | this.quizCard.descriptionElm.innerText = this.quiz.description; 111 | this.quizCard.metaQCElm.innerText = this.quiz._questions.length; 112 | this.quizCard.metaTimeElm.innerText = this.quiz._time; 113 | 114 | this.quizCard.classList.add("show"); 115 | } 116 | 117 | /** 118 | * hide the quiz card 119 | */ 120 | hideQuizCard() { 121 | this.quizCard.classList.remove("show"); 122 | } 123 | 124 | /** 125 | * Show the question card 126 | */ 127 | showQuestionsCard() { 128 | this.hideQuizCard(); 129 | 130 | this.questionCard.classList.add("show"); 131 | this.questionCard.classList.remove("time-over"); 132 | 133 | this.startQuiz(); 134 | } 135 | 136 | /** 137 | * hide the question card 138 | */ 139 | hideQuestionsCard() { 140 | this.questionCard.classList.remove("show"); 141 | } 142 | 143 | /** 144 | * Handle the visibility of the result card 145 | * @param result - the object of quiz result thet contains score property 146 | */ 147 | showResultCard(result) { 148 | this.hideQuestionsCard(); 149 | 150 | if (this.resultCard.scoreElm && result) 151 | this.resultCard.scoreElm.innerText = Math.floor(result.score * 10) / 10; 152 | 153 | this.resultCard.classList.add("show"); 154 | } 155 | 156 | /** 157 | * hide the result card 158 | */ 159 | hideResultCard() { 160 | this.resultCard.classList.remove("show"); 161 | this.showQuizCard(); 162 | } 163 | 164 | /** 165 | * Handle the starting of the quiz and control the status of it. 166 | */ 167 | startQuiz() { 168 | this.resetPreviousQuiz(); 169 | this.quiz.reset(); 170 | const firstQuestion = this.quiz.start(); 171 | if (firstQuestion) { 172 | this.parseNextQuestion(firstQuestion); 173 | } 174 | 175 | this.questionCard.nextBtn.innerText = "Next"; 176 | 177 | this._setProgressTicker(); 178 | } 179 | 180 | /** 181 | * initialize the quiz time progress on every time that quiz starts 182 | * to control the progressbar and remaining time 183 | * @private 184 | */ 185 | _setProgressTicker() { 186 | this.remainingTimeInterval = setInterval(() => { 187 | const qTime = this.quiz.timeDetails; 188 | if (qTime && qTime.remainingTime) { 189 | // update remaining time span 190 | this.questionCard.progressRemainingTimeElm.innerText = 191 | qTime.remainingTime; 192 | 193 | // update progressbar 194 | let progressPercent = 195 | ((qTime.quizTime - qTime.elapsedTime) * 100) / qTime.quizTime; 196 | if (progressPercent < 0) progressPercent = 0; 197 | this.questionCard.progressbarElm.style.width = progressPercent + "%"; 198 | } 199 | 200 | // clear & stop interval when time over 201 | if (qTime.timeOver) { 202 | this.questionCard.classList.add("time-over"); 203 | this.questionCard.nextBtn.innerText = "Show Result"; 204 | clearInterval(this.remainingTimeInterval); 205 | } 206 | }, 1000); 207 | } 208 | 209 | /** 210 | * this method putting the question in the question card 211 | * @param question - the object of the question that received from this.quiz 212 | */ 213 | parseNextQuestion(question) { 214 | const selectedOption = document.querySelector( 215 | "input[name=question-option]:checked" 216 | ); 217 | 218 | this.questionCard.progressQuestionCountElm.innerText = `Question ${this.quiz 219 | ._currentQuestionIndex + 1}/${this.quiz._questions.length}`; 220 | this.questionCard.questionTitleElm.setAttribute( 221 | "data-qn", 222 | `Q ${this.quiz._currentQuestionIndex + 1}:` 223 | ); 224 | this.questionCard.questionTitleElm.innerText = question.title; 225 | 226 | this.questionCard.optionOneElm.innerText = question.options[0]; 227 | this.questionCard.optionTwoElm.innerText = question.options[1]; 228 | this.questionCard.optionThreeElm.innerText = question.options[2]; 229 | this.questionCard.optionFourElm.innerText = question.options[3]; 230 | 231 | // reset pre selected options on every next 232 | if (selectedOption) selectedOption.checked = false; 233 | } 234 | 235 | /** 236 | * To reset the previous quiz status before restarting it. 237 | */ 238 | resetPreviousQuiz() { 239 | this.quiz.stop(); 240 | clearInterval(this.remainingTimeInterval); 241 | 242 | this.resultCard.scoreElm.innerText = 0; 243 | this.questionCard.progressRemainingTimeElm.innerText = "00:00"; 244 | this.questionCard.progressbarElm.style.width = "100%"; 245 | } 246 | 247 | /** 248 | * this will call when next button clicked 249 | */ 250 | nextBtnHandler() { 251 | const selectedOption = document.querySelector( 252 | "input[name=question-option]:checked" 253 | ); 254 | 255 | let result; 256 | if (!selectedOption) { 257 | result = this.quiz.skipCurrentQuestion(); 258 | } else { 259 | result = this.quiz.answerCurrentQuestion(selectedOption.value); 260 | } 261 | 262 | if (result.finished || result.timeOver) { 263 | this.showResultCard(result.result); 264 | } else if (result) { 265 | this.parseNextQuestion(result.nextQ); 266 | } 267 | } 268 | 269 | /** 270 | * this will call when stop button clicked 271 | */ 272 | stopBtnHandler() { 273 | this.resetPreviousQuiz(); 274 | this.showResultCard(); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /05.quiz-app/scripts/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const app = document.getElementById("quiz-app"); 4 | const quizCard = document.getElementById("quiz-details"); 5 | const questionsCard = document.getElementById("questions-card"); 6 | const resultCard = document.getElementById("result-card"); 7 | 8 | let quiz; 9 | 10 | function initApp() { 11 | const questions = [ 12 | { 13 | title: "Which one is the type of a javascript file?", 14 | options: [".ts", ".js", ".jsx", ".j"] 15 | }, { 16 | title: "Inside which HTML element do we put the JavaScript?", 17 | options: ["", " 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /06.simple-range-slider/scripts/index.js: -------------------------------------------------------------------------------- 1 | const sr = new SimpleRangeSlider(document.getElementById("simple-range"), { 2 | min: 0, 3 | max: 10, 4 | mode: "horizontal", 5 | size: "200px", 6 | defaultValue: 5, 7 | pathDiameter: "10px", 8 | handlerSize: "8px", 9 | pathColor: "#ddd", 10 | progressColor: "#1c70ff", 11 | loadingProgressColor: "#ccc", 12 | lockOnLoadingValue: false, 13 | }); 14 | 15 | sr.on("start", (value) => { 16 | // console.log("started", value); 17 | }); 18 | 19 | sr.on("dragging", (event, value) => { 20 | // console.log("dragging", event, value); 21 | }); 22 | 23 | sr.on("change", (value) => { 24 | // console.log("changed", value); 25 | }); 26 | 27 | sr.on("stop", (value) => { 28 | // console.log("stopped", value); 29 | }); 30 | 31 | sr.on("loadingChange", (value) => { 32 | // console.log("loadingChange", value); 33 | }); 34 | 35 | 36 | const srVertical = new SimpleRangeSlider(document.getElementById("simple-range-vertical"), { 37 | min: 0, 38 | max: 10, 39 | mode: "vertical", 40 | size: "200px", 41 | defaultValue: 5, 42 | pathDiameter: "10px", 43 | handlerSize: "8px", 44 | pathColor: "#ddd", 45 | progressColor: "#1c70ff", 46 | loadingProgressColor: "#ccc", 47 | lockOnLoadingValue: false, 48 | }); 49 | -------------------------------------------------------------------------------- /06.simple-range-slider/scripts/simple-range-slider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function SimpleRangeSlider(wrapper, options) { 4 | 5 | if (!wrapper || !(wrapper instanceof Element || wrapper instanceof HTMLDocument)) 6 | throw new Error("Wrapper must be a valid node."); 7 | 8 | this.defaultOptions = { 9 | min: 0, 10 | max: 100, 11 | mode: "horizontal", 12 | size: '100%', 13 | defaultValue: 50, 14 | pathDiameter: "8px", 15 | handlerSize: "20px", 16 | pathColor: "#ddd", 17 | progressColor: "#1c70ff", 18 | loadingProgressColor: "#ccc", 19 | lockOnLoadingValue: false 20 | }; 21 | 22 | this.options = Object.assign(this.defaultOptions, options || {}); 23 | this.wrapper = wrapper; 24 | 25 | this.init(); 26 | } 27 | 28 | // define value property 29 | Object.defineProperty(SimpleRangeSlider.prototype, "value", { 30 | get: function () { 31 | return this._value; 32 | }, 33 | set: function (newValue) { 34 | 35 | // check to lock progress on loading value 36 | if (this.options.lockOnLoadingValue) { 37 | if (newValue >= this.loadingValue) { 38 | newValue = this.loadingValue; 39 | } 40 | } 41 | 42 | // convert to percent with two floating point 43 | const valueAsPercent = Math.floor(Math.max(0, Math.min(100, newValue * 100 / this.options.max)) * 100) / 100; 44 | 45 | if (this.isHorizontalMode) { 46 | this.progress.style.height = "100%"; 47 | this.progress.style.width = valueAsPercent + "%"; 48 | this.handler.style.left = valueAsPercent + "%"; 49 | } else if (this.isVerticalMode) { 50 | this.progress.style.width = "100%"; 51 | this.progress.style.height = valueAsPercent + "%"; 52 | this.handler.style.bottom = valueAsPercent + "%"; 53 | } 54 | 55 | this._value = newValue; 56 | 57 | // trigger value change event 58 | this.events.call("change", this._value); 59 | } 60 | }); 61 | 62 | // define loading value property 63 | Object.defineProperty(SimpleRangeSlider.prototype, "loadingValue", { 64 | get: function () { 65 | return this._loadingValue; 66 | }, 67 | set: function (newValue) { 68 | // convert to percent with two floating point 69 | const valueAsPercent = Math.floor(Math.max(0, Math.min(100, newValue * 100 / this.options.max)) * 100) / 100; 70 | 71 | if (this.isHorizontalMode) { 72 | this.loadingProgress.style.height = "100%"; 73 | this.loadingProgress.style.width = valueAsPercent + "%"; 74 | } else if (this.isVerticalMode) { 75 | this.loadingProgress.style.width = "100%"; 76 | this.loadingProgress.style.height = valueAsPercent + "%"; 77 | } 78 | 79 | this._loadingValue = newValue; 80 | 81 | // trigger loading value change event 82 | this.events.call("loadingChange", this._loadingValue); 83 | } 84 | }); 85 | 86 | SimpleRangeSlider.prototype.init = function () { 87 | 88 | if (this.slider) 89 | return this; 90 | 91 | this.initialized = false; 92 | this.events = { 93 | call: function (event) { 94 | if (event && typeof event === "string" && typeof this.events[event] === "function") { 95 | const args = Array.prototype.slice.call(arguments).slice(1); 96 | this.events[event].apply(this, args); 97 | } 98 | }.bind(this), 99 | }; 100 | this.bound = { 101 | width: void 0, 102 | height: void 0, 103 | leftPos: void 0, 104 | topPos: void 0 105 | }; 106 | this._value = 0; 107 | this._loadingValue = 0; 108 | 109 | // create DOM elements 110 | this.slider = document.createElement("div"); 111 | this.path = document.createElement("div"); 112 | this.progress = document.createElement("div"); 113 | this.loadingProgress = document.createElement("div"); 114 | this.handler = document.createElement("div"); 115 | 116 | this.slider.classList.add("simple-slider"); 117 | this.path.classList.add("simple-slider__path"); 118 | this.progress.classList.add("simple-slider__progress"); 119 | this.loadingProgress.classList.add("simple-slider__loading-progress"); 120 | this.handler.classList.add("simple-slider__handler"); 121 | 122 | this.path.appendChild(this.loadingProgress); 123 | this.path.appendChild(this.progress); 124 | this.slider.appendChild(this.path); 125 | this.slider.appendChild(this.handler); 126 | 127 | this.wrapper.innerHTML = ''; 128 | this.wrapper.appendChild(this.slider); 129 | 130 | // apply options on slider 131 | this.applyOptions(); 132 | 133 | this.initListeners(); 134 | }; 135 | 136 | SimpleRangeSlider.prototype.initListeners = function () { 137 | if (!this.slider) 138 | return; 139 | 140 | if (this.initialized) 141 | return this; 142 | 143 | let dragging = false; 144 | 145 | const findPositionFromEvent = function (event) { 146 | let xPos = event.pageX; 147 | if (xPos && event.touches) 148 | xPos = event.touches[0].pageX; 149 | let yPos = event.pageY; 150 | if (yPos && event.touches) 151 | yPos = event.touches[0].pageY; 152 | 153 | if (this.isHorizontalMode) { 154 | setHorizontalSliderValue(xPos); 155 | } else if (this.isVerticalMode) { 156 | setVerticalSliderValue(yPos); 157 | } 158 | }.bind(this); 159 | 160 | 161 | const setHorizontalSliderValue = function (xPos) { 162 | const changeGrade = Math.min(1, Math.max(0, (xPos - this.bound.leftPos) / this.bound.width)); 163 | this.value = ((1 - changeGrade) * this.options.min) + (changeGrade * this.options.max); 164 | }.bind(this); 165 | 166 | const setVerticalSliderValue = function (yPos) { 167 | const changeGrade = Math.min(1, Math.max(0, 1 - ((yPos - this.bound.topPos) / this.bound.height))); 168 | this.value = ((1 - changeGrade) * this.options.min) + (changeGrade * this.options.max); 169 | 170 | }.bind(this); 171 | 172 | let draggingStart = function (e) { 173 | e.preventDefault(); 174 | 175 | dragging = true; 176 | 177 | // trigger drag start event 178 | this.events.call("start", this.value); 179 | 180 | let scrollLeft = window.pageXOffset; 181 | let scrollTop = window.pageYOffset; 182 | 183 | const rect = this.slider.getBoundingClientRect(); 184 | this.bound = { 185 | width: rect.width, 186 | height: rect.height, 187 | leftPos: rect.left + scrollLeft, 188 | topPos: rect.top + scrollTop 189 | }; 190 | 191 | this.slider.classList.add("--dragging"); 192 | 193 | findPositionFromEvent(e); 194 | 195 | document.addEventListener("mousemove", onDragging); 196 | document.addEventListener("touchmove", onDragging); 197 | }.bind(this); 198 | 199 | let draggingStop = function (e) { 200 | e.preventDefault(); 201 | if (!dragging) 202 | return; 203 | 204 | this.bound = { 205 | width: void 0, 206 | height: void 0, 207 | leftPos: void 0, 208 | topPos: void 0 209 | }; 210 | dragging = false; 211 | this.slider.classList.remove("--dragging"); 212 | document.removeEventListener("mousemove", onDragging); 213 | document.removeEventListener("touchmove", onDragging); 214 | 215 | // trigger stop event 216 | this.events.call("stop", this.value); 217 | }.bind(this); 218 | 219 | let onDragging = function (e) { 220 | if (!dragging) 221 | return; 222 | 223 | findPositionFromEvent(e); 224 | 225 | // trigger dragging event 226 | this.events.call("dragging", e, this.value); 227 | 228 | }.bind(this); 229 | 230 | this.slider.addEventListener("mousedown", draggingStart); 231 | this.slider.addEventListener("touchstart", draggingStart); 232 | document.addEventListener("mouseup", draggingStop); 233 | document.addEventListener("touchend", draggingStop); 234 | 235 | this.initialized = true; 236 | return this; 237 | }; 238 | 239 | SimpleRangeSlider.prototype.applyOptions = function () { 240 | 241 | let modeClass = "--" + (this.options.mode || this.defaultOptions.mode); 242 | this.slider.classList.remove("--horizontal"); 243 | this.slider.classList.remove("--vertical"); 244 | this.slider.classList.remove("--circle"); 245 | this.slider.classList.add(modeClass); 246 | 247 | this.isVerticalMode = this.options.mode === "vertical"; 248 | this.isCircleMode = this.options.mode === "circle"; 249 | this.isHorizontalMode = !this.isCircleMode && !this.isVerticalMode; 250 | 251 | if (this.options.size) { 252 | if (this.isHorizontalMode) { 253 | this.slider.style.height = "unset"; 254 | this.slider.style.width = this.options.size; 255 | } else if (this.isVerticalMode) { 256 | this.slider.style.width = "auto"; 257 | this.slider.style.height = this.options.size; 258 | } 259 | } 260 | 261 | if (this.options.pathDiameter) { 262 | this.slider.style.setProperty("--slider-path-diameter", this.options.pathDiameter); 263 | } 264 | 265 | if (this.options.handlerSize) { 266 | this.slider.style.setProperty("--slider-handler-size", this.options.handlerSize); 267 | } 268 | 269 | if (this.options.pathColor) { 270 | this.slider.style.setProperty("--slider-path-color", this.options.pathColor); 271 | } 272 | 273 | if (this.options.progressColor) { 274 | this.slider.style.setProperty("--slider-progress-color", this.options.progressColor); 275 | } 276 | 277 | if (this.options.loadingProgressColor) { 278 | this.slider.style.setProperty("--slider-loading-progress-color", this.options.loadingProgressColor); 279 | } 280 | 281 | // set default value 282 | this.value = this.options.defaultValue || 0; 283 | 284 | // set default loading value 285 | this.loadingValue = this._loadingValue || 0; 286 | 287 | }; 288 | 289 | SimpleRangeSlider.prototype.on = function (event, fn) { 290 | if (typeof event !== "string" || typeof fn !== "function") { 291 | throw "Invalid event or callback function"; 292 | } 293 | 294 | this.events[event] = fn; 295 | }; 296 | 297 | SimpleRangeSlider.prototype.update = function (options) { 298 | if (!this.slider) { 299 | return; 300 | } 301 | 302 | this.options = Object.assign(this.defaultOptions, options || {}); 303 | 304 | this.applyOptions(); 305 | 306 | return this; 307 | }; 308 | -------------------------------------------------------------------------------- /06.simple-range-slider/static/simple-range-slider-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/06.simple-range-slider/static/simple-range-slider-ss.jpg -------------------------------------------------------------------------------- /06.simple-range-slider/styles/styles.css: -------------------------------------------------------------------------------- 1 | .simple-slider { 2 | --slider-path-diameter: 6px; 3 | --slider-handler-size: 14px; 4 | --slider-path-color: #ddd; 5 | --slider-progress-color: #1c70ff; 6 | --slider-loading-progress-color: #ccc; 7 | 8 | position: relative; 9 | display: block; 10 | width: 100%; 11 | padding: calc(8px / 1) 0; 12 | padding: calc(var(--slider-path-diameter, 8px) / 1) 0; 13 | } 14 | 15 | .simple-slider.--vertical { 16 | display: inline-block; 17 | width: auto; 18 | padding: 0 calc(8px / 1); 19 | padding: 0 calc(var(--slider-path-diameter, 8px) / 1); 20 | } 21 | 22 | .simple-slider__path { 23 | display: inline-block; 24 | width: 100%; 25 | height: 8px; 26 | height: var(--slider-path-diameter, 8px); 27 | background-color: var(--slider-path-color, #ddd); 28 | border-radius: 25px; 29 | position: relative; 30 | cursor: pointer; 31 | overflow: hidden; 32 | } 33 | 34 | .simple-slider.--vertical .simple-slider__path { 35 | height: 100%; 36 | width: 8px; 37 | width: var(--slider-path-diameter, 8px); 38 | margin: 0 auto; 39 | } 40 | 41 | .simple-slider__loading-progress, 42 | .simple-slider__progress { 43 | position: absolute; 44 | left: 0; 45 | width: 0; 46 | height: 100%; 47 | background-color: var(--slider-progress-color, #1c70ff); 48 | max-width: 100% !important; 49 | z-index: 1; 50 | } 51 | 52 | .simple-slider.--vertical .simple-slider__loading-progress, 53 | .simple-slider.--vertical .simple-slider__progress { 54 | left: unset; 55 | bottom: 0; 56 | width: 100%; 57 | height: 0; 58 | } 59 | 60 | .simple-slider__loading-progress { 61 | border-radius: 25px; 62 | z-index: 0; 63 | background-color: var(--slider-loading-progress-color, #ccc); 64 | } 65 | 66 | .simple-slider__handler { 67 | -webkit-box-sizing: border-box; 68 | box-sizing: border-box; 69 | width: 8px; 70 | width: var(--slider-path-diameter, 8px); 71 | height: 8px; 72 | height: var(--slider-path-diameter, 8px); 73 | background: rgba(174, 196, 221, 0.7); 74 | border-radius: 50%; 75 | position: absolute; 76 | top: 50%; 77 | left: .5%; 78 | margin-left: calc(8px * -.5); 79 | margin-left: calc(var(--slider-path-diameter, 8px) * -.5); 80 | margin-top: calc(8px * -.5); 81 | margin-top: calc(var(--slider-path-diameter, 8px) * -.5); 82 | cursor: pointer; 83 | -webkit-transition: opacity .1s, visibility .3s, -webkit-box-shadow .3s; 84 | transition: opacity .1s, visibility .3s, -webkit-box-shadow .3s; 85 | -o-transition: box-shadow .3s, opacity .1s, visibility .3s; 86 | transition: box-shadow .3s, opacity .1s, visibility .3s; 87 | transition: box-shadow .3s, opacity .1s, visibility .3s, -webkit-box-shadow .3s; 88 | -webkit-transform-origin: center; 89 | -ms-transform-origin: center; 90 | transform-origin: center; 91 | opacity: 0; 92 | visibility: hidden; 93 | z-index: 5; 94 | } 95 | 96 | .simple-slider.--vertical .simple-slider__handler { 97 | left: 50%; 98 | top: unset; 99 | bottom: 0; 100 | margin: 0; 101 | margin-left: calc(8px * -.5); 102 | margin-left: calc(var(--slider-path-diameter, 8px) * -.5); 103 | margin-bottom: calc(8px * -.5); 104 | margin-bottom: calc(var(--slider-path-diameter, 8px) * -.5); 105 | 106 | } 107 | 108 | .simple-slider:hover .simple-slider__handler, 109 | .simple-slider.--dragging .simple-slider__handler, 110 | .simple-slider__handler:hover { 111 | -webkit-box-shadow: 0 0 0 calc(var(--slider-handler-size)) rgba(174, 196, 221, 0.7); 112 | -ms-box-shadow: 0 0 0 calc(var(--slider-handler-size)) rgba(174, 196, 221, 0.7); 113 | box-shadow: 0 0 0 calc(var(--slider-handler-size)) rgba(174, 196, 221, 0.7); 114 | background: var(--slider-progress-color, #1c70ff); 115 | opacity: 1; 116 | visibility: visible; 117 | -webkit-transition: opacity .2s, visibility .2s, -webkit-box-shadow .4s; 118 | transition: opacity .2s, visibility .2s, -webkit-box-shadow .4s; 119 | -o-transition: box-shadow .4s, opacity .2s, visibility .2s; 120 | transition: box-shadow .4s, opacity .2s, visibility .2s; 121 | transition: box-shadow .4s, opacity .2s, visibility .2s, -webkit-box-shadow .4s; 122 | } 123 | -------------------------------------------------------------------------------- /07.web-chat-app/components/active-chat.js: -------------------------------------------------------------------------------- 1 | class ActiveChat extends Component { 2 | 3 | /** 4 | * define attributes types 5 | * @returns {Object} 6 | */ 7 | static get attrTypes() { 8 | return { 9 | id: { 10 | type: "string", 11 | observe: true 12 | }, 13 | name: { 14 | type: "string", 15 | observe: true 16 | }, 17 | avatar: { 18 | type: "string", 19 | observe: true 20 | }, 21 | online: { 22 | type: "boolean", 23 | observe: true 24 | }, 25 | }; 26 | } 27 | 28 | /** 29 | * generate observed attributes array from attr types object 30 | */ 31 | static get observedAttributes() { 32 | return super.getObservedAttrs(ActiveChat.attrTypes); 33 | } 34 | 35 | /** 36 | * generate tag-name from component class name 37 | * @returns {string} 38 | */ 39 | static get tagName() { 40 | return super.generateTagName(ActiveChat.name); 41 | } 42 | 43 | /** 44 | * styles of component 45 | * @returns {string} 46 | */ 47 | static get style() { 48 | return (``) 151 | } 152 | 153 | /** 154 | * html template of component 155 | * @returns {string} 156 | */ 157 | static get template() { 158 | return (` 159 | 185 | `) 186 | } 187 | 188 | constructor() { 189 | super({ 190 | attrTypes: ActiveChat.attrTypes, 191 | template: ActiveChat.template 192 | }); 193 | 194 | this._backBtn = this.shadowRoot.getElementById("back-btn"); 195 | 196 | // render component 197 | this.render(); 198 | } 199 | 200 | // call on attributes changed 201 | attributeChangedCallback(attrName, oldValue, newValue) { 202 | if (oldValue === newValue) 203 | return; 204 | 205 | // re-render component 206 | this.render(); 207 | } 208 | 209 | onMount() { 210 | this._backBtn.addEventListener("click", this._onBackBtnClicked.bind(this)) 211 | } 212 | 213 | onUnmount() { 214 | this._backBtn.removeEventListener("click", this._onBackBtnClicked.bind(this)) 215 | } 216 | 217 | /** 218 | * fires when back btn clicked 219 | * @private 220 | */ 221 | _onBackBtnClicked(){ 222 | this.emit(APP_EVENTS.CHAT_BOX_BACK_CLICKED); 223 | } 224 | 225 | /** 226 | * render component according to template and attributes 227 | */ 228 | render() { 229 | 230 | // check the existence of avatar 231 | // fetch first char of name to show if avatar not passed 232 | if (!this.getAttribute("avatar")) { 233 | // put first char of name when avatar not passed 234 | const name = (this.getAttribute("name") || "").toUpperCase(); 235 | this.shadowRoot.querySelector(".char-avatar").innerText = name.substr(0, 1); 236 | } 237 | 238 | // loop over attributes and set all 239 | for (let attr of this.attributes) { 240 | const target = this.shadowRoot.getElementById(attr.name); 241 | if (!target) 242 | continue; 243 | 244 | switch (attr.name) { 245 | case "name": 246 | target.innerText = attr.value; 247 | break; 248 | case "avatar": 249 | target.src = attr.value; 250 | break; 251 | } 252 | 253 | } 254 | } 255 | 256 | } 257 | 258 | // define active-chat tag name 259 | customElements.define(ActiveChat.tagName, ActiveChat); 260 | -------------------------------------------------------------------------------- /07.web-chat-app/components/app-brand.js: -------------------------------------------------------------------------------- 1 | class AppBrand extends Component { 2 | 3 | /** 4 | * define attributes types 5 | * @returns {Object} 6 | */ 7 | static get attrTypes() { 8 | return {}; 9 | } 10 | 11 | /** 12 | * generate observed attributes array from attr types object 13 | */ 14 | static get observedAttributes() { 15 | return super.getObservedAttrs(AppBrand.attrTypes); 16 | } 17 | 18 | /** 19 | * generate tag-name from component class name 20 | * @returns {string} 21 | */ 22 | static get tagName() { 23 | return super.generateTagName(AppBrand.name); 24 | } 25 | 26 | /** 27 | * styles of component 28 | * @returns {string} 29 | */ 30 | static get style() { 31 | return (``) 79 | } 80 | 81 | /** 82 | * html template of component 83 | * @returns {string} 84 | */ 85 | static get template() { 86 | return (` 87 | 104 | `) 105 | } 106 | 107 | constructor() { 108 | super({ 109 | attrTypes: AppBrand.attrTypes, 110 | template: AppBrand.template 111 | }); 112 | 113 | this._profileBtn = this.shadowRoot.getElementById("profile-btn"); 114 | } 115 | 116 | // call on mounting 117 | onMount() { 118 | this.initListeners(); 119 | } 120 | 121 | // call on un-mounting 122 | onUnmount() { 123 | this.removeListeners(); 124 | } 125 | 126 | /** 127 | * Initialize required listeners 128 | */ 129 | initListeners() { 130 | this._profileBtn.addEventListener("click", this._onProfileBtnCLick.bind(this)) 131 | } 132 | 133 | /** 134 | * remove added listeners 135 | */ 136 | removeListeners() { 137 | this._profileBtn.removeEventListener("click", this._onProfileBtnCLick.bind(this)) 138 | } 139 | 140 | /** 141 | * handle profile button click 142 | * @param e 143 | * @private 144 | */ 145 | _onProfileBtnCLick(e) { 146 | this.emit(APP_EVENTS.PROFILE_BTN_CLICK) 147 | } 148 | } 149 | 150 | // define app-brand tag name 151 | customElements.define(AppBrand.tagName, AppBrand); 152 | -------------------------------------------------------------------------------- /07.web-chat-app/components/authed-user.js: -------------------------------------------------------------------------------- 1 | class AuthedUser extends Component { 2 | 3 | /** 4 | * define attributes types 5 | * @returns {Object} 6 | */ 7 | static get attrTypes() { 8 | return { 9 | id: { 10 | type: "string", 11 | observe: true 12 | }, 13 | name: { 14 | type: "string", 15 | observe: true 16 | }, 17 | avatar: { 18 | type: "string", 19 | observe: true 20 | }, 21 | hidden: { 22 | type: "boolean", 23 | observe: true 24 | }, 25 | }; 26 | } 27 | 28 | /** 29 | * generate observed attributes array from attr types object 30 | */ 31 | static get observedAttributes() { 32 | return super.getObservedAttrs(AuthedUser.attrTypes); 33 | } 34 | 35 | /** 36 | * generate tag-name from component class name 37 | * @returns {string} 38 | */ 39 | static get tagName() { 40 | return super.generateTagName(AuthedUser.name); 41 | } 42 | 43 | /** 44 | * styles of component 45 | * @returns {string} 46 | */ 47 | static get style() { 48 | return (``) 142 | } 143 | 144 | /** 145 | * html template of component 146 | * @returns {string} 147 | */ 148 | static get template() { 149 | return (` 150 | 184 | `) 185 | } 186 | 187 | constructor() { 188 | super({ 189 | attrTypes: AuthedUser.attrTypes, 190 | template: AuthedUser.template 191 | }); 192 | 193 | // render component 194 | this.render(); 195 | } 196 | 197 | // call on attributes changed 198 | attributeChangedCallback(attrName, oldValue, newValue) { 199 | if (oldValue === newValue) 200 | return; 201 | 202 | // re-render component 203 | this.render(); 204 | } 205 | 206 | setUser(user) { 207 | this._user = user; 208 | 209 | this.setAttribute("id", user.id); 210 | this.setAttribute("name", user.name); 211 | this.setAttribute("username", user.username); 212 | this.setAttribute("avatar", user.avatar); 213 | } 214 | 215 | /** 216 | * reflect the hidden attr on HTML tag 217 | * @param value 218 | */ 219 | set hidden(value) { 220 | if (value) 221 | this.setAttribute("hidden", ''); 222 | else 223 | this.removeAttribute("hidden") 224 | } 225 | 226 | get hidden() { 227 | return this.hasAttribute("hidden") 228 | } 229 | 230 | /** 231 | * render component according to template and attributes 232 | */ 233 | render() { 234 | 235 | // check the existence of avatar 236 | // fetch first char of title to show if avatar not passed 237 | if (!this.getAttribute("avatar")) { 238 | // put first char of title when avatar not passed 239 | const name = (this.getAttribute("name") || "").toUpperCase(); 240 | this.shadowRoot.querySelector(".char-avatar").innerText = name.substr(0, 1); 241 | } 242 | 243 | // loop over attributes and set all 244 | for (let attr of this.attributes) { 245 | const target = this.shadowRoot.getElementById(attr.name); 246 | if (!target) 247 | continue; 248 | 249 | switch (attr.name) { 250 | case "username": 251 | case "name": 252 | target.innerText = attr.value; 253 | break; 254 | case "avatar": 255 | target.src = attr.value; 256 | break; 257 | } 258 | 259 | } 260 | } 261 | } 262 | 263 | // define auth-user tag name 264 | customElements.define(AuthedUser.tagName, AuthedUser); 265 | -------------------------------------------------------------------------------- /07.web-chat-app/components/component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main component class that other components extends from it 3 | */ 4 | class Component extends HTMLElement { 5 | 6 | constructor({attrTypes, template, shadowMode = "open"}) { 7 | super(); 8 | 9 | this.attrTypes = attrTypes; 10 | this._template = template; 11 | this._shadowMode = shadowMode; 12 | 13 | // his method attach template to root if exists 14 | this.makeShadow(); 15 | } 16 | 17 | /** 18 | * this method fire when component attached to DOM 19 | */ 20 | connectedCallback() { 21 | // Check attributes types for each component 22 | this.checkAttrs(); 23 | // and call onMount method 24 | // onMount is the only method that call inside connectedCallback 25 | if (this.onMount && typeof this.onMount === "function") 26 | this.onMount(); 27 | } 28 | 29 | /** 30 | * This method fire when component removed from DOM 31 | */ 32 | disconnectedCallback() { 33 | // call onMount method of component. 34 | // onUnmount is the only method that call inside disconnectedCallback 35 | if (this.onUnmount && typeof this.onUnmount === "function") 36 | this.onUnmount(); 37 | } 38 | 39 | /** 40 | * parse attribute types according to passed types 41 | * @param value 42 | * @param target 43 | * @returns {(number | boolean | string)|*} 44 | */ 45 | parseAttrType(value, target) { 46 | if (value === void 0 || value === null) 47 | return value; 48 | 49 | switch (target) { 50 | case "n": 51 | case "number": 52 | value = value.indexOf(".") ? parseFloat(value) : parseInt(value); 53 | break; 54 | 55 | case "o": 56 | case "object": 57 | value = JSON.parse(value); 58 | break; 59 | 60 | case "b": 61 | case "bool": 62 | case "boolean": 63 | value = Boolean(value); 64 | break; 65 | 66 | default: 67 | value = value.toString() 68 | } 69 | 70 | return value; 71 | } 72 | 73 | /** 74 | * check type of attributes 75 | */ 76 | checkAttrs() { 77 | if (!this.attrTypes) 78 | return; 79 | 80 | for (let [attr, details] of Object.entries(this.attrTypes)) { 81 | 82 | let value = this.parseAttrType(this.getAttribute(attr), details.type); 83 | 84 | // replace attribute with parsed value if value is not null 85 | if (value !== null) 86 | this.setAttribute(attr, value || ""); 87 | 88 | if (details.required) 89 | this.assert(!!value, 90 | `"${attr}" attr is knows as required but not passed to component.`); 91 | 92 | if (value !== null && details.type) { 93 | this.assert(typeof value === details.type, 94 | `The type of "${attr}" attr must be ${details.type}.`); 95 | } 96 | 97 | } 98 | } 99 | 100 | /** 101 | * to check condition and fire event if its false 102 | * @param condition 103 | * @param error 104 | */ 105 | assert(condition, error) { 106 | if (!condition) 107 | console.error(`Warning: ${error}`) 108 | } 109 | 110 | /** 111 | * parse html and get content as html 112 | * @returns {Node} 113 | */ 114 | parseTemplate() { 115 | let parser = new DOMParser(); 116 | const doc = parser.parseFromString(this._template, 'text/html'); 117 | 118 | return doc.querySelector("template").content.cloneNode(true); 119 | } 120 | 121 | /** 122 | * attach template to shadow 123 | */ 124 | makeShadow() { 125 | // get template note 126 | const template = this.parseTemplate(); 127 | 128 | // generate shadow dom 129 | this.attachShadow({mode: this._shadowMode}).appendChild(template); 130 | } 131 | 132 | /** 133 | * dispatch an event 134 | * @param event 135 | * @param detail 136 | */ 137 | emit(event, detail) { 138 | this.dispatchEvent(new CustomEvent(event, {detail})); 139 | } 140 | 141 | /** 142 | * Add listener to the host 143 | * @param event 144 | * @param callback 145 | */ 146 | on(event, callback) { 147 | this.shadowRoot.host.addEventListener(event, callback.bind(this)) 148 | } 149 | 150 | /** 151 | * Remove listener of the host 152 | * @param event 153 | * @param callback 154 | */ 155 | off(event, callback) { 156 | this.shadowRoot.host.removeEventListener(event, callback.bind(this)) 157 | } 158 | 159 | get disabled() { 160 | return this.hasAttribute('disabled'); 161 | } 162 | 163 | /** 164 | * reflect the disabled attr on HTML tag 165 | * @param val 166 | */ 167 | set disabled(val) { 168 | const isDisabled = Boolean(val); 169 | if (isDisabled) 170 | this.setAttribute('disabled', ''); 171 | else 172 | this.removeAttribute('disabled'); 173 | } 174 | 175 | /** 176 | * generate tag-name from component class name 177 | * @returns {string} 178 | */ 179 | static generateTagName(className) { 180 | return className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 181 | } 182 | 183 | /** 184 | * generate list of attrs has observe:true 185 | * @param attrTypes {Object} 186 | * @returns {string[]} 187 | */ 188 | static getObservedAttrs(attrTypes = {}) { 189 | return Object.entries(attrTypes || {}) 190 | .filter(([_, details]) => details.observe) 191 | .map(([attr, _]) => attr); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /07.web-chat-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Web Chat App 9 | 10 | 11 | 12 |
13 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /07.web-chat-app/scripts/chat-app.js: -------------------------------------------------------------------------------- 1 | // connection between components is with events and 2 | // this const object is for keeping all event types in one place to easy access 3 | window.APP_EVENTS = { 4 | PROFILE_BTN_CLICK: "profile-btn-click", 5 | CHAT_CLICKED: "chat-clicked", 6 | CHAT_SELECTED: "chat-selected", 7 | AUTHED_USER_NEW_MESSAGE: "authed-user-new-message", 8 | USER_SIGN_IN: "user-sign-in", 9 | SEARCH_IN_CHATS: "search-in-chats", 10 | NEW_MESSAGE_RECEIVE: "new-message-receive", 11 | CHAT_BOX_BACK_CLICKED: "chat-box-back-clicked", 12 | DESELECT_SELECTED_CHAT: "deselect-selected-chat", 13 | }; 14 | 15 | /** 16 | * This class controls the whole app 17 | */ 18 | class ChatApp { 19 | 20 | /** 21 | * this receive the id of container of app and get it 22 | * the container is required to run app 23 | * @param appId 24 | */ 25 | constructor(appId) { 26 | // check the existence of appId 27 | this.assert(appId, "app container id not passed."); 28 | this._app = document.getElementById(appId); 29 | // check the existence of container 30 | this.assert(this._app, `Container with id "${appId}" not found. `); 31 | 32 | this._authedUser = null; 33 | this._chats = []; 34 | this._messages = []; 35 | this._componenets = {}; 36 | 37 | // find and assign required app-components 38 | this.assignComponents(); 39 | 40 | // initialize listeners 41 | this.initListeners(); 42 | 43 | // render the existed chats 44 | this.sendChatsToList(); 45 | } 46 | 47 | /** 48 | * Find main components and assign it to this._components property 49 | * It's just for remove duplication, after this, we access all components 50 | * in this._components without re-select from DOM and just 51 | */ 52 | assignComponents() { 53 | this._componenets.authedUser = document.querySelector("authed-user"); 54 | this._componenets.appBranch = document.querySelector("app-brand"); 55 | this._componenets.chatsList = document.querySelector("chats-list"); 56 | this._componenets.chatBox = document.querySelector("chat-box"); 57 | } 58 | 59 | /** 60 | * This method is for initializing required events 61 | */ 62 | initListeners() { 63 | this._componenets.appBranch.on(APP_EVENTS.PROFILE_BTN_CLICK, this._onProfileBtnClick.bind(this)); 64 | this._componenets.chatsList.on(APP_EVENTS.CHAT_SELECTED, this._onChatSelected.bind(this)); 65 | this._componenets.chatBox.on(APP_EVENTS.AUTHED_USER_NEW_MESSAGE, this._onAuthedUserNewMessages.bind(this)); 66 | this._componenets.chatBox.on(APP_EVENTS.CHAT_BOX_BACK_CLICKED, this._onChatBoxBack.bind(this)); 67 | } 68 | 69 | /** 70 | * To simulate sign-in, use this method. 71 | * Logged in user object should pass to this. 72 | * @param user 73 | */ 74 | signin(user) { 75 | // check the validity of user object 76 | this.assert(user && user.id, "Invalid user object"); 77 | this._authedUser = user; 78 | 79 | // after sign-in we need to set active user on authedUser component 80 | // and make it hidden by default 81 | // and tell the chatBox component that a user is signed-in 82 | this._componenets.authedUser.setUser(this._authedUser); 83 | this._componenets.authedUser.hidden = true; 84 | this._componenets.chatBox.emit(APP_EVENTS.USER_SIGN_IN, {id: user.id}); 85 | } 86 | 87 | /** 88 | * use this method to send message to app 89 | * @param msg {{time: Date, sender: String, text: String, toChat: String}} 90 | */ 91 | newMessage(msg) { 92 | // check the validity of received msg object 93 | this.assert(msg && msg.time && msg.sender && msg.text && msg.toChat, 94 | `Invalid message object.`); 95 | 96 | // push to messages pool 97 | this._messages.push(msg); 98 | 99 | // we need to check the sender of new message, if it send by logged in 100 | // user we should tell chatBox to render the message too. 101 | if (this.activeChat && msg.sender === this.activeChat.id) { 102 | this._componenets.chatBox.renderMessage(msg); 103 | } 104 | // also we need to send received message to chatsList component 105 | this._componenets.chatsList.emit(APP_EVENTS.NEW_MESSAGE_RECEIVE, msg); 106 | } 107 | 108 | /** 109 | * getter for this._authedUser 110 | * @returns {Object} 111 | */ 112 | get authedUser() { 113 | return this._authedUser; 114 | } 115 | 116 | /** 117 | * handle profile section visibility on profile-btn click 118 | * @private 119 | */ 120 | _onProfileBtnClick() { 121 | // toggle the visibility of authedUser component 122 | this._componenets.authedUser.hidden = !this._componenets.authedUser.hidden 123 | } 124 | 125 | /** 126 | * this method fire when a chat selected. 127 | * it find the messages of target chat and send those to chatBox to render 128 | * @param detail 129 | * @private 130 | */ 131 | _onChatSelected({detail}) { 132 | // find all messages of selected chat 133 | const chatMessaged = this._messages.filter(m => m.sender === detail.id || m.toChat === detail.id); 134 | 135 | // set selected chat as activeChat of whole app 136 | this.activeChat = this._chats.find(c => c.id === detail.id); 137 | 138 | // if the chatBox if open for activeChat, scroll content to end 139 | if (this._componenets.chatBox.activeChat && this._componenets.chatBox.activeChat.id === this.activeChat.id) { 140 | this._componenets.chatBox.scrollToEnd(); 141 | return; 142 | } 143 | 144 | // mark all messages as read and remove unread badge for selected chat 145 | this.activeChat.elm.markAllAsRead(); 146 | // change the current chat of chatBox component 147 | this._componenets.chatBox.setActiveChat(this.activeChat); 148 | 149 | // send all messages of target chat to render in chatBox 150 | chatMessaged.map(msg => { 151 | this._componenets.chatBox.renderMessage(msg) 152 | }) 153 | } 154 | 155 | /** 156 | * this method fire when a new message from signed in user sent to a chat 157 | * @param detail 158 | * @private 159 | */ 160 | _onAuthedUserNewMessages({detail}) { 161 | // add sender property to message and 162 | // push it to the messages pool 163 | this._messages.push({...detail, sender: this.authedUser.id}) 164 | } 165 | 166 | /** 167 | * fires when back btn clicked in chat-box 168 | * @private 169 | */ 170 | _onChatBoxBack() { 171 | this._componenets.chatsList.emit(APP_EVENTS.DESELECT_SELECTED_CHAT); 172 | } 173 | 174 | /** 175 | * send chats to chatList 176 | */ 177 | sendChatsToList() { 178 | if (!this._chats) 179 | return; 180 | 181 | this._componenets.chatsList.setChats(this._chats) 182 | } 183 | 184 | /** 185 | * use this method to add new chat to whole app 186 | * @param chat 187 | */ 188 | addChat(chat) { 189 | // check the validity of chat object 190 | this.assert(chat && chat.id, `Invalid chat object.`); 191 | 192 | // update chats array 193 | this._chats.push(chat); 194 | 195 | this.sendChatsToList(); 196 | } 197 | 198 | /** 199 | * to check condition and fire event if its false 200 | * @param condition 201 | * @param error 202 | */ 203 | assert(condition, error) { 204 | if (!condition) 205 | throw new Error(`${error}`) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /07.web-chat-app/scripts/data-factory.js: -------------------------------------------------------------------------------- 1 | // generate random date 2 | function randomDate() { 3 | const start = 1585008324467; 4 | const end = new Date().getTime(); 5 | 6 | let date = new Date(+start + Math.random() * (end - start)); 7 | let hour = randomNumber(24, 1); 8 | date.setHours(hour); 9 | return date; 10 | } 11 | 12 | // generate random number in range 13 | function randomNumber(max, min = 0) { 14 | return Math.floor(Math.random() * (max - min) + min); 15 | } 16 | 17 | // generate chat object 18 | function chatGenerator(index) { 19 | const names = ["Mario Speedwagon", "Petey Cruiser", "Anna Sthesia", "Paul Molive", "Anna Mull", "Gail Forcewind", "Paige Turner", "Bob Frapples", "Walter Melon", "Nick R. Bocker", "Barb Ackue", "Buck Kinnear", "Greta Life", "Ira Membrit", "Shonda Leer", "Brock Lee", "Maya Didas", "Rick O'Shea", "Pete Sariya", "Monty Carlo", "Sal Monella", "Sue Vaneer", "Cliff Hanger", "Barb Dwyer", "Terry Aki", "Cory Ander", "Robin Banks", "Jimmy Changa", "Barry Wine", "Wilma Mumduya", "Buster Hyman", "Poppa Cherry", "Zack Lee", "Don Stairs", "Saul T. Balls", "Peter Pants", "Hal Appeno", "Otto Matic", "Moe Fugga", "Graham Cracker", "Tom Foolery", "Al Dente", "Bud Wiser", "Polly Tech", "Holly Graham", "Frank N. Stein", "Cam L. Toe", "Pat Agonia", "Tara Zona", "Barry Cade"] 20 | const name = names[index]; 21 | let lastseen = randomDate().toLocaleDateString().replace(/\//g, "."); 22 | if (index % 3 === 0) 23 | lastseen = "Today"; 24 | if (index % 4 === 0) 25 | lastseen = "Yesterday"; 26 | 27 | return { 28 | id: Math.random().toString(32).substr(2, 10), 29 | name, 30 | username: name.replace(/[^a-zA-Z]/g, '').toLowerCase().substr(0, 8), 31 | online: Math.random() > .7, 32 | lastseen, 33 | unreadcount: "0", 34 | avatar: `https://randomuser.me/api/portraits/${index % 3 ? "women" : "men"}/${index + 1}.jpg`, 35 | } 36 | } 37 | 38 | // generate random sentences for messages 39 | function getRandomText(sub = false) { 40 | // sub-string a long paragraph. 41 | if (sub) { 42 | const lorem = `If the family member doesn’t need hospitalization and can be cared for at home, you should help him or her with basic needs and monitor the symptoms, while also keeping as much distance as possible, according to guidelines issued by the C.D.C. If there’s space, the sick family member should stay in a separate room and use a separate bathroom. If masks are available, both the sick person and the caregiver should wear them when the caregiver enters the room. Make sure not to share any dishes or other household items and to regularly clean surfaces like counters, doorknobs, toilets and tables. Don’t forget to wash your hands frequently.`; 43 | const i1 = randomNumber(lorem.length, 6); 44 | const i2 = randomNumber(lorem.length, 6); 45 | const start = Math.min(i1, i2); 46 | const end = Math.min(i2, i1); 47 | return lorem.substr(start, end) 48 | } 49 | 50 | // make a sentences of random words 51 | let verbs, nouns, adjectives, adverbs, preposition; 52 | nouns = ["bird", "clock", "boy", "plastic", "duck", "teacher", "old lady", "professor", "hamster", "dog", 53 | "area", "book", "business", "case", "child", "company", "country", "day", "eye", 54 | "fact", "family", "government", "group", "hand", "home", "job", "life", "lot"]; 55 | verbs = ["kicked", "ran", "flew", "dodged", "sliced", "rolled", "died", "breathed", "slept", "killed", 56 | "ask", "be", "become", "begin", "call", "can", "come", "could", "do", 57 | "feel", "find", "get", "give", "go", "have", "hear", "help", "keep", "know",]; 58 | adjectives = ["beautiful", "lazy", "professional", "lovely", "dumb", "rough", "soft", "hot", "vibrating", "slimy", "important", 59 | "able", "bad", "best", "better", "big", "black", "certain", "clear", "different", "early", 60 | "easy", "economic", "federal", "free", "full", "good", "great", "hard", "high", "human"]; 61 | adverbs = ["slowly", "elegantly", "precisely", "quickly", "sadly", "humbly", "proudly", "shockingly", "calmly", "passionately"]; 62 | preposition = ["down", "into", "up", "on", "upon", "below", "above", "through", "across", "towards"]; 63 | 64 | 65 | var rand1 = Math.floor(Math.random() * 10); 66 | var rand2 = Math.floor(Math.random() * 10); 67 | var rand3 = Math.floor(Math.random() * 30); 68 | var rand4 = Math.floor(Math.random() * 30); 69 | var rand5 = Math.floor(Math.random() * 30); 70 | var rand6 = Math.floor(Math.random() * 30); 71 | return "The " + adjectives[rand1] + " " + nouns[rand2] + " " + adverbs[rand1] + " " + verbs[rand4] + " because some " + nouns[rand1] 72 | + " " + adverbs[rand2] + " " + verbs[rand1] + " " + preposition[rand1] + " a " + adjectives[rand2] + " " + nouns[rand5] 73 | + " which, became a " + adjectives[rand3] + ", " + adjectives[rand4] + " " + nouns[rand6] + "."; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /07.web-chat-app/scripts/index.js: -------------------------------------------------------------------------------- 1 | const numberOfChats = 10; 2 | let fakeChats = []; 3 | // generate an array of fake chats to show in app 4 | for (let i = 1; i < numberOfChats; i++) { 5 | fakeChats.push(chatGenerator(i)) 6 | } 7 | 8 | // this is the signed-in user object 9 | const authedUser = { 10 | id: '12', 11 | name: "Behnam Azimi", 12 | username: "bhnmzm", 13 | online: true, 14 | lastSeen: "Today", 15 | avatar: "https://randomuser.me/api/portraits/men/1.jpg" 16 | }; 17 | 18 | // create instance of ChatApp, 19 | // this is the line that run application 20 | const app = new ChatApp("chat-web-app"); 21 | app.signin(authedUser); 22 | 23 | // add all generated chats to app one-by-one 24 | fakeChats.map(fc => app.addChat(fc)); 25 | 26 | 27 | // below code is just for simulating message receive 28 | // here we send 100 messages in different times ro app 29 | let fakeMsgCounter = 100; 30 | const interval = setInterval(() => { 31 | 32 | if (--fakeMsgCounter === 0) { 33 | clearInterval(interval); 34 | return; 35 | } 36 | 37 | setTimeout(() => { 38 | const fakeSender = fakeChats[randomNumber(numberOfChats, 1)]; 39 | if (!fakeSender) 40 | return; 41 | 42 | // flag with a 20% probability 43 | const randomFlag = Math.random() > .8; 44 | 45 | app.newMessage({ 46 | text: getRandomText(Math.random() > .5), 47 | sender: randomFlag ? authedUser.id : fakeSender.id, 48 | time: new Date(), 49 | toChat: randomFlag ? fakeSender.id : authedUser.id 50 | }); 51 | 52 | // new message sending time can be dynamic, between 1s and 5s 53 | }, randomNumber(1000, 5000)) 54 | 55 | }, 1500); 56 | -------------------------------------------------------------------------------- /07.web-chat-app/scripts/recorder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this class control the recording functionality. 3 | * it's enough to create an instance of this and call start() 4 | * to start recording and stop() to put the end to the recording. 5 | */ 6 | class Recorder { 7 | 8 | constructor() { 9 | this._recorder = null; 10 | this._audioChunks = []; 11 | 12 | this.init(); 13 | } 14 | 15 | /** 16 | * initial the recorder and create a new instance of MediaRecorder. 17 | */ 18 | init() { 19 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 20 | if (navigator.getUserMedia) { 21 | navigator.getUserMedia({audio: true}, (stream) => { 22 | 23 | this._recorder = new MediaRecorder(stream); 24 | 25 | // we need to listen for data available for the recorder and update the audio chunk 26 | this._recorder.addEventListener("dataavailable", (e) => { 27 | this._audioChunks.push(e.data) 28 | }); 29 | 30 | }, () => { 31 | throw new Error("Use Media not found.") 32 | }); 33 | } else { 34 | throw new Error("Use Media not found.") 35 | } 36 | } 37 | 38 | /** 39 | * call start method of recorder 40 | */ 41 | start() { 42 | if (!this._recorder) 43 | return; 44 | 45 | this._audioChunks = []; 46 | this._recorder.start(); 47 | } 48 | 49 | /** 50 | * call the stop method of recorder and generate the audio object and resolve it 51 | * @returns {Promise} 52 | */ 53 | stop() { 54 | return new Promise((resolve) => { 55 | 56 | // to create audio, we should listen for stop event of recorder 57 | this._recorder.addEventListener("stop", async () => { 58 | 59 | // to create the audio, we should make its Blob first 60 | // and then create a object URL for it and pass it to the Audio API 61 | const audioBlob = new Blob(this._audioChunks); 62 | const audioUrl = URL.createObjectURL(audioBlob); 63 | this._audio = new Audio(audioUrl); 64 | 65 | // calc the duration of audio 66 | const duration = await this.findDuration(audioBlob); 67 | 68 | resolve({audio: this._audio, duration, audioUrl}) 69 | }); 70 | 71 | // stop the recording 72 | this._recorder.stop(); 73 | }) 74 | } 75 | 76 | /** 77 | * find duration of recorded audio 78 | * @param blob 79 | * @returns {Promise} 80 | */ 81 | findDuration(blob) { 82 | return new Promise((resolve) => { 83 | const file = new File([blob], "audio.mp3"); 84 | let reader = new FileReader(); 85 | reader.onload = (e) => { 86 | let audioContext = new (window.AudioContext || window.webkitAudioContext)(); 87 | 88 | // Asynchronously decode audio file data contained in an ArrayBuffer. 89 | audioContext.decodeAudioData(e.target.result, function (buffer) { 90 | let floatDuration = buffer.duration; 91 | 92 | let dMin = Math.floor(floatDuration / 60); 93 | let dSec = Math.floor(floatDuration % 60); 94 | 95 | if (dMin < 10) 96 | dMin = "0" + dMin; 97 | 98 | if (dSec < 10) 99 | dSec = "0" + dSec; 100 | 101 | resolve(`${dMin}:${dSec}`) 102 | }); 103 | }; 104 | 105 | reader.readAsArrayBuffer(file); 106 | }) 107 | } 108 | 109 | /** 110 | * getter for audio objec 111 | * @returns {HTMLAudioElement} 112 | */ 113 | get audio() { 114 | return this._audio; 115 | } 116 | 117 | /** 118 | * check if the audio device is available 119 | * @returns {boolean} 120 | */ 121 | static isMicAvailable() { 122 | let isAvailable = true; 123 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 124 | if (navigator.getUserMedia) { 125 | navigator.getUserMedia({audio: true}, (stream) => { 126 | isAvailable = true; 127 | }, () => { 128 | isAvailable = false; 129 | }); 130 | } 131 | 132 | return isAvailable; 133 | } 134 | 135 | /** 136 | * Convert numbers in second to time string like 00:00 137 | * 138 | * @param seconds 139 | * @returns {string} 140 | */ 141 | static secToTimeStr(seconds) { 142 | let timeInHour = Math.floor(seconds / 3600); 143 | let timeInMin = Math.floor((seconds % 3600) / 60); 144 | let timeInSec = Math.floor(seconds % 60); 145 | 146 | if (timeInHour < 10) 147 | timeInHour = `0${timeInHour}`; 148 | 149 | if (timeInMin < 10) 150 | timeInMin = `0${timeInMin}`; 151 | 152 | if (timeInSec < 10) 153 | timeInSec = `0${timeInSec}`; 154 | 155 | let timeStr = `${timeInMin}:${timeInSec}`; 156 | if (parseInt(timeInHour)) 157 | timeStr = `${timeInHour}:${timeStr}`; 158 | 159 | return timeStr; 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /07.web-chat-app/static/chat-box-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/07.web-chat-app/static/chat-box-bg.png -------------------------------------------------------------------------------- /07.web-chat-app/static/mic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /07.web-chat-app/styles/styles.css: -------------------------------------------------------------------------------- 1 | /** Simple Reset - START */ 2 | html { 3 | box-sizing: border-box; 4 | font-size: 16px; 5 | overflow-x: hidden; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, h1, h2, h3, h4, h5, h6, p, ol, ul { 13 | margin: 0; 14 | padding: 0; 15 | font-weight: normal; 16 | } 17 | 18 | ol, ul { 19 | list-style: none; 20 | } 21 | 22 | img { 23 | max-width: 100%; 24 | height: auto; 25 | } 26 | 27 | /** Global - START */ 28 | 29 | body { 30 | font-family: 'Lato', sans-serif; 31 | height: 100vh; 32 | max-height: 100vh; 33 | overflow: hidden; 34 | } 35 | 36 | #chat-web-app { 37 | position: fixed; 38 | height: 100vh; 39 | width: 100vw; 40 | top: 0; 41 | left: 0; 42 | 43 | display: flex; 44 | flex-direction: row; 45 | } 46 | 47 | #chat-web-app .sidebar { 48 | box-shadow: 0 0 5px 2px rgba(0, 0, 0, .14); 49 | display: flex; 50 | flex-direction: column; 51 | width: 280px; 52 | min-width: 280px; 53 | position: relative; 54 | z-index: 2; 55 | } 56 | 57 | @media screen and (max-width: 564px) { 58 | #chat-web-app .sidebar { 59 | min-width: 100%; 60 | background-color: #fff; 61 | z-index: 3; 62 | } 63 | 64 | chat-box { 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | z-index: 2; 69 | background: #fff; 70 | width: 100%; 71 | } 72 | 73 | chat-box:not([hidden]) { 74 | z-index: 5; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /08.canvas-wallpaper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Canvas Wallpaper Tutorial 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /08.canvas-wallpaper/scripts/circle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this class is for control the circle and it's animating 3 | */ 4 | class Circle { 5 | 6 | /** 7 | * this get entrance position and radius and gradient detail 8 | * and generate the circle details 9 | * 10 | * @param entrance 11 | * @param radius 12 | * @param gradientColors 13 | */ 14 | constructor(entrance, radius, gradientColors) { 15 | this._entrance = entrance; 16 | this._gradientColors = gradientColors; 17 | this._radius = radius; 18 | 19 | // we need to keep original radius as const 20 | this._originalRadius = radius; 21 | 22 | // to control the decrease rate of size and position 23 | this._perspectiveRate = 0.1; 24 | 25 | // initialize the xy 26 | this.coordinateXY(); 27 | 28 | // initialize the variables of x, y and radius 29 | this.generatePositionVars(); 30 | } 31 | 32 | /** 33 | * initialize x and y according to size of the canvas 34 | */ 35 | coordinateXY() { 36 | switch (this._entrance) { 37 | case "bottomRight": 38 | this.x = rndNum(canvas.width * 1.1, canvas.width); 39 | this.y = rndNum(canvas.height * 1.2, canvas.height * 1.1); 40 | break; 41 | 42 | case "bottomCenter": 43 | this.x = rndNum(canvas.width / 1.5, canvas.width / 3); 44 | this.y = rndNum(canvas.height * 1.3, canvas.height * 1.2); 45 | break; 46 | 47 | case "bottomLeft": 48 | this.x = rndNum(canvas.width * -.01, canvas.width * -.02); 49 | this.y = rndNum(canvas.height * 1.3, canvas.height * 1.2); 50 | break; 51 | } 52 | 53 | }; 54 | 55 | /** 56 | * initialize the variables of x, y and radius according to 57 | * the entrance position 58 | */ 59 | generatePositionVars() { 60 | this.positionVars = { 61 | bottomRight: { 62 | varY: rndNum(5, 2.5), 63 | varX: rndNum(4, 1.5), 64 | varR: rndNum(this._radius * .007, this._radius * .003) 65 | }, 66 | bottomCenter: { 67 | varY: rndNum(7, 4.5), 68 | varX: rndNum(.5, -.5), 69 | varR: rndNum(this._radius * .007, this._radius * .003) 70 | }, 71 | bottomLeft: { 72 | varY: rndNum(5, 2.5), 73 | varX: rndNum(-3, -6), 74 | varR: rndNum(this._radius * .007, this._radius * .003) 75 | }, 76 | } 77 | }; 78 | 79 | /** 80 | * check if the circle is out of view or not 81 | * this is different for each entrance 82 | * 83 | * @returns {boolean} 84 | */ 85 | isOutOfView() { 86 | switch (this._entrance) { 87 | case "bottomRight": 88 | return this.x + this._radius < 0 || this.y + this._radius < 0; 89 | 90 | case "bottomCenter": 91 | return this.y + this._radius < 0 || (this._radius < 0 && this.x > canvas.width); 92 | 93 | case "bottomLeft": 94 | return this.x + this._radius * 2 < 0 || this.y + (this._radius * 2) < 0; 95 | } 96 | 97 | }; 98 | 99 | /** 100 | * generate the fill gradient of circle according 101 | * to the took gradient details 102 | * 103 | * @returns {CanvasGradient} 104 | */ 105 | generateGradient() { 106 | 107 | let circleFill = ctx.createRadialGradient( 108 | this.x, this.y, this._radius / 2, 109 | this.x, this.y, this._radius); 110 | 111 | // loop over gradient details to add color stop 112 | // each item of this._gradientColors contains an offset and a color 113 | for (let [offset, color] of this._gradientColors) { 114 | circleFill.addColorStop(offset, color); 115 | } 116 | 117 | return circleFill; 118 | }; 119 | 120 | /** 121 | * method to draw a circle on canvas with data obtained 122 | */ 123 | draw() { 124 | ctx.beginPath(); 125 | ctx.arc(this.x, this.y, this._radius, 0, Math.PI * 2, false); 126 | 127 | ctx.strokeStyle = "transparent"; 128 | ctx.fillStyle = this.generateGradient(); 129 | ctx.fill(); 130 | ctx.stroke(); 131 | }; 132 | 133 | /** 134 | * calculate the change rate by percent of topOffset and perspective rate 135 | * 136 | * @param topOffset 137 | * @param rate 138 | * @returns {number} 139 | */ 140 | calcChangeRate(topOffset, rate) { 141 | if (topOffset === 100) return 1; 142 | return (((topOffset - 100) / 10) * rate) + 1 143 | } 144 | 145 | /** 146 | * update the x, y and the radius of the circle, 147 | * and call draw method to re-draw it 148 | */ 149 | animate() { 150 | 151 | // to have easy access, put entrance vars in variables 152 | const varY = this.positionVars[this._entrance].varY; 153 | const varX = this.positionVars[this._entrance].varX; 154 | const varR = this.positionVars[this._entrance].varR; 155 | 156 | // calculate the offsetTop percent of circle position 157 | let topOffset = 100; 158 | if (canvas.height - this.y + this._radius > 0) { 159 | topOffset = this.y * 100 / canvas.height; 160 | } 161 | 162 | // calculate the change rate for the offset 163 | const changeRate = this.calcChangeRate(topOffset, this._perspectiveRate); 164 | 165 | // update the x and y 166 | this.x -= Math.max(varX * changeRate, varX * .3); 167 | this.y -= Math.max(varY * changeRate, varY * .4); 168 | 169 | // update the radius 170 | if (topOffset > 50 && this._radius > this._originalRadius * .3) { 171 | this._radius -= Math.max(varR * changeRate, varR * rndNum(.5, .2)); 172 | } else { 173 | this._radius -= Math.min(varR * changeRate, varR * rndNum(.2, .15)) 174 | } 175 | 176 | // reset the position and the radius if the circle has gone from view 177 | if (this.isOutOfView() || this._radius < 1) { 178 | this.coordinateXY(); 179 | this._radius = this._originalRadius; 180 | } 181 | 182 | // re-draw the circle with updated properties 183 | this.draw(); 184 | }; 185 | 186 | } 187 | -------------------------------------------------------------------------------- /08.canvas-wallpaper/scripts/index.js: -------------------------------------------------------------------------------- 1 | // first of all, I detect my canvas and set the width 2 | // and height of it as large as the screen. 3 | let canvas = document.getElementById("canvas"); 4 | canvas.height = window.innerHeight; 5 | canvas.width = window.innerWidth; 6 | 7 | // then I get and save the context of my canvas as **ctx** also, 8 | const ctx = canvas.getContext("2d"); 9 | 10 | // in order to have easy access to the min/max of the canvas sides 11 | // in the future, I take them and put in the variables 12 | let canvasMin = Math.min(canvas.width, canvas.height); 13 | let canvasMax = Math.max(canvas.width, canvas.height); 14 | 15 | // number of circles that should add to canvas 16 | const numberOfCircles = 50; 17 | 18 | // to have animated canvas with circles, we need to create circles and keep them. 19 | let circles = []; 20 | 21 | // to prevent scaling in canvas we should listen for resize 22 | // of window and update the size of canvas 23 | window.addEventListener("resize", onResize); 24 | 25 | /** 26 | * update the width and height of the canvas on resize 27 | */ 28 | function onResize() { 29 | canvas.height = window.innerHeight; 30 | canvas.width = window.innerWidth; 31 | 32 | canvasMin = Math.min(canvas.width, canvas.height); 33 | canvasMax = Math.max(canvas.width, canvas.height); 34 | } 35 | 36 | /** 37 | * this method is used for generating random numbers between min and max 38 | * @param max 39 | * @param min 40 | * @param floor 41 | * @returns {number} 42 | */ 43 | function rndNum(max, min = 0, floor = false) { 44 | if (floor) 45 | return Math.floor(Math.random() * (max - min) + min); 46 | 47 | return Math.random() * (max - min) + min; 48 | } 49 | 50 | /** 51 | * this function will draw the entire background of canvas. 52 | */ 53 | function drawBackground() { 54 | 55 | // first clear the the whole canvas 56 | ctx.clearRect(0, 0, canvas.width, canvas.height); 57 | 58 | // this will generate the main gradient (main-light) of wallpaper 59 | let mainGrd = ctx.createRadialGradient( 60 | canvas.width / 2, rndNum(-85, -100), 1, 61 | canvas.width / 2, canvasMax / 4, canvasMin * 1.8); 62 | mainGrd.addColorStop(.4, "#1a0003"); 63 | mainGrd.addColorStop(0, "#d58801"); 64 | 65 | // after creating the gradient and set it colors, 66 | // we should set it as the fillStyle of the context and 67 | // paint whole canvas 68 | ctx.fillStyle = mainGrd; 69 | ctx.fillRect(0, 0, canvas.width, canvas.height); 70 | } 71 | 72 | /** 73 | * this method is a util to generate a random circle 74 | * and push it to the circles array to keep 75 | */ 76 | function addNewCircle() { 77 | 78 | // circles can expose in 3 position, 79 | // bottom-left corner, bottom-right corner and bottom center. 80 | const entrances = ["bottomRight", "bottomCenter", "bottomLeft"]; 81 | // I take one of entrances randomly as target entrance 82 | const targetEntrance = entrances[rndNum(entrances.length, 0, true)]; 83 | 84 | // we have 5 different gradient to give each 85 | // circle a different appearance. each item 86 | // in below array has colors and offset of gradient. 87 | const possibleGradients = [ 88 | [ 89 | [0, "rgba(238,31,148,0.14)"], 90 | [1, "rgba(238,31,148,0)"] 91 | ], 92 | [ 93 | [0, "rgba(213,136,1,.2)"], 94 | [1, "rgba(213,136,1,0)"] 95 | ], 96 | [ 97 | [.5, "rgba(213,136,1,.2)"], 98 | [1, "rgba(213,136,1,0)"] 99 | ], 100 | [ 101 | [.7, "rgba(255,254,255,0.07)"], 102 | [1, "rgba(255,254,255,0)"] 103 | ], 104 | [ 105 | [.8, "rgba(255,254,255,0.05)"], 106 | [.9, "rgba(255,254,255,0)"] 107 | ] 108 | ]; 109 | // I take one of gradients details as target gradient details 110 | const targetGrd = possibleGradients[rndNum(possibleGradients.length, 0, true)]; 111 | 112 | // each circle should have a radius. and it will be 113 | // a random number between three and four quarters of canvas-min side 114 | const radius = rndNum(canvasMin / 3, canvasMin / 4); 115 | 116 | // this will push the created Circle to the circles array 117 | circles.push(new Circle(targetEntrance, radius, targetGrd)) 118 | } 119 | 120 | // to add circles randomly I use an interval that fire every 300ms and a timeout in it. 121 | // every 300ms it will call a timeout func with a delay between 700 and 2000ms, and when 122 | // the timeout callback fired, it will call the addNewCircle method 123 | let addingInterval = setInterval(() => { 124 | 125 | // after adding as manny as expected circles, 126 | // we clear the interval to stop adding 127 | if (circles.length > numberOfCircles) 128 | clearInterval(addingInterval); 129 | 130 | setTimeout(() => { 131 | addNewCircle(); 132 | }, rndNum(700, 2000)); 133 | 134 | }, 300); 135 | 136 | /** 137 | * to animate wallpaper, we need to call draw functions frequently. 138 | * to do that, we use the requestAnimationFrame method that is a browser API. 139 | * the requestAnimationFrame method get the animateMyWallpaper as the callback 140 | * function and call it frequently. 141 | * actually we made a recursion that call draw function on each call. 142 | * 143 | * You can read more about [recursion here](https://en.wikipedia.org/wiki/Recursion_(computer_science)) 144 | */ 145 | function animateMyWallpaper() { 146 | 147 | // requestAnimationFrame() is a JavaScript method for creating smoother, 148 | // less resource intensive JavaScript animations 149 | // you can read mote [here](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) 150 | requestAnimationFrame(animateMyWallpaper); 151 | 152 | // call background draw function. 153 | drawBackground(); 154 | 155 | // loop over circles and call animate function of it 156 | for (let i = 0; i < circles.length; i++) { 157 | circles[i].animate(); 158 | } 159 | } 160 | 161 | 162 | // this just starts animations 163 | animateMyWallpaper(); 164 | -------------------------------------------------------------------------------- /08.canvas-wallpaper/styles/styles.css: -------------------------------------------------------------------------------- 1 | /** Simple Reset - START */ 2 | html { 3 | box-sizing: border-box; 4 | font-size: 16px; 5 | overflow-x: hidden; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, h1, h2, h3, h4, h5, h6, p, ol, ul { 13 | margin: 0; 14 | padding: 0; 15 | font-weight: normal; 16 | } 17 | 18 | img { 19 | max-width: 100%; 20 | height: auto; 21 | } 22 | 23 | /** Global - START */ 24 | html { 25 | height: 100%; 26 | } 27 | 28 | body { 29 | height: 100%; 30 | width: 100%; 31 | max-height: 100%; 32 | overflow: hidden; 33 | } 34 | 35 | #canvas{ 36 | width: 100%; 37 | height: 100%; 38 | background-color: #efefef; 39 | } 40 | -------------------------------------------------------------------------------- /09.split-screen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Split Screen 8 | 9 | 10 | 11 | 12 |
13 |
14 |

15 | blue is depth 16 |

17 |
18 |
19 |

20 | black is power 21 |

22 |
23 |
24 | 25 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /09.split-screen/scripts/main.js: -------------------------------------------------------------------------------- 1 | const left = document.querySelector(".left"); 2 | const right = document.querySelector(".right"); 3 | const container = document.querySelector(".container"); 4 | 5 | left.addEventListener("mouseenter", () => { 6 | container.classList.add("hover-left"); 7 | }); 8 | 9 | left.addEventListener("mouseleave", () => { 10 | container.classList.remove("hover-left"); 11 | }); 12 | 13 | right.addEventListener("mouseenter", () => { 14 | container.classList.add("hover-right"); 15 | }); 16 | 17 | right.addEventListener("mouseleave", () => { 18 | container.classList.remove("hover-right"); 19 | }); 20 | -------------------------------------------------------------------------------- /09.split-screen/static/img/bg-bw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/09.split-screen/static/img/bg-bw.jpg -------------------------------------------------------------------------------- /09.split-screen/static/img/bg-colored.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/09.split-screen/static/img/bg-colored.jpg -------------------------------------------------------------------------------- /09.split-screen/style/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;800&display=swap'); 2 | 3 | :root { 4 | --hover-width: 99%; 5 | --other-width: 1%; 6 | --font-family: "Work Sans", sans-serif 7 | } 8 | 9 | html, 10 | body { 11 | padding: 0; 12 | margin: 0; 13 | font-family: var(--font-family); 14 | width: 100%; 15 | height: 100%; 16 | overflow-x: hidden; 17 | text-decoration: none; 18 | box-sizing: border-box; 19 | } 20 | 21 | h2 { 22 | text-transform: uppercase; 23 | font-size: 4rem; 24 | font-weight: normal; 25 | color: #fff; 26 | position: absolute; 27 | left: 50%; 28 | top: 50%; 29 | transform: translate(-50%, -50%); 30 | white-space: nowrap; 31 | z-index: 1; 32 | } 33 | h2 strong { 34 | font-weight: 800; 35 | } 36 | 37 | .container { 38 | background: url("../static/img/bg-bw.jpg") no-repeat; 39 | background-attachment: fixed; 40 | background-position: center; 41 | background-size: cover; 42 | position: relative; 43 | width: 100%; 44 | height: 100%; 45 | } 46 | 47 | .split { 48 | position: absolute; 49 | width: 50%; 50 | height: 100%; 51 | overflow: hidden; 52 | } 53 | 54 | .split.left:before { 55 | content: ""; 56 | position: absolute; 57 | background: url("../static/img/bg-colored.jpg") no-repeat; 58 | background-attachment: fixed; 59 | background-position: center; 60 | background-size: cover; 61 | top: 0; 62 | right: 0; 63 | bottom: 0; 64 | left: 0; 65 | } 66 | 67 | .split.right { 68 | right: 0; 69 | } 70 | 71 | .split.left, 72 | .split.right { 73 | transition: 1000ms all ease-in-out; 74 | } 75 | 76 | .split.left *, 77 | .split.right * { 78 | opacity: 0; 79 | transition: 1000ms all ease-out; 80 | } 81 | 82 | .hover-left .left { 83 | width: var(--hover-width); 84 | } 85 | 86 | .hover-left .left * { 87 | opacity: 1; 88 | } 89 | .hover-left .right { 90 | width: var(--other-width); 91 | } 92 | 93 | .hover-right .right { 94 | width: var(--hover-width); 95 | } 96 | 97 | .hover-right .right * { 98 | opacity: 1; 99 | } 100 | 101 | .hover-right .left { 102 | width: var(--other-width); 103 | } 104 | 105 | @media (max-width: 800px) { 106 | h1 { 107 | font-size: 2rem; 108 | } 109 | } 110 | 111 | .credit { 112 | position: absolute; 113 | bottom: 20px; 114 | right: 20px; 115 | background: rgba(255, 255, 255, 0.4); 116 | border: 2px solid rgba(0, 0, 0, 0.4); 117 | border-radius: 5px; 118 | color: black; 119 | padding: 0.5rem 1rem; 120 | } 121 | -------------------------------------------------------------------------------- /10.responsive-font-size/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Responsive Font Size Using min(), max() 7 | 8 | 9 | 10 |
11 |

12 | This text is always legible, and is responsive, to a point 13 |

14 |

15 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquam 16 | reprehenderit velit tenetur totam ipsum incidunt beatae? Voluptatem 17 | asperiores maiores ex libero repellendus dolorum ipsa animi corporis 18 | reiciendis. Quibusdam, esse in? 19 |
20 | 21 | This paragraph is gonna shrink in size if width get smaller than a certain point 22 | 23 |

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /10.responsive-font-size/style/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;800&display=swap'); 2 | 3 | :root { 4 | --body-bg: #FFF8E1; 5 | --title-color: #FF8F00; 6 | } 7 | html, body, h1, h2, h3 { 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: border-box; 11 | font-family: "Open Sans", sans-serif; 12 | } 13 | body { 14 | background-color: var(--body-bg); 15 | height: 100vh; 16 | display: flex; 17 | align-items: center; 18 | padding: 0 2rem; 19 | } 20 | .page-title { 21 | margin: 2rem 0; 22 | text-align: center; 23 | } 24 | .card { 25 | width: 800px; 26 | max-width: 100rem; 27 | padding: 1rem; 28 | background-color: white; 29 | box-shadow: 0 2px 8px 1px rgba(0,0,0,0.15); 30 | margin: 0 auto; 31 | } 32 | .card-title { 33 | color: var(--title-color); 34 | text-transform: capitalize; 35 | font-size: max(2.85vw, 1.5rem); 36 | } 37 | p { 38 | font-size: min(2vw, 1rem); 39 | } -------------------------------------------------------------------------------- /11.css-escape-loading-animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CSS Escape Loading Animation 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /11.css-escape-loading-animation/styles/styles.css: -------------------------------------------------------------------------------- 1 | /** Simple Reset - START */ 2 | html { 3 | box-sizing: border-box; 4 | font-size: 16px; 5 | overflow-x: hidden; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, h1, h2, h3, h4, h5, h6, p, ol, ul { 13 | margin: 0; 14 | padding: 0; 15 | font-weight: normal; 16 | } 17 | 18 | img { 19 | max-width: 100%; 20 | height: auto; 21 | } 22 | 23 | /** Animating - START */ 24 | @keyframes moving { 25 | 0%, 26 | 5% { 27 | left: 0; 28 | background-color: #039be5; 29 | } 30 | 95%, 31 | 100% { 32 | left: calc(100% - 3rem); 33 | background-color: #84d6fd; 34 | } 35 | } 36 | 37 | @keyframes box-2-anim { 38 | 0% { 39 | transform: scale(1, 1) rotate(0deg); 40 | bottom: 0; 41 | } 42 | 5% { 43 | bottom: 3rem; 44 | } 45 | 15% { 46 | bottom: 0; 47 | transform: scale(1, 1) rotate(-180deg); 48 | } 49 | 17.001% { 50 | bottom: 0; 51 | transform: translate(0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(-180deg); 52 | } 53 | 25% { 54 | bottom: 0; 55 | transform: scale(1, 1) rotate(-180deg); 56 | } 57 | 66% { 58 | transform: scale(1, 1) rotate(-180deg); 59 | bottom: 0; 60 | } 61 | 71% { 62 | bottom: 3rem; 63 | } 64 | 81% { 65 | bottom: 0; 66 | transform: scale(1, 1) rotate(0deg); 67 | } 68 | 83.001% { 69 | bottom: 0; 70 | transform: translate(-0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(0deg); 71 | } 72 | 91% { 73 | transform: scale(1, 1) rotate(0deg); 74 | } 75 | 100% { 76 | transform: scale(1, 1) rotate(0deg); 77 | } 78 | } 79 | 80 | @keyframes box-3-anim { 81 | 0% { 82 | transform: scale(1, 1) rotate(0deg); 83 | bottom: 0; 84 | } 85 | 5% { 86 | bottom: 3rem; 87 | } 88 | 15% { 89 | bottom: 0; 90 | transform: scale(1, 1) rotate(-180deg); 91 | } 92 | 17.001% { 93 | bottom: 0; 94 | transform: translate(0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(-180deg); 95 | } 96 | 25% { 97 | bottom: 0; 98 | transform: scale(1, 1) rotate(-180deg); 99 | } 100 | 56% { 101 | transform: scale(1, 1) rotate(-180deg); 102 | bottom: 0; 103 | } 104 | 61% { 105 | bottom: 3rem; 106 | } 107 | 71% { 108 | bottom: 0; 109 | transform: scale(1, 1) rotate(0deg); 110 | } 111 | 73.001% { 112 | bottom: 0; 113 | transform: translate(-0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(0deg); 114 | } 115 | 81% { 116 | transform: scale(1, 1) rotate(0deg); 117 | } 118 | 100% { 119 | transform: scale(1, 1) rotate(0deg); 120 | } 121 | } 122 | 123 | @keyframes box-4-anim { 124 | 0% { 125 | transform: scale(1, 1) rotate(0deg); 126 | bottom: 0; 127 | } 128 | 5% { 129 | bottom: 3rem; 130 | } 131 | 15% { 132 | bottom: 0; 133 | transform: scale(1, 1) rotate(-180deg); 134 | } 135 | 17.001% { 136 | bottom: 0; 137 | transform: translate(0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(-180deg); 138 | } 139 | 25% { 140 | bottom: 0; 141 | transform: scale(1, 1) rotate(-180deg); 142 | } 143 | 45% { 144 | transform: scale(1, 1) rotate(-180deg); 145 | bottom: 0; 146 | } 147 | 50% { 148 | bottom: 3rem; 149 | } 150 | 60% { 151 | bottom: 0; 152 | transform: scale(1, 1) rotate(0deg); 153 | } 154 | 62.001% { 155 | bottom: 0; 156 | transform: translate(-0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(0deg); 157 | } 158 | 70% { 159 | transform: scale(1, 1) rotate(0deg); 160 | } 161 | 100% { 162 | transform: scale(1, 1) rotate(0deg); 163 | } 164 | } 165 | 166 | @keyframes box-5-anim { 167 | 0% { 168 | transform: scale(1, 1) rotate(0deg); 169 | bottom: 0; 170 | } 171 | 5% { 172 | bottom: 3rem; 173 | } 174 | 15% { 175 | bottom: 0; 176 | transform: scale(1, 1) rotate(-180deg); 177 | } 178 | 17.001% { 179 | bottom: 0; 180 | transform: translate(0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(-180deg); 181 | } 182 | 25% { 183 | bottom: 0; 184 | transform: scale(1, 1) rotate(-180deg); 185 | } 186 | 33% { 187 | transform: scale(1, 1) rotate(-180deg); 188 | bottom: 0; 189 | } 190 | 38% { 191 | bottom: 3rem; 192 | } 193 | 48% { 194 | bottom: 0; 195 | transform: scale(1, 1) rotate(0deg); 196 | } 197 | 50.001% { 198 | bottom: 0; 199 | transform: translate(-0.9rem, 0.6rem) scale(1.3, 0.6) translateY(0px) rotate(0deg); 200 | } 201 | 58% { 202 | transform: scale(1, 1) rotate(0deg); 203 | } 204 | 100% { 205 | transform: scale(1, 1) rotate(0deg); 206 | } 207 | } 208 | 209 | .loading-container { 210 | display: flex; 211 | justify-content: center; 212 | align-items: center; 213 | height: 90vh; 214 | } 215 | 216 | .box-loading { 217 | width: 21rem; 218 | height: 3rem; 219 | position: relative; 220 | margin: 0 auto; 221 | } 222 | 223 | .box-loading > .box { 224 | position: absolute; 225 | width: 3rem; 226 | height: 3rem; 227 | border-radius: 0.6rem; 228 | background-color: #007bff; 229 | transform-origin: -0.75rem 1.5rem; 230 | box-shadow: 0 0 6px 2px rgba(40, 139, 171, 0.15); 231 | } 232 | 233 | .box-loading > .box:nth-child(1) { 234 | left: 0rem; 235 | background-color: #06abfc; 236 | } 237 | 238 | .box-loading > .box:nth-child(2) { 239 | left: 4.5rem; 240 | background-color: #1fb4fc; 241 | animation: box-2-anim 2s infinite linear; 242 | animation-delay: 0.22s; 243 | } 244 | 245 | .box-loading > .box:nth-child(3) { 246 | left: 9rem; 247 | background-color: #38bcfc; 248 | animation: box-3-anim 2s infinite linear; 249 | animation-delay: 0.33s; 250 | } 251 | 252 | .box-loading > .box:nth-child(4) { 253 | left: 13.5rem; 254 | background-color: #51c5fd; 255 | animation: box-4-anim 2s infinite linear; 256 | animation-delay: 0.44s; 257 | } 258 | 259 | .box-loading > .box:nth-child(5) { 260 | left: 18rem; 261 | background-color: #6acdfd; 262 | animation: box-5-anim 2s infinite linear; 263 | animation-delay: 0.55s; 264 | } 265 | 266 | .box-loading > .box:nth-child(1) { 267 | background-color: #039be5; 268 | animation: moving 1s infinite cubic-bezier(0.6, 0, 0.4, 1) alternate; 269 | } -------------------------------------------------------------------------------- /12.image-slider-3d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Slider 3D 8 | 9 | 10 | 11 |
12 |
    13 |
    14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /12.image-slider-3d/scripts/index.js: -------------------------------------------------------------------------------- 1 | const slider = document.getElementById("slider"); 2 | 3 | // Initial data to add inside the slider 4 | const imagesData = [ 5 | { imageName: "img1.png", alt: "slider-item-1" }, 6 | { imageName: "img2.png", alt: "slider-item-2" }, 7 | { imageName: "img3.png", alt: "slider-item-3" }, 8 | { imageName: "img4.jpg", alt: "slider-item-4" }, 9 | { imageName: "img5.jpg", alt: "slider-item-5" }, 10 | { imageName: "img6.jpg", alt: "slider-item-6" }, 11 | { imageName: "img7.jpg", alt: "slider-item-7" }, 12 | { imageName: "img8.png", alt: "slider-item-8" }, 13 | { imageName: "img9.png", alt: "slider-item-9" }, 14 | { imageName: "img10.png", alt: "slider-item-10" }, 15 | ]; 16 | const imagesCount = imagesData.length; 17 | 18 | // Animation duration per image in the secondes unit 19 | const animationTimePerImage = 3; 20 | 21 | // Using the map method to fill the slider(slider is ul element) 22 | imagesData.forEach((img, index) => { 23 | const liElm = document.createElement("li"); 24 | const animationIndex = index - imagesCount; 25 | 26 | // Adding animation details to slide item 27 | liElm.style.animationName = dynamicAnimationHandler(imagesCount); 28 | liElm.style.animationDuration = `${animationTimePerImage * imagesCount}s`; 29 | liElm.style.animationDelay = `${animationTimePerImage * animationIndex}s`; 30 | 31 | if (index === 1) { 32 | liElm.style.transform = "translateX(240px) translateZ(-240px) rotateY(-45deg)"; 33 | } else if (index === imagesCount - 1) { 34 | liElm.style.transform = "translateX(-240px) translateZ(-240px) rotateY(45deg)"; 35 | } else { 36 | liElm.style.transform = "translateZ(-500px)"; 37 | } 38 | 39 | // Create and append image element to slide item 40 | const imageElement = document.createElement("img"); 41 | imageElement.src = `./static/images/${img.imageName}`; 42 | imageElement.alt = img.alt; 43 | liElm.appendChild(imageElement); 44 | 45 | slider.appendChild(liElm); 46 | }); 47 | 48 | // This function appends a custom stylesheet(including a dynamic keyframe) to the DOM and returns a suitable name 49 | function dynamicAnimationHandler(imagesCount) { 50 | // Each animation has a freezing time and a range of time to start a movement 51 | const freezeTime = 100 / imagesCount; 52 | const movementRange = freezeTime * 0.2; 53 | 54 | const animationName = `animationFor${imagesCount}Images`; 55 | const animationBody = `0%, 56 | ${freezeTime - movementRange}% { 57 | transform: translateX(0); 58 | } 59 | ${freezeTime}%, 60 | ${2 * freezeTime - movementRange}% { 61 | transform: translateX(-240px) translateZ(-240px) rotateY(45deg); 62 | } 63 | ${2 * freezeTime}%, 64 | ${100 - freezeTime - movementRange}% { 65 | transform: translateZ(-500px); 66 | } 67 | ${100 - freezeTime}%, 68 | ${100 - movementRange}% { 69 | transform: translateX(240px) translateZ(-240px) rotateY(-45deg); 70 | } 71 | ${100 - movementRange / 2}% { 72 | transform: translateX(240px) translateZ(-240px) rotateY(-45deg) translateX(160px); 73 | } 74 | 100% { 75 | transform: translateX(0); 76 | }`; 77 | 78 | // Create an empty style element and append it to the DOM 79 | const styleElement = document.createElement("style"); 80 | document.head.appendChild(styleElement); 81 | 82 | // Inserting the animation values to the stylesheet 83 | styleElement.sheet.insertRule(`@keyframes ${animationName} {${animationBody}}`, styleElement.length); 84 | 85 | return animationName; 86 | } 87 | -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img1.png -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img10.png -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img2.png -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img3.png -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img4.jpg -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img5.jpg -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img6.jpg -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img7.jpg -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img8.png -------------------------------------------------------------------------------- /12.image-slider-3d/static/images/img9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behnamazimi/practical-front-end-projects/eb21d5b71f62b2519c949c47bb5db621eb1c52d9/12.image-slider-3d/static/images/img9.png -------------------------------------------------------------------------------- /12.image-slider-3d/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | list-style: none; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | width: 100vw; 13 | height: 100vh; 14 | } 15 | 16 | #wrapper { 17 | display: block; 18 | width: 100%; 19 | height: 10rem; 20 | overflow-x: hidden; 21 | } 22 | 23 | #slider { 24 | position: relative; 25 | width: 75%; 26 | height: 100%; 27 | perspective: 44rem; 28 | transform-style: preserve-3d; 29 | } 30 | 31 | #slider li { 32 | position: absolute; 33 | left: 17.5%; 34 | width: 100%; 35 | height: 100%; 36 | animation-iteration-count: infinite; 37 | border-radius: 0.5rem; 38 | box-shadow: 0 0.25rem 1rem 0.125rem gray; 39 | overflow: hidden; 40 | } 41 | 42 | #slider:hover li { 43 | animation-play-state: paused; 44 | } 45 | 46 | #slider li img { 47 | width: 100%; 48 | height: 100%; 49 | object-fit: cover; 50 | object-position: center; 51 | } 52 | 53 | /* X-Small devices (landscape phones, 426px and up) */ 54 | @media (min-width: 426px) { 55 | #wrapper { 56 | height: 15rem; 57 | } 58 | } 59 | 60 | /* Small devices (landscape phones, 576px and up) */ 61 | @media (min-width: 576px) { 62 | #wrapper { 63 | height: 18rem; 64 | } 65 | } 66 | 67 | /* Medium devices (tablets, 768px and up) */ 68 | @media (min-width: 768px) { 69 | #wrapper { 70 | width: 40rem; 71 | height: 16rem; 72 | overflow-x: unset; 73 | } 74 | 75 | #slider li { 76 | left: 18.5%; 77 | box-shadow: 0 0.5rem 2rem 0.25rem gray; 78 | } 79 | } 80 | 81 | /* Large devices (desktops, 992px and up) */ 82 | @media (min-width: 992px) { 83 | #wrapper { 84 | width: 46rem; 85 | height: 18rem; 86 | } 87 | 88 | #slider li { 89 | left: 19%; 90 | } 91 | } 92 | 93 | /* X-Large devices (large desktops, 1200px and up) */ 94 | @media (min-width: 1200px) { 95 | #wrapper { 96 | width: 50rem; 97 | height: 20rem; 98 | } 99 | 100 | #slider li { 101 | left: 20%; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Practical Front-End Projects with Pure Javascript, CSS, and HTML 2 | 3 | > Welcome to the Practical Front-End Projects repository! 4 | 5 | > This repository contains a range of practical and user-friendly projects for front-end enthusiasts and beginners. The primary objective of this repository is to facilitate learning. 6 | 7 | > All the code samples provided are free and will always remain so. 8 | 9 | ## Table of contents 10 | 1. [Key Features](#key-features) 11 | 2. [Educational Purpose](#Educational-Purpose) 12 | 3. [Projects](#Projects) 13 | - [Custom Video Player](#1-custom-video-player) 14 | - [Lovely Movies](#2-lovely-movies) 15 | - [Note App](#3-note-app) 16 | - [Othello Board Game](#4-othello-board-game) 17 | - [Quiz App](#5-quiz-app) 18 | - [Simple Range Slider](#6-simple-range-slider) 19 | - [Web Chat App](#7-web-chat-app) 20 | - [Canvas Wallpaper](#8-canvas-wallpaper) 21 | - [Split Screen](#9-split-screen) 22 | - [Escape Loading Animation](#10-escape-loading-animation---css) 23 | - [Image Slider 3D](#11-image-slider-3d) 24 | 4. [Running Locally](#Running-Locally) 25 | 5. [New Projects in the Pipeline](#New-Projects-in-the-Pipeline) 26 | 6. [Contribute](#Contribute) 27 | 28 | ## Key Features 29 | 30 | - **No Bundlers**: The projects in this repository have been developed without the use of bundlers like Webpack or Gulp. This allows you to focus on the core front-end concepts and techniques, without getting tangled in the complexities of build tools. 31 | 32 | - **No Third-Party Libraries**: The projects showcased here do not rely on any third-party libraries. By using pure Javascript, CSS, and HTML, you'll gain a deeper understanding of the fundamentals of front-end development. 33 | 34 | - **Modern Browser Support**: The code samples provided in this repository are designed to be compatible with modern browsers. This ensures that you can apply the knowledge gained from these projects to real-world scenarios with confidence. 35 | 36 | ## Educational Purpose 37 | 38 | Please note that the projects included in this repository are intended solely for educational purposes. They serve as valuable learning resources to strengthen your front-end development skills. However, keep in mind that they may not be optimized for production environments or feature advanced functionalities. 39 | 40 | We hope you find these practical front-end projects helpful and enjoyable as you embark on your journey to becoming a proficient front-end developer. Happy coding! 41 | 42 | ## Projects 43 | ### 1. Custom Video Player 44 | 45 | In this project I customized the video controls and designed them manually. Focus on handling the video node and how to implement custom behavior for it. 46 | 47 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/custom-video-player/)** 48 | 49 | **Special topics covered:** 50 | 51 | - Video node controls 52 | - Fullscreen handling 53 | - CSS variables 54 | 55 | ### 2. Lovely Movies 56 | 57 | A simple movie search website. 58 | 59 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/lovely-movies/)** 60 | 61 | **Special topics covered:** 62 | 63 | - Promises and Fetch data with API 64 | - Control DOM behaviors and events 65 | - Usage of `position: static;` in CSS 66 | - Using CSS variables 67 | 68 | ### 3. Note App 69 | 70 | A practical note web app to handle categorized notes. There are notes that can have a category for. you can search in notes and edit or remove those. 71 | 72 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/notes-app/)** 73 | 74 | **Special topics covered:** 75 | 76 | - Object Oriented Programming (OOP) 77 | - Creating DOM elements 78 | - Layouting with CSS grid system 79 | - localStorage usage 80 | 81 | ### 4. Othello Board Game 82 | 83 | Famous strategy game Othello, known as Reversi, implemented in pure Javascript. 84 | 85 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/othello-board-game/)** 86 | 87 | **Special topics covered:** 88 | 89 | - Object Oriented Programming (OOP) 90 | - Othello game strategy 91 | - Creating DOM elements 92 | - Event handling 93 | - Error handling 94 | 95 | ### 5. Quiz App 96 | 97 | Simulating a quiz in web app. 98 | 99 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/quiz-app/)** 100 | 101 | **Special topics covered:** 102 | 103 | - Object Oriented Programming (OOP) 104 | - Creating and handling DOM elements 105 | - CSS animation 106 | 107 | ### 6. Simple Range Slider 108 | 109 | A simple implementation of a small range slider with pure Javascript. 110 | 111 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/simple-range-slider/)** 112 | 113 | **Special topics covered:** 114 | 115 | - Prototypal Object-Oriented Programming 116 | - DOM events handling 117 | - CSS variables 118 | 119 | ### 7. Web Chat App 120 | 121 | This project is a real messaging app that developed with pure javascript without third-party libs. We focused on the Web Components in this project and give it a real component structure. All chats, messages, data are fake and generated with a data-factory. I hope It would be useful. 122 | 123 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/web-chat-app/)** 124 | 125 | **Special topics covered:** 126 | 127 | - Web Components 128 | - Object-Oriented Programming 129 | - Event handling 130 | - DOM controlling 131 | - CSS flex 132 | 133 | ### 8. Canvas Wallpaper 134 | 135 | This is a practical canvas tutorial, a animated wallpaper with circles that moves on it. The code is full documented and easy to read. 136 | 137 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/canvas-wallpaper/)** 138 | 139 | **Special topics covered:** 140 | 141 | - HTML Canvas 142 | - Coding strategies 143 | - Mathematical operations 144 | 145 | ### 9. Split Screen 146 | 147 | A modern design concept to showcase content in a container with two splitted sections which will resize on mouse over 148 | 149 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/split-screen/)** 150 | 151 | **Special topics covered:** 152 | 153 | - CSS 154 | - variable 155 | - relative and absolute positioning 156 | - use of pseudo classes 157 | - JavaScript 158 | - DOM manipulation 159 | 160 | ### 10. Escape Loading Animation - CSS 161 | 162 | Cool loading animation with pure CSS. Animation contains sliding and floating boxes, designed by [Vitaly Silkin](https://dribbble.com/shots/4268258-Evitare-loader). 163 | 164 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/css-escape-loading-animation/)** 165 | 166 | **Special topics covered:** 167 | 168 | - CSS 169 | - Keyframe animations 170 | - Transform and transform origin 171 | 172 | ### 11. Image Slider 3D 173 | 174 | It is an image slider with 3D animation that changes slides automatically based on a duration time. By hovering the mouse on slides, the animation will be paused. 175 | 176 | **[Online Demo](https://behnamazimi.github.io/simple-web-projects/image-slider-3d/)** 177 | 178 | **Special topics covered:** 179 | 180 | - Adding data to the slider by map method 181 | - CSS 182 | - Keyframe animation with 3D effect 183 | - Making dynamic animation keyframe 184 | - Handling dynamic animation timing 185 | 186 | ## Running Locally 187 | 188 | Running the projects locally is a breeze. Just follow these simple steps: 189 | 190 | 1. Clone or download the repository to your local machine. 191 | 2. Open the project directory. 192 | 3. Launch the `index.html` file in your preferred browser. 193 | 194 | Since there are no bundlers used in this repository, all scripts have been directly injected into the HTML files. This allows for a straightforward setup and easy execution of the projects. 195 | 196 | ## New Projects in the Pipeline 197 | 198 | We are working on adding new projects to this repository. Your input and feedback are highly appreciated, as they help us improve the repository and enhance its usefulness. 199 | 200 | We eagerly look forward to your contributions, suggestions, and comments to make this repository a thriving hub of practical front-end projects. Together, we can create an exceptional resource for aspiring developers. 201 | 202 | ## Contribute 203 | 204 | We warmly welcome contributions to this project! As it is specifically designed for beginners and serves educational purposes, we encourage you to contribute by suggesting useful changes or even submitting new projects. Don't hesitate, even if it's your first time contributing to an open-source project! 205 | 206 | Here's a simple guide on how you can get involved: 207 | 208 | 1. **Fork the repository**: Start by forking the repository to your GitHub account. This will create a copy of the project under your account, allowing you to make changes. 209 | 210 | 2. **Make your changes**: Create a new branch in your forked repository and make the desired changes. You can suggest improvements, fix bugs, add new features, or even introduce entirely new projects that align with the educational focus. 211 | 212 | 3. **Submit a pull request**: Once you are satisfied with your changes, submit a pull request on this repository. This will notify the project maintainers about your proposed changes. We will review your submission, provide feedback, and collaborate with you to ensure the quality and relevance of your contribution. 213 | 214 | > Remember, this is a friendly and inclusive community. We appreciate all contributions, regardless of your level of experience. Feel free to ask questions and seek guidance along the way. 215 | 216 | Thank you for your interest in making this repository even better. Together, we can create an invaluable resource for beginners in the world of front-end development! 217 | --------------------------------------------------------------------------------