├── .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 |
20 |
21 |
22 | Result for "Lovely "
23 |
24 |
25 | Next Page
26 |
27 |
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 | `;
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 | `;
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 |
59 |
60 |
61 |
62 |
63 |
64 |
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 |
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 |
32 | Start
33 |
34 |
35 |
36 |
37 |
38 |
39 |
Question 0/0
40 |
00:00
41 |
42 |
43 |
44 |
45 |
66 |
67 |
68 | STOP
70 |
71 | NEXT
73 |
74 |
75 |
76 |
77 |
78 | 0
79 | Your Score
80 |
81 |
82 | Go to Home
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 |