diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index 94ad7dfe169..85b42474234 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -2342,7 +2342,10 @@ }, "pehm": { "title": "227", "intro": [] }, "cvsw": { "title": "228", "intro": [] }, - "qjws": { "title": "229", "intro": [] }, + "lab-bookmark-manager-app": { + "title": "Build a Bookmark Manager App", + "intro": ["For this lab, you will build a bookmark manager app."] + }, "fgbp": { "title": "230", "intro": [] }, "quiz-local-storage-and-crud": { "title": "Local Storage and CRUD Quiz", diff --git a/client/src/pages/learn/front-end-development/lab-bookmark-manager-app/index.md b/client/src/pages/learn/front-end-development/lab-bookmark-manager-app/index.md new file mode 100644 index 00000000000..68e3799ea0b --- /dev/null +++ b/client/src/pages/learn/front-end-development/lab-bookmark-manager-app/index.md @@ -0,0 +1,9 @@ +--- +title: Introduction to the Build a Bookmark Manager App +block: lab-bookmark-manager-app +superBlock: front-end-development +--- + +## Introduction to the Build a Bookmark Manager App + +For this lab, you will build a bookmark manager app. diff --git a/curriculum/challenges/_meta/lab-bookmark-manager-app/meta.json b/curriculum/challenges/_meta/lab-bookmark-manager-app/meta.json new file mode 100644 index 00000000000..50ed2534417 --- /dev/null +++ b/curriculum/challenges/_meta/lab-bookmark-manager-app/meta.json @@ -0,0 +1,11 @@ +{ + "name": "Build a Bookmark Manager App", + "isUpcomingChange": true, + "usesMultifileEditor": true, + "blockType": "lab", + "dashedName": "lab-bookmark-manager-app", + "order": 229, + "superBlock": "front-end-development", + "challengeOrder": [{ "id": "66def5467aee701733aaf8cc", "title": "Build a Bookmark Manager App" }], + "helpCategory": "JavaScript" +} diff --git a/curriculum/challenges/english/25-front-end-development/lab-bookmark-manager-app/66def5467aee701733aaf8cc.md b/curriculum/challenges/english/25-front-end-development/lab-bookmark-manager-app/66def5467aee701733aaf8cc.md new file mode 100644 index 00000000000..0d9116f596e --- /dev/null +++ b/curriculum/challenges/english/25-front-end-development/lab-bookmark-manager-app/66def5467aee701733aaf8cc.md @@ -0,0 +1,789 @@ +--- +id: 66def5467aee701733aaf8cc +title: Build a Bookmark Manager App +challengeType: 14 +dashedName: build-a-bookmark-manager-app +demoType: onClick +--- + +# --description-- + +For this lab, all the HTML and CSS has been provided to you. You will use JavaScript to complete the Bookmark Manager app so that it can store a collection of bookmarks in the local storage and read data from it. + +**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab. + +**User Stories:** + +1. You should have a `getBookmarks` function that returns the `bookmarks` array stored in the local storage. If the `bookmarks` key has not been set yet, the `getBookmarks` function should return an empty array. +1. The `bookmarks` key stored in the local storage should be an array of bookmark objects. Each bookmark object should have three keys: `name`, `category`, and `url`. +1. You should have a function named `displayOrCloseForm` that toggles the `hidden` class on `#main-section` and `#form-section`. +1. When you click `#add-bookmark-button`, you should update the inner text of `.category-name` to be the value of the selected option from `#category-dropdown` and call `displayOrCloseForm` to display the form section and hide the main section. +1. When you click `#close-form-button`, you should run your function to hide the form section and display the main section. +1. When you click `#add-bookmark-button-form`, you should update the `bookmarks` key stored in the local storage by adding a bookmark object to the end of the array. The object should have `name` set to the value of the `#name` input, `category` set to the value of the selected option from the category dropdown, and `url` set to the value of the `#url` input. +1. Once the `bookmarks` key is updated, you should reset the value of `#name` and `#url` to an empty string before running your function to hide the form section and show the main section. +1. You should have a function named `displayOrHideCategory` that toggles the `hidden` class on `#main-section` and `#bookmark-list-section`. +1. When you click `#view-category-button`, you should update the inner text of `.category-name` to be the value of the selected option from `#category-dropdown`, modify the inner HTML of `#category-list` according to the user stories below, and call the `displayOrHideCategory` function. +1. If none of the bookmarks in local storage have the category, you should set the inner HTML of the `#category-list` to a `p` element with the text `No Bookmarks Found`. +1. If one or more bookmarks in local storage have the selected category you should add a radio button with `id` and `value` attributes set to the bookmark name to the `#category-list`'s inner HTML for each of those bookmarks. +1. Each radio button should have a corresponding label containing an anchor element with the bookmark name and the `href` attribute set to the bookmark URL. +1. When you click `#close-list-button`, you should run your function to hide the `#bookmark-list-section` and display the main section. +1. When you click `#delete-bookmark-button`, you should delete the bookmark corresponding to the selected radio button from the local storage and update the displayed bookmark list. + +# --hints-- + +You should have a `getBookmarks` function. + +```js +assert.isFunction(getBookmarks); +``` + +Your `getBookmarks` function should return an array. + +```js +assert.isArray(getBookmarks()); +``` + +Your `getBookmarks` function should return the `bookmarks` key from `localStorage`. + +```js +try { + setLocalStorage() + const testArr = getBookmarks(); + const expected = [{name: "example1", category: "news", url: "example1.com"}, {name: "example2", category: "entertainment", url: "example2.com"}, {name: "example3", category: "work", url: "example3.com"}, {name: "example4", category: "news", url: "example4.com"}]; + assert.deepEqual(testArr, expected); +} finally { + resetLocalStorage(); +} +``` + +When the `bookmarks` key is not set in `localStorage` or is an empty array, the `getBookmarks` function should return an empty array. + +```js +try { + localStorage.setItem("bookmarks", JSON.stringify([])); + assert.isEmpty(getBookmarks()); + localStorage.removeItem("bookmarks"); + assert.isEmpty(getBookmarks()); +} finally { + resetLocalStorage(); +} +``` + +You should have a function named `displayOrCloseForm`. + +```js +assert.isFunction(displayOrCloseForm); +``` + +Your `displayOrCloseForm` function should toggle the `hidden` class on `#main-section` and `#form-section`. + +```js +const hidden = getHidden("form"); +displayOrCloseForm(); +const hiddenAfterCall = getHidden("form"); +assert.isDefined(hidden, hiddenAfterCall); +assert.notEqual(hidden, hiddenAfterCall); +``` + +When you click `#add-bookmark-button`, you should update the inner text of `.category-name` to be the value of the selected option from `#category-dropdown`. + +```js +addBookMarkButtonTest.dispatchEvent(new Event("click")); +const categoryName = document.querySelector(".category-name").innerText; +assert.strictEqual(categoryName.toLowerCase(), categoryDropdownTest.value.toLowerCase()); +``` + +When you click `#add-bookmark-button`, you should call `displayOrCloseForm` to display the form section and hide the main section. + +```js +assert.strictEqual(getHidden("form"), "form"); +addBookMarkButtonTest.dispatchEvent(new Event("click")); +assert.strictEqual(getHidden("form"), "main"); +``` + +When you click `#close-form-button`, you should call `displayOrCloseForm` to hide the form section and display the main section. + +```js +assert.strictEqual(getHidden("form"), "main"); +closeFormButtonTest.dispatchEvent(new Event("click")); +assert.strictEqual(getHidden("form"), "form"); +``` + +When you click `#add-bookmark-button-form`, you should update the `bookmarks` key stored in the local storage by adding an object to the end of the array. The added object should have `name` set to the value of the `#name` input, `category` set to the value of the selected option from the category dropdown, and `url` set to the value of the `#url` input. + +```js +setLocalStorage(); +try { + bookmarkNameTest.value = "test example5"; + bookmarkURLTest.value = "test example5.com"; + const bookmarkObject = { + name: bookmarkNameTest.value, + category: categoryDropdownTest.value, + url: bookmarkURLTest.value + }; + const expected = [{name: "example1", category: "news", url: "example1.com"}, {name: "example2", category: "entertainment", url: "example2.com"}, {name: "example3", category: "work", url: "example3.com"}, {name: "example4", category: "news", url: "example4.com"}, bookmarkObject]; + addBookmarkButtonFormTest.dispatchEvent(new Event("click")); + assert.deepEqual(getBookmarks(), expected); +} finally { + bookmarkNameTest.value = ""; + bookmarkURLTest.value = ""; + resetLocalStorage(); +} +``` + +When you click `#add-bookmark-button-form`, you should reset the value of `#name` and `#url` to an empty string. + +```js +try { + bookmarkNameTest.value = "test example2"; + bookmarkURLTest.value = "test example2.com"; + addBookmarkButtonFormTest.dispatchEvent(new Event("click")); + assert.strictEqual(bookmarkNameTest.value, ""); + assert.strictEqual(bookmarkURLTest.value, ""); +} finally { + resetLocalStorage() + bookmarkNameTest.value = ""; + bookmarkURLTest.value = ""; +} +``` + +When you click `#add-bookmark-button-form`, you should run `displayOrCloseForm` to hide the form section and show the main section. + +```js +try { + bookmarkNameTest.value = "test example3"; + bookmarkURLTest.value = "test example3.com"; + addBookMarkButtonTest.dispatchEvent(new Event("click")); + assert.strictEqual(getHidden("form"), "main"); + addBookmarkButtonFormTest.dispatchEvent(new Event("click")); + assert.strictEqual(getHidden("form"), "form"); +} finally { + resetLocalStorage() +} +``` + +You should have a function named `displayOrHideCategory`. + +```js +assert.isFunction(displayOrHideCategory); +``` + +Your `displayOrHideCategory` function should toggle the `hidden` class on `#main-section` and `#bookmark-list-section`. + +```js +const hidden = getHidden("bookmark list"); +displayOrHideCategory(); +const hiddenAfterCall = getHidden("bookmark list"); +assert.isDefined(hidden, hiddenAfterCall) +assert.notEqual(hidden, hiddenAfterCall) +``` + +When you click `#view-category-button`, you should update the inner text of `.category-name` to be the value of the selected option from `#category-dropdown`. + +```js +viewCategoryButtonTest.dispatchEvent(new Event("click")); +const categoryName = document.querySelector(".category-name").innerText; +assert.strictEqual(categoryName.toLowerCase(), categoryDropdownTest.value.toLowerCase()); +``` + +When you click `#view-category-button`, you should add a `p` element with the text `No Bookmarks Found` to `#category-list`'s inner HTML if none of the bookmarks in local storage have the selected category. + +```js +try { + setLocalStorage() + categoryDropdownTest.value = "miscellaneous"; + viewCategoryButtonTest.dispatchEvent(new Event("click")); + const categoryList = document.getElementById("category-list").innerHTML; + assert.strictEqual(categoryList, "
No Bookmarks Found
") +} finally { + resetLocalStorage() + clearCategoryList() +} +``` + +When you click `#view-category-button`, you should modify `#category-list`'s inner HTML by adding a radio button having the `id` and `value` attributes set to the bookmark name for each bookmark in the selected category. + +```js +try { + setLocalStorage(); + categoryDropdownTest.value = "news"; + viewCategoryButtonTest.dispatchEvent(new Event("click")); + const bookmarksDisplayed = document.querySelectorAll('#category-list input[type="radio"]'); + assert.lengthOf(bookmarksDisplayed, 2); + assert.strictEqual(bookmarksDisplayed[0].id, "example1"); + assert.strictEqual(bookmarksDisplayed[0].value, "example1"); + assert.strictEqual(bookmarksDisplayed[1].id, "example4"); + assert.strictEqual(bookmarksDisplayed[1].value, "example4"); +} finally { + resetLocalStorage(); +} +``` + +Each radio button added to `#category-list`'s inner HTML should have a corresponding label containing an anchor element with the bookmark name and the `href` attribute set to the bookmark URL. + +```js +setLocalStorage(); +try { + categoryDropdownTest.value = "news"; + viewCategoryButtonTest.dispatchEvent(new Event("click")); + const bookmarkLabelsDisplayed = document.querySelectorAll('#category-list label'); + assert.lengthOf(bookmarkLabelsDisplayed, 2); + assert.strictEqual(bookmarkLabelsDisplayed[0].htmlFor, "example1"); + assert.strictEqual(bookmarkLabelsDisplayed[1].htmlFor, "example4"); +} finally { + resetLocalStorage(); +} +``` + +Each `label` element should contain an anchor element with the bookmark name as text, and the `href` attribute set to the bookmark URL. + +```js +setLocalStorage(); +try { + categoryDropdownTest.value = "news"; + viewCategoryButtonTest.dispatchEvent(new Event("click")); + const bookmarkAnchorsDisplayed = document.querySelectorAll('#category-list label>a'); + assert.lengthOf(bookmarkAnchorsDisplayed, 2); + assert.strictEqual(bookmarkAnchorsDisplayed[0].innerText, "example1"); + assert(bookmarkAnchorsDisplayed[0].href.endsWith("example1.com")); + assert.strictEqual(bookmarkAnchorsDisplayed[1].innerText, "example4"); + assert(bookmarkAnchorsDisplayed[1].href.endsWith("example4.com")); +} finally { + resetLocalStorage(); +} +``` + +When you click `#close-list-button`, you should hide `#bookmark-list-section` and display the main section. + +```js +viewCategoryButtonTest.dispatchEvent(new Event("click")); +assert.strictEqual(getHidden("bookmark list"), "main"); +closeListButtonTest.dispatchEvent(new Event("click")); +assert.strictEqual(getHidden("bookmark list"), "bookmark list"); +``` + +When you click `#delete-bookmark-button`, you should delete the bookmark corresponding to the selected radio button from the local storage and update the displayed bookmark list. + +```js +setLocalStorage(); +try { + categoryDropdownTest.value = "news"; + viewCategoryButtonTest.dispatchEvent(new Event("click")); + const firstRadioButton = document.querySelector('#category-list input[type="radio"]'); + firstRadioButton.checked = true; + deleteBookmarkButtonTest.dispatchEvent(new Event("click")); + let bookmarksDisplayed = document.querySelectorAll('#category-list input[type="radio"]'); + let bookmarkLabelsDisplayed = document.querySelectorAll('#category-list label'); + assert.lengthOf(bookmarksDisplayed, 1); + assert.strictEqual(bookmarksDisplayed[0].id, "example4"); + assert.strictEqual(bookmarksDisplayed[0].value, "example4"); + assert.lengthOf(bookmarkLabelsDisplayed, 1); + assert.strictEqual(bookmarkLabelsDisplayed[0].htmlFor, "example4"); + const expected = [{name: "example2", category: "entertainment", url: "example2.com"}, {name: "example3", category: "work", url: "example3.com"}, {name: "example4", category: "news", url: "example4.com"}]; + assert.deepEqual(getBookmarks(), expected); + +} finally { + resetLocalStorage(); +} +``` + +# --seed-- + +## --after-user-code-- + +```js +const viewCategoryButtonTest = document.getElementById("view-category-button"); +const closeListButtonTest = document.getElementById("close-list-button"); +const closeFormButtonTest = document.getElementById("close-form-button"); +const addBookMarkButtonTest = document.getElementById("add-bookmark-button"); +const addBookmarkButtonFormTest = document.getElementById("add-bookmark-button-form"); +const deleteBookmarkButtonTest = document.getElementById("delete-bookmark-button"); +const bookmarkNameTest = document.getElementById("name"); +const categoryDropdownTest = document.getElementById("category-dropdown"); +const bookmarkURLTest = document.getElementById("url"); +const mainSection = document.querySelector("#main-section"); +const formSection = document.querySelector("#form-section"); +const bookmarkListSection = document.querySelector("#bookmark-list-section"); +const getHidden = (notMain) => { + if (notMain === "form") { + if (mainSection.classList.contains("hidden") && !formSection.classList.contains("hidden")) { + return "main" + } else if (!mainSection.classList.contains("hidden") && formSection.classList.contains("hidden")) { + return notMain; + } + } else if (notMain === "bookmark list") { + if (mainSection.classList.contains("hidden") && !bookmarkListSection.classList.contains("hidden")) { + return "main" + } else if (!mainSection.classList.contains("hidden") && bookmarkListSection.classList.contains("hidden")) { + return notMain; + } + } +} + +const originalLocalStorage = JSON.parse(localStorage.getItem("bookmarks")) || []; +const temp = originalLocalStorage; +function resetLocalStorage() { + localStorage.setItem("bookmarks", JSON.stringify(temp)); +} + +function setLocalStorage() { + localStorage.setItem("bookmarks", JSON.stringify([{name: "example1", category: "news", url: "example1.com"}, {name: "example2", category: "entertainment", url: "example2.com"}, {name: "example3", category: "work", url: "example3.com"}, {name: "example4", category: "news", url: "example4.com"}])); +} + +function clearCategoryList() { + const categoryList = document.getElementById("category-list"); + categoryList.innerHTML = ""; +} +``` + +## --seed-contents-- + +```js + +``` + +```html + + + + + + +No Bookmarks Found
'; + } +} + +viewCategoryButton.addEventListener("click", () => { + fillBookmarkList(); + displayOrHideCategory(); +}) + +closeListButton.addEventListener("click", displayOrHideCategory) + +const deleteBookmark = () => { + const radioBookmarks = document.querySelectorAll('input[type="radio"]'); + for (const radioBookmark of radioBookmarks) { + if (radioBookmark.checked) { + const bookmarks = getBookmarks(); + const indexToRemove = bookmarks.findIndex(i => i.name == radioBookmark.value); + bookmarks.splice(indexToRemove, 1); + localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); + return + } + } +} + +deleteBookmarkButton.addEventListener("click", () => { + deleteBookmark(); + fillBookmarkList(); +}) +``` + +```html + + + + + + +