From 4bb9760346fd831e629597bc6467bdfeb65a0e6f Mon Sep 17 00:00:00 2001 From: markitosha Date: Sun, 8 Mar 2026 10:46:46 +0100 Subject: [PATCH 1/8] Updated week1 to include frontend implementation --- .../advanced-javascript/week1/assignment.md | 9 +- .../week1/session-materials/exercises.md | 53 ++-- .../week1/session-materials/index.html | 17 ++ .../listings-demo/index-solution.js | 249 ++++++++++++++++ .../listings-demo/index.html | 77 +++++ .../session-materials/listings-demo/index.js | 247 ++++++++++++++++ .../session-materials/listings-demo/style.css | 276 ++++++++++++++++++ .../mentors-demo/index-solution.js | 155 ++++++++++ .../session-materials/mentors-demo/index.html | 35 +++ .../session-materials/mentors-demo/index.js | 107 +++++++ .../session-materials/mentors-demo/style.css | 131 +++++++++ .../week1/session-materials/style.css | 37 +++ 12 files changed, 1354 insertions(+), 39 deletions(-) create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/index.html create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/style.css create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/style.css diff --git a/courses/frontend/advanced-javascript/week1/assignment.md b/courses/frontend/advanced-javascript/week1/assignment.md index 26cc90d0..f5a049d9 100644 --- a/courses/frontend/advanced-javascript/week1/assignment.md +++ b/courses/frontend/advanced-javascript/week1/assignment.md @@ -4,6 +4,8 @@ The warmup exercises will be a bit abstract. But the in the **hyfBay exercise** the task will be a lot closer to a **real world task**. +**Frontend requirement:** For each task, implement a frontend (HTML + CSS + JavaScript) that shows your solution in the browser. Display the results in the page. You may use one HTML page per exercise or organize multiple sections on a single page—as long as the user can see the output of each task in the UI. + ## 1. Doubling of number Say you would like to write a program that **doubles the odd numbers** in an array and **throws away the even number**. @@ -19,11 +21,10 @@ for (let i = 0; i < numbers.length; i++) { newNumbers[i] = numbers[i] * 2; } } - -console.log("The doubled numbers are", newNumbers); // [2, 6] +// expected result: [2, 6] ``` -Rewrite the above program using `map` and `filter` don't forget to use arrow functions. +Rewrite the above program using `map` and `filter`; don't forget to use arrow functions. Show the result in your page. ## 2. Codewars! @@ -36,7 +37,7 @@ Complete these Katas: ![cinema](https://media.giphy.com/media/l6mBchxYZc7Sw/giphy.gif) -Copy the movies array in the [movies](./session-materials/movies.js) file. Use this array to do the following tasks: +Copy the movies array in the [movies](./session-materials/movies.js) file. Use this array to do the following tasks. **Implement a frontend** so each task’s result is visible in the browser (e.g. sections or cards for each sub-task, with the computed data rendered in the page). 1. Create an array of movies containing the **movies with a short title** (you define what short means) 2. Create an array of movie titles with **long movie titles** diff --git a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md index 99bf0061..241bdce1 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md +++ b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md @@ -1,22 +1,22 @@ # Exercises -Use [generateListings()](./code-inspiration.md#generatelistings) to generate random listings +Work in the [Listings demo](./listings-demo/) in session materials. Open `index.js` and implement the tasks there. Use the **Generate listings (37)** button to get data. ## `forEach` -- Create 37 listings and log out every listing's size +**Task 1** in the demo: implement `generateAndRenderListings` (assign `generateListings(37)` to `currentListings`, then call `renderListings(currentListings)`), and implement `renderListings`: use `prepareListingsView(listings)`, then **`listings.forEach(...)`** to add one card per listing with `renderListingCard(listing)`. Click **Generate listings (37)** to see your result. ## `map` -- Create an array that contains all the 37 listing prices. +**Task 2** in the demo: implement `showPrices`: use **`map`** to create an array of prices from `currentListings`, then display it in the `#prices` element. Click Generate first, then **Show prices**. ## `filter` -Using the 37 listings from the previous tasks, +**Task 3** in the demo (Filters block – three buttons): -- Create an array of cheap listings. You define what "cheap" means. Each item in this array should be of type `object` -- Create an array of expensive listings' prices. Each item in this array should be of type `number` -- Create an array of listings that have parking. Each item in this array should be of type `object` +- **3a. Cheap listings:** implement `showCheapListings`: use **`filter`** to get listings that are "cheap" (you define the condition, e.g. price below a number). Result: array of **objects**. Call `renderListings` with that array. +- **3b. Expensive prices:** implement `showExpensivePrices`: use **`filter`** to get expensive listings, then **`map`** to get an array of their prices. Result: array of **numbers**. Display it in `#expensive-prices`. +- **3c. With parking:** implement `showListingsWithParking`: use **`filter`** to get listings that have "Parkering" in `facilities`. Result: array of **objects**. Call `renderListings` with that array. ## Arrow functions @@ -24,35 +24,18 @@ Rewrite the code above (`forEach`, `map` and `filter`) to arrow functions. ## Listing project -Imagine we have a website like [Danske Bank](https://danskebank.dk/bolig/sogning/liste?sorter=hoejt-forbrug) where a user can search for different parameters. For example: what type the listing should be, the price, size, location etc etc. +**Task 4** in the demo (Advanced filters block): implement **filterListings(listings, filter)** only. Reading the form and building the filter object is already done for you. -### Filter the listings +**Filter object format** (the second argument you receive): an object with only the fields the user set. Possible properties: -Image that a user clicks on a button indicating that they only want listings that are of the type "farm". Let's try and imagine how we would use a function to create this functionality: +| Property | Type | Meaning | +|----------------|-----------|---------| +| `filter.type` | string | Listing type must equal this (e.g. `"Farm"`, `"House"`). | +| `filter.minPrice` | number | Listing price must be ≥ this. | +| `filter.minSize` | number | Listing size (m²) must be ≥ this. | +| `filter.hasGarden` | boolean | If `true`, listing must have a garden. | +| `filter.facilities` | string[] | Listing must have **every** facility in this array (e.g. `["Parkering", "Have"]`). | -```js -const listings = generateListings(20); +If a property is missing from `filter`, do not filter by it. Return only listings that match **every** present property. -const filter = { - type: "farm", -}; - -const farmListings = filterListings(listings, filter); -``` - -Okay, so the `filterListings` function takes a filter which is an `object`. Say the user wants "farm" listings that cost more than 1.5 million. - -```js -const filter2 = { - type: "farm", - minPrice: 1500000, -}; - -const expensiveFarmListings = filterListings(listings, filter2); -``` - -Your job is to create the `filterListings` function. The function should support these filters: type, facilities, minPrice, hasGarden and size. Use arrow functions! - -### Render the listings - -Now create a function called `renderListings`. It should have one parameter: `listings`. When called, the function should render the listings in an html list. How it should be rendered is up to you, but you could take inspiration from [Danske Bank](https://danskebank.dk/bolig/sogning?sorter=hoejt-forbrug) +Click Generate, set the Advanced filters, then **Apply filters**. You can use arrow functions. diff --git a/courses/frontend/advanced-javascript/week1/session-materials/index.html b/courses/frontend/advanced-javascript/week1/session-materials/index.html new file mode 100644 index 00000000..7ecdf793 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/index.html @@ -0,0 +1,17 @@ + + + + + + Advanced JS Week 1 – Session materials + + + +

Week 1 – Array functions & arrow functions

+

Choose a demo to run during the session:

+ + + diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js new file mode 100644 index 00000000..7f65f640 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js @@ -0,0 +1,249 @@ +// ============================================================================= +// DATA & HELPERS +// ============================================================================= + +function randomIntFromInterval(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function generateListings(numberOfListings) { + const listings = []; + const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; + const listingFacilities = ["Parkering", "Elevator", "Altan", "Have", "Husdyr"]; + + for (let i = 0; i < numberOfListings; i++) { + const listing = {}; + const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); + const numberOfFacilities = randomIntFromInterval(1, listingFacilities.length - 1); + const facilities = []; + for (let j = 0; j < numberOfFacilities; j++) { + const randomIndexFacilities = randomIntFromInterval(0, listingFacilities.length - 1); + const randomFacility = listingFacilities[randomIndexFacilities]; + if (!facilities.includes(randomFacility)) { + facilities.push(randomFacility); + } + } + listing.type = listingType[randomTypeIndex]; + listing.facilities = facilities; + listing.price = randomIntFromInterval(1, 10000); + listing.hasGarden = Boolean(randomIntFromInterval(0, 1)); + listing.size = randomIntFromInterval(12, 1000); + listing.img = `https://loremflickr.com/200/200/${listing.type}`; + listings.push(listing); + } + return listings; +} + +// ============================================================================= +// RENDER +// ============================================================================= + +/** + * Build one card element for a single listing. Helper – already implemented. + * @param {Object} listing - One listing object (type, price, size, hasGarden, facilities, img) + * @returns {HTMLElement} A div.card element + */ +function renderListingCard(listing) { + const card = document.createElement("div"); + card.className = "card"; + const badgeClass = function (f) { + return f === "Have" ? " garden" : ""; + }; + const badgesHtml = listing.facilities + .map(function (f) { + return '' + f + ""; + }) + .join(""); + card.innerHTML = + '
' + + (listing.img ? '' : listing.type) + + "
" + + '
' + + '
' + listing.type + "
" + + '
' + listing.price.toLocaleString() + " kr
" + + '
' + listing.size + " m²" + (listing.hasGarden ? " · Garden" : "") + "
" + + '
' + badgesHtml + "
" + + "
"; + return card; +} + +/** + * Update count and empty state, clear the cards container. Returns the #cards container if there + * are listings to render, or null if not (so you can return early). + * Use this at the start of renderListings. + * @param {Array} listings + * @returns {HTMLElement|null} The #cards container, or null when listings.length === 0 + */ +function prepareListingsView(listings) { + const container = document.getElementById("cards"); + const countEl = document.getElementById("count"); + const emptyEl = document.getElementById("empty"); + + countEl.textContent = listings.length + " listing" + (listings.length !== 1 ? "s" : ""); + + if (listings.length === 0) { + container.innerHTML = ""; + if (emptyEl) { + emptyEl.textContent = "No listings."; + emptyEl.style.display = "block"; + } + return null; + } + + if (emptyEl) { + emptyEl.style.display = "none"; + } + + container.innerHTML = ""; + + return container; +} + +/** + * Render the listings array into #cards. Uses prepareListingsView, then forEach to append cards. + */ +function renderListings(listings) { + const container = prepareListingsView(listings); + if (!container) { + return; + } + + listings.forEach(function (listing) { + const card = renderListingCard(listing); + container.appendChild(card); + }); +} + +// ============================================================================= +// STATE +// ============================================================================= + +/** Last generated listings; used by Show prices (Task 2) and filter (Task 3). */ +let currentListings = []; + +function generateAndRenderListings() { + currentListings = generateListings(37); + renderListings(currentListings); +} + +/** + * Show prices using map: create array of prices from currentListings, display in #prices. + */ +function showPrices() { + const prices = currentListings.map(function (listing) { + return listing.price; + }); + const pricesEl = document.getElementById("prices"); + pricesEl.textContent = prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; +} + +/** + * Show cheap listings (price < 2000). Filter, then renderListings. Result: array of objects. + */ +function showCheapListings() { + const cheap = currentListings.filter(function (listing) { + return listing.price < 2000; + }); + renderListings(cheap); +} + +/** + * Show expensive listings' prices only. Filter expensive, map to price. Result: array of numbers. Display in #expensive-prices. + */ +function showExpensivePrices() { + const expensiveListings = currentListings.filter(function (listing) { + return listing.price > 5000; + }); + const prices = expensiveListings.map(function (listing) { + return listing.price; + }); + const el = document.getElementById("expensive-prices"); + el.textContent = prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; +} + +/** + * Show only listings that have parking (Parkering in facilities). Filter, then renderListings. Result: array of objects. + */ +function showListingsWithParking() { + const withParking = currentListings.filter(function (listing) { + return listing.facilities.includes("Parkering"); + }); + renderListings(withParking); +} + +// ============================================================================= +// ADVANCED FILTERS (Task 4 – Listing project) +// ============================================================================= + +/** + * Read Advanced filters form and return a filter object. Only includes set values. + * Filter object format: { type?, minPrice?, minSize?, hasGarden?, facilities? } – see filterListings. + */ +function getFilterFromForm() { + const type = document.getElementById("advType").value || undefined; + const minPriceRaw = document.getElementById("advMinPrice").value; + const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); + const minSizeRaw = document.getElementById("advMinSize").value; + const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); + const hasGarden = document.getElementById("advHasGarden").checked || undefined; + const facilityCheckboxes = document.querySelectorAll('input[name="advFacility"]:checked'); + const facilities = Array.from(facilityCheckboxes).map(function (cb) { + return cb.value; + }); + + const filter = {}; + if (type) { + filter.type = type; + } + if (minPrice != null && !isNaN(minPrice)) { + filter.minPrice = minPrice; + } + if (minSize != null && !isNaN(minSize)) { + filter.minSize = minSize; + } + if (hasGarden) { + filter.hasGarden = true; + } + if (facilities.length > 0) { + filter.facilities = facilities; + } + + return filter; +} + +/** + * Return only listings that match every property in filter. + * Filter object: type (string), minPrice (number), minSize (number), hasGarden (boolean), facilities (string[]). + * Only present keys are applied; listing must match all present criteria. + */ +function filterListings(listings, filter) { + return listings.filter(function (listing) { + if (filter.type && listing.type !== filter.type) { + return false; + } + if (filter.minPrice != null && listing.price < filter.minPrice) { + return false; + } + if (filter.minSize != null && listing.size < filter.minSize) { + return false; + } + if (filter.hasGarden && !listing.hasGarden) { + return false; + } + if (filter.facilities && filter.facilities.length > 0) { + for (let i = 0; i < filter.facilities.length; i++) { + if (!listing.facilities.includes(filter.facilities[i])) { + return false; + } + } + } + return true; + }); +} + +function applyAdvancedFilters() { + const filter = getFilterFromForm(); + const filtered = filterListings(currentListings, filter); + + renderListings(filtered); +} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html new file mode 100644 index 00000000..f9acbb7a --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html @@ -0,0 +1,77 @@ + + + + + + Listings demo – Generate and render + + + +

Listings demo – Generate and render

+ +
+ + + No listings yet. Click Generate. +
+ +
+ Filters (Task 3): + + + +
+ +
+ Advanced filters (Task 4 – Listing project): +
+ + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+ +
+ +
+ +
+ Click “Generate listings” to create 37 random listings and render them as cards. +
+
+
+
+ + + + diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js new file mode 100644 index 00000000..7e62c61f --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js @@ -0,0 +1,247 @@ +// ============================================================================= +// DATA & HELPERS +// ============================================================================= + +function randomIntFromInterval(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function generateListings(numberOfListings) { + const listings = []; + const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; + const listingFacilities = ["Parkering", "Elevator", "Altan", "Have", "Husdyr"]; + + for (let i = 0; i < numberOfListings; i++) { + const listing = {}; + const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); + const numberOfFacilities = randomIntFromInterval(1, listingFacilities.length - 1); + const facilities = []; + for (let j = 0; j < numberOfFacilities; j++) { + const randomIndexFacilities = randomIntFromInterval(0, listingFacilities.length - 1); + const randomFacility = listingFacilities[randomIndexFacilities]; + if (!facilities.includes(randomFacility)) { + facilities.push(randomFacility); + } + } + listing.type = listingType[randomTypeIndex]; + listing.facilities = facilities; + listing.price = randomIntFromInterval(1, 10000); + listing.hasGarden = Boolean(randomIntFromInterval(0, 1)); + listing.size = randomIntFromInterval(12, 1000); + listing.img = `https://loremflickr.com/200/200/${listing.type}`; + listings.push(listing); + } + return listings; +} + +// ============================================================================= +// RENDER +// ============================================================================= + +/** + * Build one card element for a single listing. Helper – already implemented. + * @param {Object} listing - One listing object (type, price, size, hasGarden, facilities, img) + * @returns {HTMLElement} A div.card element + */ +function renderListingCard(listing) { + const card = document.createElement("div"); + card.className = "card"; + const badgeClass = function (f) { + return f === "Have" ? " garden" : ""; + }; + const badgesHtml = listing.facilities + .map(function (f) { + return '' + f + ""; + }) + .join(""); + card.innerHTML = + '
' + + (listing.img ? '' : listing.type) + + "
" + + '
' + + '
' + listing.type + "
" + + '
' + listing.price.toLocaleString() + " kr
" + + '
' + listing.size + " m²" + (listing.hasGarden ? " · Garden" : "") + "
" + + '
' + badgesHtml + "
" + + "
"; + return card; +} + +/** + * Update count and empty state, clear the cards container. Returns the #cards container if there + * are listings to render, or null if not (so you can return early). + * Use this at the start of renderListings. + * @param {Array} listings + * @returns {HTMLElement|null} The #cards container, or null when listings.length === 0 + */ +function prepareListingsView(listings) { + const container = document.getElementById("cards"); + const countEl = document.getElementById("count"); + const emptyEl = document.getElementById("empty"); + + countEl.textContent = listings.length + " listing" + (listings.length !== 1 ? "s" : ""); + + if (listings.length === 0) { + container.innerHTML = ""; + + if (emptyEl) { + emptyEl.textContent = "No listings."; + emptyEl.style.display = "block"; + } + + return null; + } + + if (emptyEl) { + emptyEl.style.display = "none"; + } + + container.innerHTML = ""; + + return container; +} + +/* + * Task 1. Implement two things: + * + * 1. generateAndRenderListings: assign generateListings(37) to currentListings (below), then + * call renderListings(currentListings). (currentListings is used later by Task 2.) + * + * 2. renderListings: use prepareListingsView(listings) first; if it returns null, return. + * Otherwise use the returned container: call listings.forEach(function (listing) { ... }) + * and append renderListingCard(listing) to the container. + */ +function renderListings(listings) { + const container = prepareListingsView(listings); + + if (!container) { + return; + } + + /* + * Task 1.2: add listings.forEach(...) here and append renderListingCard(listing) to container + */ +} + +// ============================================================================= +// STATE +// ============================================================================= + +/** Last generated listings; used by Show prices (Task 2) and filter (Task 3). */ +let currentListings = []; + +function generateAndRenderListings() { + /* + * Task 1.1: assign generateListings(37) to currentListings, then call renderListings(currentListings) + */ +} + +/* + * Task 2. Implement showPrices using map: create an array of prices from currentListings + * (one price per listing), then display that array in the #prices element (e.g. as text or a list). + */ +function showPrices() { + /* + * Task 2: use map to get an array of prices from currentListings, then display it in #prices + */ +} + +/* + * Task 3. Implement three filter functions: + * + * 3a. showCheapListings: use filter to get listings that are "cheap" (you define the condition, e.g. price below a number). + * Result: array of objects. Call renderListings with that array. + * + * 3b. showExpensivePrices: use filter to get expensive listings, then use map to get an array of their prices (numbers). + * Result: array of numbers. Display it in the #expensive-prices element. + * + * 3c. showListingsWithParking: use filter to get listings that have "Parkering" in facilities. + * Result: array of objects. Call renderListings with that array. + */ +function showCheapListings() { + /* + * Task 3a: use filter for cheap listings (e.g. by price), then renderListings(...) + */ +} + +function showExpensivePrices() { + /* + * Task 3b: use filter for expensive listings, then map to prices (numbers), display in #expensive-prices + */ +} + +function showListingsWithParking() { + /* + * Task 3c: use filter for listings with parking, then renderListings(...) + */ +} + +// ============================================================================= +// ADVANCED FILTERS (Task 4 – Listing project) +// ============================================================================= + +/** + * Read Advanced filters form and return a filter object. Only includes set values. + * (Already implemented – you only implement filterListings below.) + */ +function getFilterFromForm() { + const type = document.getElementById("advType").value || undefined; + const minPriceRaw = document.getElementById("advMinPrice").value; + const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); + const minSizeRaw = document.getElementById("advMinSize").value; + const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); + const hasGarden = document.getElementById("advHasGarden").checked || undefined; + const facilityCheckboxes = document.querySelectorAll('input[name="advFacility"]:checked'); + const facilities = Array.from(facilityCheckboxes).map(function (cb) { + return cb.value; + }); + + const filter = {}; + if (type) { + filter.type = type; + } + if (minPrice != null && !isNaN(minPrice)) { + filter.minPrice = minPrice; + } + if (minSize != null && !isNaN(minSize)) { + filter.minSize = minSize; + } + if (hasGarden) { + filter.hasGarden = true; + } + if (facilities.length > 0) { + filter.facilities = facilities; + } + + return filter; +} + +/* + * Task 4. Implement filterListings(listings, filter). + * + * The filter object (built from the Advanced filters form) can have these properties. Omit a property + * if the user left the field empty or unchecked – in that case, do not filter by it. + * + * filter.type {string} – listing.type must equal this (e.g. "Farm", "House"). + * filter.minPrice {number} – listing.price must be >= filter.minPrice. + * filter.minSize {number} – listing.size must be >= filter.minSize (m²). + * filter.hasGarden {boolean} – if true, listing.hasGarden must be true. + * filter.facilities {string[]} – listing.facilities must include every string in this array + * (e.g. ["Parkering", "Have"] means the listing must have both). + * + * Return only listings that match every present filter property. + */ +function filterListings(listings, filter) { + /* + * Task 4: return listings that match every key in filter (see comment above for format) + */ + return listings; +} + +function applyAdvancedFilters() { + const filter = getFilterFromForm(); + + const filtered = filterListings(currentListings, filter); + + renderListings(filtered); +} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css new file mode 100644 index 00000000..d4481c2c --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css @@ -0,0 +1,276 @@ +/* ============================================================================= + Listings demo – Base & layout + ============================================================================= */ + +* { + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", system-ui, sans-serif; + margin: 0; + padding: 1.5rem; + background: #f0f2f5; + color: #1a1a1a; +} + +h1 { + margin-top: 0; + font-size: 1.5rem; +} + +/* ============================================================================= + Toolbar + ============================================================================= */ + +.toolbar { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + margin-bottom: 1.5rem; + padding: 1rem; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.count { + font-size: 0.9rem; + color: #666; + margin-left: auto; + align-self: center; +} + +/* ============================================================================= + Buttons + ============================================================================= */ + +button { + padding: 0.6rem 1.2rem; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; +} + +.btn-generate { + background: #2563eb; + color: #fff; +} + +.btn-generate:hover { + background: #1d4ed8; +} + +.btn-prices { + background: #e0e7ff; + color: #3730a3; +} + +.btn-prices:hover { + background: #c7d2fe; +} + +.btn-filter { + background: #d1fae5; + color: #065f46; +} + +.btn-filter:hover { + background: #a7f3d0; +} + +.btn-apply { + background: #7c3aed; + color: #fff; +} + +.btn-apply:hover { + background: #6d28d9; +} + +/* ============================================================================= + Filters block (Task 3) + ============================================================================= */ + +.filters { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + margin-bottom: 1rem; + padding: 0.75rem 1rem; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.filters-label { + font-size: 0.9rem; + font-weight: 600; + color: #555; +} + +/* ============================================================================= + Advanced filters form (Task 4) + ============================================================================= */ + +.advanced-filters { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + margin-bottom: 1rem; + padding: 1rem; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + border: 1px solid #e5e7eb; +} + +.advanced-filters .filters-label { + width: 100%; + margin-bottom: 0.25rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.field label { + font-size: 0.85rem; + font-weight: 600; + color: #555; +} + +.facilities { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.facilities label { + display: flex; + align-items: center; + gap: 0.35rem; + font-weight: normal; + font-size: 0.9rem; + cursor: pointer; +} + +select, +input[type="number"] { + padding: 0.5rem 0.75rem; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 0.95rem; +} + +/* ============================================================================= + Cards grid + ============================================================================= */ + +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1rem; +} + +.card { + background: #fff; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: transform 0.2s, box-shadow 0.2s; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} + +.card-image { + height: 140px; + background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%); + display: flex; + align-items: center; + justify-content: center; + color: #64748b; + font-size: 0.85rem; +} + +.card-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.card-body { + padding: 1rem; +} + +.card-type { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin-bottom: 0.25rem; +} + +.card-price { + font-size: 1.25rem; + font-weight: 700; + color: #1e293b; +} + +.card-meta { + font-size: 0.9rem; + color: #64748b; + margin-top: 0.5rem; +} + +.card-facilities { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; +} + +.badge { + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + background: #e0e7ff; + color: #3730a3; + border-radius: 4px; +} + +.badge.garden { + background: #d1fae5; + color: #065f46; +} + +/* ============================================================================= + Empty state & prices output + ============================================================================= */ + +.empty { + text-align: center; + padding: 3rem; + color: #64748b; + background: #fff; + border-radius: 10px; +} + +.prices { + margin: 1rem 0; + padding: 1rem; + background: #fff; + border-radius: 8px; + font-size: 0.9rem; + color: #374151; +} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js new file mode 100644 index 00000000..a4491fc9 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js @@ -0,0 +1,155 @@ +// ============================================================================= +// DATA +// ============================================================================= + +const mentors = [ + { name: "Abed Sujan", subjects: ["JS", "HTML", "CSS", "NODEJS"], yearOfExperience: 4 }, + { name: "Ahmed Magdy", subjects: ["JS", "Database", "CSS"], yearOfExperience: 1 }, + { name: "Alicia Gonzales", subjects: ["DB", "HTML", "NODEJS"], yearOfExperience: 8 }, + { name: "allan Thraen", subjects: ["REACT", "HTML", "CSS"], yearOfExperience: 3 }, + { name: "Anders Ravn", subjects: ["JS", "HTML", "NODEJS"], yearOfExperience: 2 }, + { name: "Daniel Fernandes", subjects: ["Database", "HTML", "CSS"], yearOfExperience: 9 }, +]; + +// ============================================================================= +// HOMEMADE ARRAY FUNCTIONS (our own implementations) +// ============================================================================= + +function forEachHomemade(array, functionToExecute) { + for (let i = 0; i < array.length; i++) { + const currentItem = array[i]; + functionToExecute(currentItem, i); + } +} + +function mapHomemade(array, functionToExecute) { + const mappedArray = []; + + for (let i = 0; i < array.length; i++) { + const currentItem = array[i]; + const newItem = functionToExecute(currentItem, i); + + mappedArray.push(newItem); + } + + return mappedArray; +} + +function filterHomemade(array, functionToExecute) { + const filteredArray = []; + + for (let i = 0; i < array.length; i++) { + const currentItem = array[i]; + const shouldKeepItemInNewArray = functionToExecute(currentItem, i); + + if (shouldKeepItemInNewArray) { + filteredArray.push(currentItem); + } + } + + return filteredArray; +} + +// ============================================================================= +// RENDER HELPERS +// ============================================================================= + +function renderMentorCard(mentor) { + const div = document.createElement("div"); + div.className = "card"; + + const subjectSpans = mapHomemade(mentor.subjects, function (subject) { + return `${subject}`; + }); + const subjectsHtml = subjectSpans.join(""); + + div.innerHTML = ` +
${mentor.name}
+
${mentor.yearOfExperience} years experience
+
${subjectsHtml}
+ `; + + return div; +} + +function renderCards(mentorsToShow) { + const container = document.getElementById("result"); + container.innerHTML = ""; + + const wrapper = document.createElement("div"); + wrapper.className = "cards"; + + forEachHomemade(mentorsToShow, function (mentor) { + wrapper.appendChild(renderMentorCard(mentor)); + }); + + container.appendChild(wrapper); +} + +function renderNamesList(names) { + const container = document.getElementById("result"); + container.innerHTML = ""; + + const div = document.createElement("div"); + div.className = `names-list${names.length === 0 ? " empty" : ""}`; + div.textContent = names.length === 0 ? "No names." : names.join(", "); + + container.appendChild(div); +} + +// ============================================================================= +// DEMO ACTIONS (forEach, map, filter) +// ============================================================================= + +function showAllMentors() { + document.getElementById("resultLabel").textContent = "Result: forEach – all mentors"; + + renderCards(mentors); +} + +function showNamesOnly() { + document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; + + const mentorNames = mapHomemade(mentors, function (mentor) { + return mentor.name; + }); + + renderNamesList(mentorNames); +} + +function showExperienced() { + document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; + + const experiencedMentors = filterHomemade(mentors, function (mentor) { + return mentor.yearOfExperience > 7; + }); + + renderCards(experiencedMentors); +} + +function showNamesStartingWithA() { + document.getElementById("resultLabel").textContent = 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + + const mentorsThatStartWithA = filterHomemade(mentors, function (mentor) { + return mentor.name[0] === "A"; + }); + + renderCards(mentorsThatStartWithA); +} + +// ============================================================================= +// BUTTON ACTIVE STATE (called from onclick in HTML) +// ============================================================================= + +function setActive(clickedBtn) { + const buttons = document.querySelectorAll(".actions button"); + + for (let i = 0; i < buttons.length; i++) { + buttons[i].classList.remove("active"); + } + + clickedBtn.classList.add("active"); +} + +// Default: show all mentors on load +showAllMentors(); diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html new file mode 100644 index 00000000..3ff2f8b0 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html @@ -0,0 +1,35 @@ + + + + + + Mentors demo – forEach, map, filter + + + +

Mentors demo – forEach, map, filter

+

+ Use the buttons to see different array operations. Results are rendered below (no console). +

+ +
+ + + + +
+ +
Result
+
+ + + + diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js new file mode 100644 index 00000000..498e5da2 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js @@ -0,0 +1,107 @@ +// ============================================================================= +// DATA +// ============================================================================= + +const mentors = [ + { name: "Abed Sujan", subjects: ["JS", "HTML", "CSS", "NODEJS"], yearOfExperience: 4 }, + { name: "Ahmed Magdy", subjects: ["JS", "Database", "CSS"], yearOfExperience: 1 }, + { name: "Alicia Gonzales", subjects: ["DB", "HTML", "NODEJS"], yearOfExperience: 8 }, + { name: "allan Thraen", subjects: ["REACT", "HTML", "CSS"], yearOfExperience: 3 }, + { name: "Anders Ravn", subjects: ["JS", "HTML", "NODEJS"], yearOfExperience: 2 }, + { name: "Daniel Fernandes", subjects: ["Database", "HTML", "CSS"], yearOfExperience: 9 }, +]; + +// ============================================================================= +// HOMEMADE ARRAY FUNCTIONS (our own implementations) +// ============================================================================= + +/** + * Executes function for each item in the array, NO RETURN! + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + */ +function forEachHomemade(array, functionToExecute) { + +} + +/** + * Changes/transforms the items in the array. Returns a new array with the transformed items. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + */ +function mapHomemade(array, functionToExecute) { + +} + +/** + * Changes the number of items in the array based on a condition. Returns a new array with only the items that satisfy the condition. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + */ +function filterHomemade(array, functionToExecute) { + +} + +// ============================================================================= +// RENDER HELPERS +// ============================================================================= + +function renderMentorCard(mentor) { + +} + +function renderCards(mentorsToShow) { + +} + +function renderNamesList(names) { + +} + +// ============================================================================= +// DEMO ACTIONS (forEach, map, filter) +// ============================================================================= + +function showAllMentors() { + document.getElementById("resultLabel").textContent = "Result: forEach – all mentors"; + + renderCards(mentors); +} + +function showNamesOnly() { + document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; + + const mentorNames = []; + + renderNamesList(mentorNames); +} + +function showExperienced() { + document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; + + const experiencedMentors = mentors; + + renderCards(experiencedMentors); +} + +function showNamesStartingWithA() { + document.getElementById("resultLabel").textContent = 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + + const mentorsThatStartWithA = mentors; + + renderCards(mentorsThatStartWithA); +} + +// ============================================================================= +// BUTTON ACTIVE STATE (called from onclick in HTML) +// ============================================================================= + +function setActive(clickedBtn) { + const buttons = document.querySelectorAll(".actions button"); + + for (let i = 0; i < buttons.length; i++) { + buttons[i].classList.remove("active"); + } + + clickedBtn.classList.add("active"); +} + +// Default: show all mentors on load +showAllMentors(); diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/style.css b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/style.css new file mode 100644 index 00000000..01e82e3b --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/style.css @@ -0,0 +1,131 @@ +/* ============================================================================= + Mentors demo – Base & layout + ============================================================================= */ + +* { + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", system-ui, sans-serif; + margin: 0; + padding: 1.5rem; + background: #f8fafc; + color: #1e293b; +} + +h1 { + margin-top: 0; + font-size: 1.5rem; +} + +.intro { + color: #64748b; + margin-bottom: 1.5rem; + font-size: 0.95rem; +} + +/* ============================================================================= + Action buttons + ============================================================================= */ + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1.5rem; +} + +.actions button { + padding: 0.6rem 1rem; + border: 1px solid #cbd5e1; + border-radius: 8px; + background: #fff; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + color: #475569; +} + +.actions button:hover { + background: #f1f5f9; + border-color: #94a3b8; + color: #334155; +} + +.actions button.active { + background: #334155; + border-color: #334155; + color: #fff; +} + +/* ============================================================================= + Result area + ============================================================================= */ + +.result-label { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin-bottom: 0.5rem; +} + +/* ============================================================================= + Mentor cards + ============================================================================= */ + +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.card { + background: #fff; + border-radius: 10px; + padding: 1.25rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + border: 1px solid #e2e8f0; +} + +.card-name { + font-size: 1.1rem; + font-weight: 700; + color: #0f172a; + margin-bottom: 0.35rem; +} + +.card-experience { + font-size: 0.85rem; + color: #64748b; + margin-bottom: 0.75rem; +} + +.card-subjects { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.card-subjects span { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + background: #e0f2fe; + color: #0369a1; + border-radius: 4px; +} + +.names-list { + background: #fff; + border-radius: 10px; + padding: 1rem 1.25rem; + border: 1px solid #e2e8f0; + font-size: 1rem; + line-height: 1.8; + color: #334155; +} + +.names-list.empty { + color: #94a3b8; +} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/style.css b/courses/frontend/advanced-javascript/week1/session-materials/style.css new file mode 100644 index 00000000..6204ef44 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/style.css @@ -0,0 +1,37 @@ +/* ============================================================================= + Session materials – Landing + ============================================================================= */ + +body { + font-family: system-ui, sans-serif; + padding: 2rem; + max-width: 600px; +} + +h1 { + font-size: 1.25rem; +} + +ul { + list-style: none; + padding: 0; +} + +li { + margin-bottom: 0.75rem; +} + +a { + color: #2563eb; + font-weight: 500; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +p { + color: #64748b; + font-size: 0.9rem; +} From 5c5024fc135b77bf79c47c8dfec86624f3c12dd7 Mon Sep 17 00:00:00 2001 From: markitosha Date: Sun, 8 Mar 2026 10:47:43 +0100 Subject: [PATCH 2/8] Refactor HTML structure and improve code formatting for readability --- .../week1/session-materials/exercises.md | 12 +-- .../week1/session-materials/index.html | 36 +++++---- .../listings-demo/index-solution.js | 56 ++++++++++---- .../listings-demo/index.html | 76 +++++++++++++++---- .../session-materials/listings-demo/index.js | 52 ++++++++++--- .../session-materials/listings-demo/style.css | 4 +- .../mentors-demo/index-solution.js | 48 +++++++++--- .../session-materials/mentors-demo/index.html | 16 +++- .../session-materials/mentors-demo/index.js | 72 +++++++++++------- package-lock.json | 2 +- 10 files changed, 269 insertions(+), 105 deletions(-) diff --git a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md index 241bdce1..6a8d5458 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md +++ b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md @@ -28,12 +28,12 @@ Rewrite the code above (`forEach`, `map` and `filter`) to arrow functions. **Filter object format** (the second argument you receive): an object with only the fields the user set. Possible properties: -| Property | Type | Meaning | -|----------------|-----------|---------| -| `filter.type` | string | Listing type must equal this (e.g. `"Farm"`, `"House"`). | -| `filter.minPrice` | number | Listing price must be ≥ this. | -| `filter.minSize` | number | Listing size (m²) must be ≥ this. | -| `filter.hasGarden` | boolean | If `true`, listing must have a garden. | +| Property | Type | Meaning | +| ------------------- | -------- | ---------------------------------------------------------------------------------- | +| `filter.type` | string | Listing type must equal this (e.g. `"Farm"`, `"House"`). | +| `filter.minPrice` | number | Listing price must be ≥ this. | +| `filter.minSize` | number | Listing size (m²) must be ≥ this. | +| `filter.hasGarden` | boolean | If `true`, listing must have a garden. | | `filter.facilities` | string[] | Listing must have **every** facility in this array (e.g. `["Parkering", "Have"]`). | If a property is missing from `filter`, do not filter by it. Return only listings that match **every** present property. diff --git a/courses/frontend/advanced-javascript/week1/session-materials/index.html b/courses/frontend/advanced-javascript/week1/session-materials/index.html index 7ecdf793..f4a2fce3 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/index.html +++ b/courses/frontend/advanced-javascript/week1/session-materials/index.html @@ -1,17 +1,23 @@ - + - - - - Advanced JS Week 1 – Session materials - - - -

Week 1 – Array functions & arrow functions

-

Choose a demo to run during the session:

- - + + + + Advanced JS Week 1 – Session materials + + + +

Week 1 – Array functions & arrow functions

+

Choose a demo to run during the session:

+ + diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js index 7f65f640..33d75812 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js @@ -9,15 +9,27 @@ function randomIntFromInterval(min, max) { function generateListings(numberOfListings) { const listings = []; const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; - const listingFacilities = ["Parkering", "Elevator", "Altan", "Have", "Husdyr"]; + const listingFacilities = [ + "Parkering", + "Elevator", + "Altan", + "Have", + "Husdyr", + ]; for (let i = 0; i < numberOfListings; i++) { const listing = {}; const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); - const numberOfFacilities = randomIntFromInterval(1, listingFacilities.length - 1); + const numberOfFacilities = randomIntFromInterval( + 1, + listingFacilities.length - 1, + ); const facilities = []; for (let j = 0; j < numberOfFacilities; j++) { - const randomIndexFacilities = randomIntFromInterval(0, listingFacilities.length - 1); + const randomIndexFacilities = randomIntFromInterval( + 0, + listingFacilities.length - 1, + ); const randomFacility = listingFacilities[randomIndexFacilities]; if (!facilities.includes(randomFacility)) { facilities.push(randomFacility); @@ -56,13 +68,25 @@ function renderListingCard(listing) { .join(""); card.innerHTML = '
' + - (listing.img ? '' : listing.type) + + (listing.img + ? '' + : listing.type) + "
" + '
' + - '
' + listing.type + "
" + - '
' + listing.price.toLocaleString() + " kr
" + - '
' + listing.size + " m²" + (listing.hasGarden ? " · Garden" : "") + "
" + - '
' + badgesHtml + "
" + + '
' + + listing.type + + "
" + + '
' + + listing.price.toLocaleString() + + " kr
" + + '
' + + listing.size + + " m²" + + (listing.hasGarden ? " · Garden" : "") + + "
" + + '
' + + badgesHtml + + "
" + "
"; return card; } @@ -79,7 +103,8 @@ function prepareListingsView(listings) { const countEl = document.getElementById("count"); const emptyEl = document.getElementById("empty"); - countEl.textContent = listings.length + " listing" + (listings.length !== 1 ? "s" : ""); + countEl.textContent = + listings.length + " listing" + (listings.length !== 1 ? "s" : ""); if (listings.length === 0) { container.innerHTML = ""; @@ -134,7 +159,8 @@ function showPrices() { return listing.price; }); const pricesEl = document.getElementById("prices"); - pricesEl.textContent = prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; + pricesEl.textContent = + prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; } /** @@ -158,7 +184,8 @@ function showExpensivePrices() { return listing.price; }); const el = document.getElementById("expensive-prices"); - el.textContent = prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; + el.textContent = + prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; } /** @@ -185,8 +212,11 @@ function getFilterFromForm() { const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); const minSizeRaw = document.getElementById("advMinSize").value; const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); - const hasGarden = document.getElementById("advHasGarden").checked || undefined; - const facilityCheckboxes = document.querySelectorAll('input[name="advFacility"]:checked'); + const hasGarden = + document.getElementById("advHasGarden").checked || undefined; + const facilityCheckboxes = document.querySelectorAll( + 'input[name="advFacility"]:checked', + ); const facilities = Array.from(facilityCheckboxes).map(function (cb) { return cb.value; }); diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html index f9acbb7a..6294244f 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html @@ -1,4 +1,4 @@ - + @@ -10,10 +10,16 @@

Listings demo – Generate and render

- - + No listings yet. Click Generate.
@@ -25,13 +31,23 @@

Listings demo – Generate and render

- -
- Advanced filters (Task 4 – Listing project): + + Advanced filters (Task 4 – Listing project):
+
- +
- - - - - + + + + +
- +
- Click “Generate listings” to create 37 random listings and render them as cards. + Click “Generate listings” to create 37 random listings and render them as + cards.
diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js index 7e62c61f..1e92b4a5 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js @@ -9,15 +9,27 @@ function randomIntFromInterval(min, max) { function generateListings(numberOfListings) { const listings = []; const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; - const listingFacilities = ["Parkering", "Elevator", "Altan", "Have", "Husdyr"]; + const listingFacilities = [ + "Parkering", + "Elevator", + "Altan", + "Have", + "Husdyr", + ]; for (let i = 0; i < numberOfListings; i++) { const listing = {}; const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); - const numberOfFacilities = randomIntFromInterval(1, listingFacilities.length - 1); + const numberOfFacilities = randomIntFromInterval( + 1, + listingFacilities.length - 1, + ); const facilities = []; for (let j = 0; j < numberOfFacilities; j++) { - const randomIndexFacilities = randomIntFromInterval(0, listingFacilities.length - 1); + const randomIndexFacilities = randomIntFromInterval( + 0, + listingFacilities.length - 1, + ); const randomFacility = listingFacilities[randomIndexFacilities]; if (!facilities.includes(randomFacility)) { facilities.push(randomFacility); @@ -56,13 +68,25 @@ function renderListingCard(listing) { .join(""); card.innerHTML = '
' + - (listing.img ? '' : listing.type) + + (listing.img + ? '' + : listing.type) + "
" + '
' + - '
' + listing.type + "
" + - '
' + listing.price.toLocaleString() + " kr
" + - '
' + listing.size + " m²" + (listing.hasGarden ? " · Garden" : "") + "
" + - '
' + badgesHtml + "
" + + '
' + + listing.type + + "
" + + '
' + + listing.price.toLocaleString() + + " kr
" + + '
' + + listing.size + + " m²" + + (listing.hasGarden ? " · Garden" : "") + + "
" + + '
' + + badgesHtml + + "
" + "
"; return card; } @@ -79,7 +103,8 @@ function prepareListingsView(listings) { const countEl = document.getElementById("count"); const emptyEl = document.getElementById("empty"); - countEl.textContent = listings.length + " listing" + (listings.length !== 1 ? "s" : ""); + countEl.textContent = + listings.length + " listing" + (listings.length !== 1 ? "s" : ""); if (listings.length === 0) { container.innerHTML = ""; @@ -88,7 +113,7 @@ function prepareListingsView(listings) { emptyEl.textContent = "No listings."; emptyEl.style.display = "block"; } - + return null; } @@ -190,8 +215,11 @@ function getFilterFromForm() { const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); const minSizeRaw = document.getElementById("advMinSize").value; const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); - const hasGarden = document.getElementById("advHasGarden").checked || undefined; - const facilityCheckboxes = document.querySelectorAll('input[name="advFacility"]:checked'); + const hasGarden = + document.getElementById("advHasGarden").checked || undefined; + const facilityCheckboxes = document.querySelectorAll( + 'input[name="advFacility"]:checked', + ); const facilities = Array.from(facilityCheckboxes).map(function (cb) { return cb.value; }); diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css index d4481c2c..36d38a02 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css @@ -186,7 +186,9 @@ input[type="number"] { border-radius: 10px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: transform 0.2s, box-shadow 0.2s; + transition: + transform 0.2s, + box-shadow 0.2s; } .card:hover { diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js index a4491fc9..8e2b50dd 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js @@ -3,12 +3,36 @@ // ============================================================================= const mentors = [ - { name: "Abed Sujan", subjects: ["JS", "HTML", "CSS", "NODEJS"], yearOfExperience: 4 }, - { name: "Ahmed Magdy", subjects: ["JS", "Database", "CSS"], yearOfExperience: 1 }, - { name: "Alicia Gonzales", subjects: ["DB", "HTML", "NODEJS"], yearOfExperience: 8 }, - { name: "allan Thraen", subjects: ["REACT", "HTML", "CSS"], yearOfExperience: 3 }, - { name: "Anders Ravn", subjects: ["JS", "HTML", "NODEJS"], yearOfExperience: 2 }, - { name: "Daniel Fernandes", subjects: ["Database", "HTML", "CSS"], yearOfExperience: 9 }, + { + name: "Abed Sujan", + subjects: ["JS", "HTML", "CSS", "NODEJS"], + yearOfExperience: 4, + }, + { + name: "Ahmed Magdy", + subjects: ["JS", "Database", "CSS"], + yearOfExperience: 1, + }, + { + name: "Alicia Gonzales", + subjects: ["DB", "HTML", "NODEJS"], + yearOfExperience: 8, + }, + { + name: "allan Thraen", + subjects: ["REACT", "HTML", "CSS"], + yearOfExperience: 3, + }, + { + name: "Anders Ravn", + subjects: ["JS", "HTML", "NODEJS"], + yearOfExperience: 2, + }, + { + name: "Daniel Fernandes", + subjects: ["Database", "HTML", "CSS"], + yearOfExperience: 9, + }, ]; // ============================================================================= @@ -102,13 +126,15 @@ function renderNamesList(names) { // ============================================================================= function showAllMentors() { - document.getElementById("resultLabel").textContent = "Result: forEach – all mentors"; + document.getElementById("resultLabel").textContent = + "Result: forEach – all mentors"; renderCards(mentors); } function showNamesOnly() { - document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; + document.getElementById("resultLabel").textContent = + "Result: map – mentor names (array of strings)"; const mentorNames = mapHomemade(mentors, function (mentor) { return mentor.name; @@ -118,7 +144,8 @@ function showNamesOnly() { } function showExperienced() { - document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; + document.getElementById("resultLabel").textContent = + "Result: filter – yearOfExperience > 7"; const experiencedMentors = filterHomemade(mentors, function (mentor) { return mentor.yearOfExperience > 7; @@ -128,7 +155,8 @@ function showExperienced() { } function showNamesStartingWithA() { - document.getElementById("resultLabel").textContent = 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + document.getElementById("resultLabel").textContent = + 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; const mentorsThatStartWithA = filterHomemade(mentors, function (mentor) { return mentor.name[0] === "A"; diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html index 3ff2f8b0..dd69b6c0 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.html @@ -1,4 +1,4 @@ - + @@ -9,11 +9,16 @@

Mentors demo – forEach, map, filter

- Use the buttons to see different array operations. Results are rendered below (no console). + Use the buttons to see different array operations. Results are rendered + below (no console).

- -
diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js index 498e5da2..27440d73 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js @@ -3,12 +3,36 @@ // ============================================================================= const mentors = [ - { name: "Abed Sujan", subjects: ["JS", "HTML", "CSS", "NODEJS"], yearOfExperience: 4 }, - { name: "Ahmed Magdy", subjects: ["JS", "Database", "CSS"], yearOfExperience: 1 }, - { name: "Alicia Gonzales", subjects: ["DB", "HTML", "NODEJS"], yearOfExperience: 8 }, - { name: "allan Thraen", subjects: ["REACT", "HTML", "CSS"], yearOfExperience: 3 }, - { name: "Anders Ravn", subjects: ["JS", "HTML", "NODEJS"], yearOfExperience: 2 }, - { name: "Daniel Fernandes", subjects: ["Database", "HTML", "CSS"], yearOfExperience: 9 }, + { + name: "Abed Sujan", + subjects: ["JS", "HTML", "CSS", "NODEJS"], + yearOfExperience: 4, + }, + { + name: "Ahmed Magdy", + subjects: ["JS", "Database", "CSS"], + yearOfExperience: 1, + }, + { + name: "Alicia Gonzales", + subjects: ["DB", "HTML", "NODEJS"], + yearOfExperience: 8, + }, + { + name: "allan Thraen", + subjects: ["REACT", "HTML", "CSS"], + yearOfExperience: 3, + }, + { + name: "Anders Ravn", + subjects: ["JS", "HTML", "NODEJS"], + yearOfExperience: 2, + }, + { + name: "Daniel Fernandes", + subjects: ["Database", "HTML", "CSS"], + yearOfExperience: 9, + }, ]; // ============================================================================= @@ -19,54 +43,44 @@ const mentors = [ * Executes function for each item in the array, NO RETURN! * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach */ -function forEachHomemade(array, functionToExecute) { - -} +function forEachHomemade(array, functionToExecute) {} /** * Changes/transforms the items in the array. Returns a new array with the transformed items. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map */ -function mapHomemade(array, functionToExecute) { - -} +function mapHomemade(array, functionToExecute) {} /** * Changes the number of items in the array based on a condition. Returns a new array with only the items that satisfy the condition. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter */ -function filterHomemade(array, functionToExecute) { - -} +function filterHomemade(array, functionToExecute) {} // ============================================================================= // RENDER HELPERS // ============================================================================= -function renderMentorCard(mentor) { +function renderMentorCard(mentor) {} -} - -function renderCards(mentorsToShow) { - -} - -function renderNamesList(names) { +function renderCards(mentorsToShow) {} -} +function renderNamesList(names) {} // ============================================================================= // DEMO ACTIONS (forEach, map, filter) // ============================================================================= function showAllMentors() { - document.getElementById("resultLabel").textContent = "Result: forEach – all mentors"; + document.getElementById("resultLabel").textContent = + "Result: forEach – all mentors"; renderCards(mentors); } function showNamesOnly() { - document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; + document.getElementById("resultLabel").textContent = + "Result: map – mentor names (array of strings)"; const mentorNames = []; @@ -74,7 +88,8 @@ function showNamesOnly() { } function showExperienced() { - document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; + document.getElementById("resultLabel").textContent = + "Result: filter – yearOfExperience > 7"; const experiencedMentors = mentors; @@ -82,7 +97,8 @@ function showExperienced() { } function showNamesStartingWithA() { - document.getElementById("resultLabel").textContent = 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + document.getElementById("resultLabel").textContent = + 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; const mentorsThatStartWithA = mentors; diff --git a/package-lock.json b/package-lock.json index 19f7f3e3..0f7a2c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "program", + "name": "programme", "lockfileVersion": 3, "requires": true, "packages": { From 33be43ef78c7dbb8c36c0c0f670a35c61ea6652d Mon Sep 17 00:00:00 2001 From: markitosha Date: Sun, 8 Mar 2026 10:49:34 +0100 Subject: [PATCH 3/8] Removed solution file --- .../listings-demo/index-solution.js | 279 ------------------ 1 file changed, 279 deletions(-) delete mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js deleted file mode 100644 index 33d75812..00000000 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index-solution.js +++ /dev/null @@ -1,279 +0,0 @@ -// ============================================================================= -// DATA & HELPERS -// ============================================================================= - -function randomIntFromInterval(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function generateListings(numberOfListings) { - const listings = []; - const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; - const listingFacilities = [ - "Parkering", - "Elevator", - "Altan", - "Have", - "Husdyr", - ]; - - for (let i = 0; i < numberOfListings; i++) { - const listing = {}; - const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); - const numberOfFacilities = randomIntFromInterval( - 1, - listingFacilities.length - 1, - ); - const facilities = []; - for (let j = 0; j < numberOfFacilities; j++) { - const randomIndexFacilities = randomIntFromInterval( - 0, - listingFacilities.length - 1, - ); - const randomFacility = listingFacilities[randomIndexFacilities]; - if (!facilities.includes(randomFacility)) { - facilities.push(randomFacility); - } - } - listing.type = listingType[randomTypeIndex]; - listing.facilities = facilities; - listing.price = randomIntFromInterval(1, 10000); - listing.hasGarden = Boolean(randomIntFromInterval(0, 1)); - listing.size = randomIntFromInterval(12, 1000); - listing.img = `https://loremflickr.com/200/200/${listing.type}`; - listings.push(listing); - } - return listings; -} - -// ============================================================================= -// RENDER -// ============================================================================= - -/** - * Build one card element for a single listing. Helper – already implemented. - * @param {Object} listing - One listing object (type, price, size, hasGarden, facilities, img) - * @returns {HTMLElement} A div.card element - */ -function renderListingCard(listing) { - const card = document.createElement("div"); - card.className = "card"; - const badgeClass = function (f) { - return f === "Have" ? " garden" : ""; - }; - const badgesHtml = listing.facilities - .map(function (f) { - return '' + f + ""; - }) - .join(""); - card.innerHTML = - '
' + - (listing.img - ? '' - : listing.type) + - "
" + - '
' + - '
' + - listing.type + - "
" + - '
' + - listing.price.toLocaleString() + - " kr
" + - '
' + - listing.size + - " m²" + - (listing.hasGarden ? " · Garden" : "") + - "
" + - '
' + - badgesHtml + - "
" + - "
"; - return card; -} - -/** - * Update count and empty state, clear the cards container. Returns the #cards container if there - * are listings to render, or null if not (so you can return early). - * Use this at the start of renderListings. - * @param {Array} listings - * @returns {HTMLElement|null} The #cards container, or null when listings.length === 0 - */ -function prepareListingsView(listings) { - const container = document.getElementById("cards"); - const countEl = document.getElementById("count"); - const emptyEl = document.getElementById("empty"); - - countEl.textContent = - listings.length + " listing" + (listings.length !== 1 ? "s" : ""); - - if (listings.length === 0) { - container.innerHTML = ""; - if (emptyEl) { - emptyEl.textContent = "No listings."; - emptyEl.style.display = "block"; - } - return null; - } - - if (emptyEl) { - emptyEl.style.display = "none"; - } - - container.innerHTML = ""; - - return container; -} - -/** - * Render the listings array into #cards. Uses prepareListingsView, then forEach to append cards. - */ -function renderListings(listings) { - const container = prepareListingsView(listings); - if (!container) { - return; - } - - listings.forEach(function (listing) { - const card = renderListingCard(listing); - container.appendChild(card); - }); -} - -// ============================================================================= -// STATE -// ============================================================================= - -/** Last generated listings; used by Show prices (Task 2) and filter (Task 3). */ -let currentListings = []; - -function generateAndRenderListings() { - currentListings = generateListings(37); - renderListings(currentListings); -} - -/** - * Show prices using map: create array of prices from currentListings, display in #prices. - */ -function showPrices() { - const prices = currentListings.map(function (listing) { - return listing.price; - }); - const pricesEl = document.getElementById("prices"); - pricesEl.textContent = - prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; -} - -/** - * Show cheap listings (price < 2000). Filter, then renderListings. Result: array of objects. - */ -function showCheapListings() { - const cheap = currentListings.filter(function (listing) { - return listing.price < 2000; - }); - renderListings(cheap); -} - -/** - * Show expensive listings' prices only. Filter expensive, map to price. Result: array of numbers. Display in #expensive-prices. - */ -function showExpensivePrices() { - const expensiveListings = currentListings.filter(function (listing) { - return listing.price > 5000; - }); - const prices = expensiveListings.map(function (listing) { - return listing.price; - }); - const el = document.getElementById("expensive-prices"); - el.textContent = - prices.length > 0 ? prices.join(", ") + " kr" : "Generate listings first."; -} - -/** - * Show only listings that have parking (Parkering in facilities). Filter, then renderListings. Result: array of objects. - */ -function showListingsWithParking() { - const withParking = currentListings.filter(function (listing) { - return listing.facilities.includes("Parkering"); - }); - renderListings(withParking); -} - -// ============================================================================= -// ADVANCED FILTERS (Task 4 – Listing project) -// ============================================================================= - -/** - * Read Advanced filters form and return a filter object. Only includes set values. - * Filter object format: { type?, minPrice?, minSize?, hasGarden?, facilities? } – see filterListings. - */ -function getFilterFromForm() { - const type = document.getElementById("advType").value || undefined; - const minPriceRaw = document.getElementById("advMinPrice").value; - const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); - const minSizeRaw = document.getElementById("advMinSize").value; - const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); - const hasGarden = - document.getElementById("advHasGarden").checked || undefined; - const facilityCheckboxes = document.querySelectorAll( - 'input[name="advFacility"]:checked', - ); - const facilities = Array.from(facilityCheckboxes).map(function (cb) { - return cb.value; - }); - - const filter = {}; - if (type) { - filter.type = type; - } - if (minPrice != null && !isNaN(minPrice)) { - filter.minPrice = minPrice; - } - if (minSize != null && !isNaN(minSize)) { - filter.minSize = minSize; - } - if (hasGarden) { - filter.hasGarden = true; - } - if (facilities.length > 0) { - filter.facilities = facilities; - } - - return filter; -} - -/** - * Return only listings that match every property in filter. - * Filter object: type (string), minPrice (number), minSize (number), hasGarden (boolean), facilities (string[]). - * Only present keys are applied; listing must match all present criteria. - */ -function filterListings(listings, filter) { - return listings.filter(function (listing) { - if (filter.type && listing.type !== filter.type) { - return false; - } - if (filter.minPrice != null && listing.price < filter.minPrice) { - return false; - } - if (filter.minSize != null && listing.size < filter.minSize) { - return false; - } - if (filter.hasGarden && !listing.hasGarden) { - return false; - } - if (filter.facilities && filter.facilities.length > 0) { - for (let i = 0; i < filter.facilities.length; i++) { - if (!listing.facilities.includes(filter.facilities[i])) { - return false; - } - } - } - return true; - }); -} - -function applyAdvancedFilters() { - const filter = getFilterFromForm(); - const filtered = filterListings(currentListings, filter); - - renderListings(filtered); -} From ef26c0a565ef84cbebcfcd42406179ab914deeeb Mon Sep 17 00:00:00 2001 From: markitosha Date: Mon, 9 Mar 2026 14:17:09 +0100 Subject: [PATCH 4/8] Updated session matherials and assigment --- .../advanced-javascript/week1/README.md | 2 + .../advanced-javascript/week1/assignment.md | 12 +- .../week1/session-materials/exercises.md | 66 +++--- .../session-materials/listings-demo/README.md | 66 ++++++ .../listings-demo/index.html | 20 +- .../session-materials/listings-demo/index.js | 198 ++++++++++-------- .../session-materials/mentors-demo/README.md | 49 +++++ .../mentors-demo/index-solution.js | 146 +++++++------ .../session-materials/mentors-demo/index.js | 109 +++++++--- .../advanced-javascript/week1/session-plan.md | 8 + 10 files changed, 460 insertions(+), 216 deletions(-) create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md create mode 100644 courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/README.md diff --git a/courses/frontend/advanced-javascript/week1/README.md b/courses/frontend/advanced-javascript/week1/README.md index 3addebdb..34866ba3 100644 --- a/courses/frontend/advanced-javascript/week1/README.md +++ b/courses/frontend/advanced-javascript/week1/README.md @@ -4,6 +4,8 @@ This session is about mastering the most commonly used array functions provided Understanding how to use array functions and the arrow notation can greatly improve the readability of your code. +The module strongly encourages going beyond JavaScript alone and **implementing a real frontend**—work that runs in the browser with a visible, interactive UI. All exercises (session materials and assignment) are designed so that JavaScript drives HTML and the interface. + ## Contents - [Preparation](./preparation.md) diff --git a/courses/frontend/advanced-javascript/week1/assignment.md b/courses/frontend/advanced-javascript/week1/assignment.md index f5a049d9..c35a57c6 100644 --- a/courses/frontend/advanced-javascript/week1/assignment.md +++ b/courses/frontend/advanced-javascript/week1/assignment.md @@ -1,10 +1,8 @@ # Assignment - +Only **task 3** (movies) requires a frontend: HTML + CSS + JavaScript that runs in the browser, with the result visible in the page. **Tasks 1 and 2** do not: for task 1 submit the JavaScript (map/filter) solution (an HTML page is optional though); for task 2 just complete the Katas. -The warmup exercises will be a bit abstract. But the in the **hyfBay exercise** the task will be a lot closer to a **real world task**. - -**Frontend requirement:** For each task, implement a frontend (HTML + CSS + JavaScript) that shows your solution in the browser. Display the results in the page. You may use one HTML page per exercise or organize multiple sections on a single page—as long as the user can see the output of each task in the UI. +The first task is a short warmup. The second is practice on Codewars. The third (movies) is a larger, real-world-style task: one page or app that shows all the movie results in the UI. ## 1. Doubling of number @@ -24,7 +22,7 @@ for (let i = 0; i < numbers.length; i++) { // expected result: [2, 6] ``` -Rewrite the above program using `map` and `filter`; don't forget to use arrow functions. Show the result in your page. +Rewrite the above program using `map` and `filter`; don't forget to use arrow functions. Showing the result in a page is optional for this task (but nice if you do). ## 2. Codewars! @@ -37,7 +35,9 @@ Complete these Katas: ![cinema](https://media.giphy.com/media/l6mBchxYZc7Sw/giphy.gif) -Copy the movies array in the [movies](./session-materials/movies.js) file. Use this array to do the following tasks. **Implement a frontend** so each task’s result is visible in the browser (e.g. sections or cards for each sub-task, with the computed data rendered in the page). +**What the user sees:** One page (e.g. sections or cards) where every sub-task’s result is visible in the browser: short titles, long titles, 1980s count, tagged movies, ratings over 6, keyword count, duplicated-word titles, and optionally average rating and Good/Average/Bad counts. + +Copy the movies array from [movies](./session-materials/movies.js) and use it for the tasks below. 1. Create an array of movies containing the **movies with a short title** (you define what short means) 2. Create an array of movie titles with **long movie titles** diff --git a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md index 6a8d5458..465fde15 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md +++ b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md @@ -1,41 +1,59 @@ # Exercises -Work in the [Listings demo](./listings-demo/) in session materials. Open `index.js` and implement the tasks there. Use the **Generate listings (37)** button to get data. +Work in the [Listings demo](./listings-demo/). Open **index.html** in your browser to run the demo, and open **index.js** in your IDE to implement the tasks. In `index.js`, search for **`Task`** (e.g. Task 1.1, Task 2)—each task has a comment with explicit instructions about what code to write. This document describes the **end-result** you’re aiming for and how the exercise is set up. The generation of random listings and the render helpers (cards, price list) are already implemented; you only need to call them and add the logic that uses `.forEach()`, `.map()`, and `.filter()`. -## `forEach` + +## Task 1: Generate and show listings (`forEach`) -**Task 1** in the demo: implement `generateAndRenderListings` (assign `generateListings(37)` to `currentListings`, then call `renderListings(currentListings)`), and implement `renderListings`: use `prepareListingsView(listings)`, then **`listings.forEach(...)`** to add one card per listing with `renderListingCard(listing)`. Click **Generate listings (37)** to see your result. +**What should happen:** When the user clicks **Generate listings (37) — Task 1**, 37 random listings are generated and displayed as cards on the page. -## `map` +**Listing** (each object in the generated array): -**Task 2** in the demo: implement `showPrices`: use **`map`** to create an array of prices from `currentListings`, then display it in the `#prices` element. Click Generate first, then **Show prices**. +| Property | Type | Description | +|---------------|----------|-------------| +| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | +| `facilities` | string[] | e.g. `["Parkering", "Have"]` | +| `price` | number | 1–10000 | +| `hasGarden` | boolean | | +| `size` | number | m², 12–1000 | +| `img` | string | image URL | -## `filter` +You implement the logic that (1) triggers generation and (2) renders the listings by iterating over the array and adding one card per listing. The code that builds each card and prepares the view is already there; you use `listings.forEach(...)` to show every listing. -**Task 3** in the demo (Filters block – three buttons): + +## Task 2: Show prices (`map`) -- **3a. Cheap listings:** implement `showCheapListings`: use **`filter`** to get listings that are "cheap" (you define the condition, e.g. price below a number). Result: array of **objects**. Call `renderListings` with that array. -- **3b. Expensive prices:** implement `showExpensivePrices`: use **`filter`** to get expensive listings, then **`map`** to get an array of their prices. Result: array of **numbers**. Display it in `#expensive-prices`. -- **3c. With parking:** implement `showListingsWithParking`: use **`filter`** to get listings that have "Parkering" in `facilities`. Result: array of **objects**. Call `renderListings` with that array. +**What should happen:** After generating listings, when the user clicks **Show prices — Task 2**, a list of all listing prices appears (e.g. comma-separated) in the prices area. -## Arrow functions +You implement getting an array of prices from the current listings and passing it to the helper that displays numbers. Use `.map()` to turn the list of listings into a list of prices. -Rewrite the code above (`forEach`, `map` and `filter`) to arrow functions. + +## Task 3: Filter buttons (`filter`) -## Listing project +**What should happen:** When the user clicks one of the filter buttons, the page shows only the matching listings (or their prices). -**Task 4** in the demo (Advanced filters block): implement **filterListings(listings, filter)** only. Reading the form and building the filter object is already done for you. +- **Cheap listings — Task 3a:** Only “cheap” listings are shown as cards. You decide what “cheap” means (e.g. price below a certain number). Use `.filter()` and then the same render-as-cards logic as in Task 1. +- **Expensive prices — Task 3b:** Only the *prices* of “expensive” listings are shown (as numbers). Use `.filter()` to get expensive listings, then `.map()` to get their prices, and the helper that displays numbers. +- **With parking — Task 3c:** Only listings that have “Parkering” in their facilities are shown as cards. Use `.filter()` on the facilities. -**Filter object format** (the second argument you receive): an object with only the fields the user set. Possible properties: + +## Task: Arrow functions -| Property | Type | Meaning | -| ------------------- | -------- | ---------------------------------------------------------------------------------- | -| `filter.type` | string | Listing type must equal this (e.g. `"Farm"`, `"House"`). | -| `filter.minPrice` | number | Listing price must be ≥ this. | -| `filter.minSize` | number | Listing size (m²) must be ≥ this. | -| `filter.hasGarden` | boolean | If `true`, listing must have a garden. | -| `filter.facilities` | string[] | Listing must have **every** facility in this array (e.g. `["Parkering", "Have"]`). | +Rewrite the code you wrote for Tasks 1–3 to use arrow function syntax instead of `function`. -If a property is missing from `filter`, do not filter by it. Return only listings that match **every** present property. + +## Task 4: Advanced filters (Listing project) -Click Generate, set the Advanced filters, then **Apply filters**. You can use arrow functions. +**What should happen:** The user can set type, min price, min size, facilities, and garden in the Advanced filters form. When they click **Apply filters — Task 4**, only listings that match *all* selected criteria are shown as cards. + +You implement **filterListings(listings, filter)**. Reading the form and building the `filter` object is already done; you receive an object with only the fields the user set. Return only listings that match every property present in `filter`. If a property is missing from `filter`, don’t filter by it. + +**Filter** (object passed to `filterListings`; only set fields are present): + +| Property | Type | Meaning | +|---------------|----------|---------| +| `type` | string | `listing.type` must equal this. | +| `minPrice` | number | `listing.price` must be ≥ this. | +| `minSize` | number | `listing.size` must be ≥ this (m²). | +| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | +| `facilities` | string[] | `listing.facilities` must include every string in this array. | diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md new file mode 100644 index 00000000..27dadbe9 --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md @@ -0,0 +1,66 @@ +# Listings demo – forEach, map, filter + +Exercise for **trainees** (Week 1). Generate random listings, render them as cards, then use `.forEach()`, `.map()`, and `.filter()` to show prices and apply filters. All implemented code is at the top of `index.js`; your tasks are at the bottom. + +## Files + +| File | Purpose | +|------|--------| +| **index.js** | Main file. Implement the task functions (search for `Task`). | +| **index.html** | Page with Generate button, Show prices, filters, advanced filters form, and result areas. Buttons pass `currentListings` into the task functions. | +| **style.css** | Styles for cards and layout. | + +## Where to find tasks + +Search for **`Task`** (e.g. Task 1.1, Task 2) in `index.js`. Each task has a comment with explicit instructions. More on these tasks: [Exercises](../exercises.md). + +## How the code works + +### Main HTML elements + +Each button is labeled with its task number in the UI. + +| Element | Task | Action | Output | +|--------|------|--------|--------| +| **Generate listings (37) — Task 1** | 1 | `generateAndRenderListings()` | **#cards** | +| **Show prices — Task 2** | 2 | `showPrices(currentListings)` | **#prices** | +| **Cheap listings — Task 3a** | 3a | `showCheapListings(currentListings)` | **#cards** | +| **Expensive prices — Task 3b** | 3b | `showExpensivePrices(currentListings)` | **#expensive-prices** | +| **With parking — Task 3c** | 3c | `showListingsWithParking(currentListings)` | **#cards** | +| **Apply filters — Task 4** | 4 | `applyAdvancedFilters()` (form: type, min price, min size, facilities, garden) | **#cards** | + +Other elements: **#count** (listing count), **#empty** (empty state message). + +### Implemented functions (top of index.js) + +- **generateListings(numberOfListings)** – builds random listings (see Data structures below), sets `currentListings` internally, returns the array. +- **renderListingCard(listing)** – builds one card DOM element. +- **prepareListingsView(listings)** – clears `#cards`, updates count/empty; returns container or `null`. +- **renderNumbersInElement(numbers, elementId)** – clears the element and shows the numbers as comma-separated text (use `"prices"` or `"expensive-prices"` for elementId). +- **getFilterFromForm()** – reads the advanced form into a filter object. +- **applyAdvancedFilters()** – gets filter, calls `filterListings(currentListings, filter)`, then `renderListings(filtered)`. + +## Data structures + +**Listing** (each object in the generated array): + +| Property | Type | Description | +|-------------|-----------|-------------| +| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | +| `facilities`| string[] | e.g. `["Parkering", "Have"]` | +| `price` | number | 1–10000 | +| `hasGarden` | boolean | | +| `size` | number | m², 12–1000 | +| `img` | string | image URL | + +**Filter** (object passed to `filterListings`; built from the Advanced filters form; only set fields are present): + +| Property | Type | Meaning | +|-------------|----------|---------| +| `type` | string | `listing.type` must equal this. | +| `minPrice` | number | `listing.price` must be ≥ this. | +| `minSize` | number | `listing.size` must be ≥ this (m²). | +| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | +| `facilities`| string[] | `listing.facilities` must include every string in this array. | + +If a filter property is missing, do not filter by it. Return listings that match every present property. diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html index 6294244f..66055c35 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html @@ -15,28 +15,28 @@

Listings demo – Generate and render

type="button" onclick="generateAndRenderListings();" > - Generate listings (37) + Generate listings (37) — Task 1 - No listings yet. Click Generate.
Filters (Task 3): - -
@@ -107,7 +107,7 @@

Listings demo – Generate and render

garden - +
diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js index 1e92b4a5..cc58af2e 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js @@ -2,10 +2,24 @@ // DATA & HELPERS // ============================================================================= +let currentListings = []; + function randomIntFromInterval(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } +/* + * Listing structure (each object in the listings array): + * type {string} – e.g. "House", "Apartment", "Shed", "Dorm", "Farm" + * facilities {string[]} – e.g. ["Parkering", "Have"] + * price {number} – 1–10000 + * hasGarden {boolean} + * size {number} – m², 12–1000 + * img {string} – image URL + * + * Creates an array of random listing objects and assigns it to currentListings (global). + * Returns the same array. + */ function generateListings(numberOfListings) { const listings = []; const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; @@ -43,17 +57,18 @@ function generateListings(numberOfListings) { listing.img = `https://loremflickr.com/200/200/${listing.type}`; listings.push(listing); } + + currentListings = listings; + return listings; } // ============================================================================= -// RENDER +// RENDER HELPERS (already implemented) // ============================================================================= /** - * Build one card element for a single listing. Helper – already implemented. - * @param {Object} listing - One listing object (type, price, size, hasGarden, facilities, img) - * @returns {HTMLElement} A div.card element + * Build one card element for a single listing. */ function renderListingCard(listing) { const card = document.createElement("div"); @@ -92,11 +107,7 @@ function renderListingCard(listing) { } /** - * Update count and empty state, clear the cards container. Returns the #cards container if there - * are listings to render, or null if not (so you can return early). - * Use this at the start of renderListings. - * @param {Array} listings - * @returns {HTMLElement|null} The #cards container, or null when listings.length === 0 + * Update count and empty state, clear #cards. Returns the container or null if no listings. */ function prepareListingsView(listings) { const container = document.getElementById("cards"); @@ -126,16 +137,91 @@ function prepareListingsView(listings) { return container; } +/** + * Display an array of numbers in a given element (e.g. #prices or #expensive-prices). + * Clears the element and shows the numbers as comma-separated text. + */ +function renderNumbersInElement(numbers, elementId) { + const el = document.getElementById(elementId); + + el.innerHTML = ""; + el.textContent = numbers.length === 0 ? "" : numbers.join(", "); +} + +// ============================================================================= +// ADVANCED FILTERS HELPERS +// ============================================================================= + +/** + * Read Advanced filters form and return a filter object. Only includes set values. + */ +function getFilterFromForm() { + const type = document.getElementById("advType").value || undefined; + const minPriceRaw = document.getElementById("advMinPrice").value; + const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); + const minSizeRaw = document.getElementById("advMinSize").value; + const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); + const hasGarden = + document.getElementById("advHasGarden").checked || undefined; + const facilityCheckboxes = document.querySelectorAll( + 'input[name="advFacility"]:checked', + ); + const facilities = Array.from(facilityCheckboxes).map(function (cb) { + return cb.value; + }); + + const filter = {}; + + if (type) { + filter.type = type; + } + + if (minPrice != null && !isNaN(minPrice)) { + filter.minPrice = minPrice; + } + + if (minSize != null && !isNaN(minSize)) { + filter.minSize = minSize; + } + + if (hasGarden) { + filter.hasGarden = true; + } + + if (facilities.length > 0) { + filter.facilities = facilities; + } + + return filter; +} + +function applyAdvancedFilters() { + const filter = getFilterFromForm(); + const filtered = filterListings(currentListings, filter); + + renderListings(filtered); +} + +// ============================================================================= +// TASKS (implement the functions below – search for Task N) +// ============================================================================= + /* * Task 1. Implement two things: * - * 1. generateAndRenderListings: assign generateListings(37) to currentListings (below), then - * call renderListings(currentListings). (currentListings is used later by Task 2.) + * 1. generateAndRenderListings: call generateListings(37) (it generates listings for further use), then + * call renderListings(currentListings). * * 2. renderListings: use prepareListingsView(listings) first; if it returns null, return. * Otherwise use the returned container: call listings.forEach(function (listing) { ... }) * and append renderListingCard(listing) to the container. */ +function generateAndRenderListings() { + /* + * Task 1.1: call generateListings(37) to generate the data, then call renderListings(currentListings) to render it + */ +} + function renderListings(listings) { const container = prepareListingsView(listings); @@ -148,26 +234,13 @@ function renderListings(listings) { */ } -// ============================================================================= -// STATE -// ============================================================================= - -/** Last generated listings; used by Show prices (Task 2) and filter (Task 3). */ -let currentListings = []; - -function generateAndRenderListings() { - /* - * Task 1.1: assign generateListings(37) to currentListings, then call renderListings(currentListings) - */ -} - /* - * Task 2. Implement showPrices using map: create an array of prices from currentListings - * (one price per listing), then display that array in the #prices element (e.g. as text or a list). + * Task 2. Implement showPrices using map: create an array of prices from listings + * (one price per listing), then call renderNumbersInElement(prices, "prices"). */ -function showPrices() { +function showPrices(listings) { /* - * Task 2: use map to get an array of prices from currentListings, then display it in #prices + * Task 2: use map to get an array of prices from listings, then renderNumbersInElement(prices, "prices") */ } @@ -175,75 +248,32 @@ function showPrices() { * Task 3. Implement three filter functions: * * 3a. showCheapListings: use filter to get listings that are "cheap" (you define the condition, e.g. price below a number). - * Result: array of objects. Call renderListings with that array. + * Result: array of **objects**. Call renderListings with that array. * * 3b. showExpensivePrices: use filter to get expensive listings, then use map to get an array of their prices (numbers). - * Result: array of numbers. Display it in the #expensive-prices element. + * Result: array of **numbers**. Call renderNumbersInElement(prices, "expensive-prices"). * * 3c. showListingsWithParking: use filter to get listings that have "Parkering" in facilities. - * Result: array of objects. Call renderListings with that array. + * Result: array of **objects**. Call renderListings with that array. */ -function showCheapListings() { +function showCheapListings(listings) { /* - * Task 3a: use filter for cheap listings (e.g. by price), then renderListings(...) + * Task 3a: use filter on listings for cheap (e.g. by price), then renderListings(...) */ } -function showExpensivePrices() { +function showExpensivePrices(listings) { /* - * Task 3b: use filter for expensive listings, then map to prices (numbers), display in #expensive-prices + * Task 3b: use filter on listings for expensive, then map to prices, then renderNumbersInElement(prices, "expensive-prices") */ } -function showListingsWithParking() { +function showListingsWithParking(listings) { /* - * Task 3c: use filter for listings with parking, then renderListings(...) + * Task 3c: use filter on listings for "Parkering" in facilities, then renderListings(...) */ } -// ============================================================================= -// ADVANCED FILTERS (Task 4 – Listing project) -// ============================================================================= - -/** - * Read Advanced filters form and return a filter object. Only includes set values. - * (Already implemented – you only implement filterListings below.) - */ -function getFilterFromForm() { - const type = document.getElementById("advType").value || undefined; - const minPriceRaw = document.getElementById("advMinPrice").value; - const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); - const minSizeRaw = document.getElementById("advMinSize").value; - const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); - const hasGarden = - document.getElementById("advHasGarden").checked || undefined; - const facilityCheckboxes = document.querySelectorAll( - 'input[name="advFacility"]:checked', - ); - const facilities = Array.from(facilityCheckboxes).map(function (cb) { - return cb.value; - }); - - const filter = {}; - if (type) { - filter.type = type; - } - if (minPrice != null && !isNaN(minPrice)) { - filter.minPrice = minPrice; - } - if (minSize != null && !isNaN(minSize)) { - filter.minSize = minSize; - } - if (hasGarden) { - filter.hasGarden = true; - } - if (facilities.length > 0) { - filter.facilities = facilities; - } - - return filter; -} - /* * Task 4. Implement filterListings(listings, filter). * @@ -265,11 +295,3 @@ function filterListings(listings, filter) { */ return listings; } - -function applyAdvancedFilters() { - const filter = getFilterFromForm(); - - const filtered = filterListings(currentListings, filter); - - renderListings(filtered); -} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/README.md b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/README.md new file mode 100644 index 00000000..9b550c8f --- /dev/null +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/README.md @@ -0,0 +1,49 @@ +# Mentors demo – forEach, map, filter + +In-session demo for **Week 1** (Advanced JavaScript). A fixed list of mentors is shown in the page; four buttons run different array operations (forEach, map, filter, and chained filter). All output is rendered in the page. The goal is to implement the missing parts during the session using the normal array methods (`.forEach()`, `.map()`, `.filter()`), and to implement the "homemade" versions for demonstration. + +--- + +## Files in this folder + +| File | Purpose | +|------|--------| +| **index.js** | Worksheet for **implementing in class**. Contains data, render helpers, and stubs. Missing logic is marked with `// {TOPIC} TODO:` comments. Use this file when leading the session. | +| **index-solution.js** | Final implementation. | +| **index.html** | Page layout: four action buttons and a result area. Load this in a browser to run the demo. No edits needed. | +| **style.css** | Styles for cards, buttons, and layout. No edits needed. | + +--- + +## Where to find tasks and how they are marked + +All tasks are in **index.js**. They are marked with **TODO** comments so you can search for `TODO` in the file. + +- **FOREACH TODO** – Implement the homemade `forEach` (demonstration only) and/or the logic in `renderCards` that shows all mentors as cards (using `.forEach()`). +- **MAP TODO** – Implement the homemade `map` (demonstration only) and/or the logic in `showNamesOnly` that builds an array of mentor names and passes it to `renderNamesList` (using `.map()`). +- **FILTER TODO** – Implement the homemade `filter` (demonstration only) and/or the logic in `showExperienced` that keeps only mentors with `yearOfExperience > 7` (using `.filter()`). +- **CHAINING TODO** – In `showNamesStartingWithA`, implement filtering with multiple conditions (e.g. name starts with `"A"` and `yearOfExperience > 2`), using chained `.filter()` or a single `.filter()` with both conditions. + +--- + +## How the code works + +### Main HTML elements (index.html) + +- **`.actions`** – Wrapper around the four buttons. Each button calls `setActive(this)` and one of: `showAllMentors()`, `showNamesOnly()`, `showExperienced()`, `showNamesStartingWithA()`. +- **`#resultLabel`** – Text that describes the current result (e.g. “Result: forEach – all mentors”). +- **`#result`** – Container where the output is rendered: either a grid of mentor cards or a comma-separated list of names. + +### Main render functions (index.js) + +- **`renderMentorCard(mentor)`** – Creates one card element for a mentor: name, years of experience, and subjects (using `.map()` on `mentor.subjects`). Returns a `div.card`; does **not** append it to the page. +- **`renderNamesList(names)`** – Clears `#result`, then shows the `names` array as comma-separated text in a single element. If `names` is empty, shows `"No names."`. +- **`renderCards(mentorsToShow)`** – Clears `#result`, creates a wrapper `div.cards`, then should append one card per mentor by calling `renderMentorCard(mentor)` for each and appending to the wrapper. The wrapper is then appended to `#result`. This is the **forEach** example: use `mentorsToShow.forEach(...)` to add every card. +- **`setActive(clickedBtn)`** – Removes the `active` class from all `.actions` buttons and adds it to the clicked button (for styling the selected action). + +### Data and demo flow + +- **`mentors`** – Array of mentor objects, each with `name`, `subjects` (array of strings), and `yearOfExperience` (number). +- **Demo actions** – `showAllMentors()`, `showNamesOnly()`, `showExperienced()`, `showNamesStartingWithA()`. See the code for what each does. + +On load, `showAllMentors()` runs so the page initially shows all mentors as cards. diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js index 8e2b50dd..b58a8c57 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index-solution.js @@ -36,17 +36,82 @@ const mentors = [ ]; // ============================================================================= -// HOMEMADE ARRAY FUNCTIONS (our own implementations) +// RENDER HELPERS (already implemented – go through with trainees) // ============================================================================= +/** + * Create one card element for a mentor + */ +function renderMentorCard(mentor) { + const div = document.createElement("div"); + div.className = "card"; + + const subjectSpans = mentor.subjects.map(function (subject) { + return `${subject}`; + }); + const subjectsHtml = subjectSpans.join(""); + + div.innerHTML = ` +
${mentor.name}
+
${mentor.yearOfExperience} years experience
+
${subjectsHtml}
+ `; + + return div; +} + +/** + * Clear #result and show names as comma-separated text (or "No names." if empty). + */ +function renderNamesList(names) { + const container = document.getElementById("result"); + container.innerHTML = ""; + + const div = document.createElement("div"); + div.className = `names-list${names.length === 0 ? " empty" : ""}`; + div.textContent = names.length === 0 ? "No names." : names.join(", "); + + container.appendChild(div); +} + +/** + * Helper function to set the active button + */ +function setActive(clickedBtn) { + const buttons = document.querySelectorAll(".actions button"); + + for (let i = 0; i < buttons.length; i++) { + buttons[i].classList.remove("active"); + } + + clickedBtn.classList.add("active"); +} + +// Default: show all mentors on load +showAllMentors(); + +// ============================================================================= +// HOMEMADE ARRAY FUNCTIONS (implement these during the session for demostration) +// ============================================================================= + +/** + * Executes function for each item in the array, NO RETURN! + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + */ function forEachHomemade(array, functionToExecute) { + // FOREACH TODO: implement forEach for (let i = 0; i < array.length; i++) { const currentItem = array[i]; functionToExecute(currentItem, i); } } +/** + * Changes/transforms the items in the array. Returns a new array with the transformed items. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + */ function mapHomemade(array, functionToExecute) { + // MAP TODO: implement map const mappedArray = []; for (let i = 0; i < array.length; i++) { @@ -59,7 +124,12 @@ function mapHomemade(array, functionToExecute) { return mappedArray; } +/** + * Changes the number of items in the array based on a condition. Returns a new array with only the items that satisfy the condition. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + */ function filterHomemade(array, functionToExecute) { + // FILTER TODO: implement filter const filteredArray = []; for (let i = 0; i < array.length; i++) { @@ -75,27 +145,9 @@ function filterHomemade(array, functionToExecute) { } // ============================================================================= -// RENDER HELPERS +// DEMO ACTIONS (forEach, map, filter) // ============================================================================= -function renderMentorCard(mentor) { - const div = document.createElement("div"); - div.className = "card"; - - const subjectSpans = mapHomemade(mentor.subjects, function (subject) { - return `${subject}`; - }); - const subjectsHtml = subjectSpans.join(""); - - div.innerHTML = ` -
${mentor.name}
-
${mentor.yearOfExperience} years experience
-
${subjectsHtml}
- `; - - return div; -} - function renderCards(mentorsToShow) { const container = document.getElementById("result"); container.innerHTML = ""; @@ -103,28 +155,14 @@ function renderCards(mentorsToShow) { const wrapper = document.createElement("div"); wrapper.className = "cards"; - forEachHomemade(mentorsToShow, function (mentor) { + // FOREACH TODO: implement showing all mentors as cards instead + mentorsToShow.forEach(function (mentor) { wrapper.appendChild(renderMentorCard(mentor)); }); container.appendChild(wrapper); } -function renderNamesList(names) { - const container = document.getElementById("result"); - container.innerHTML = ""; - - const div = document.createElement("div"); - div.className = `names-list${names.length === 0 ? " empty" : ""}`; - div.textContent = names.length === 0 ? "No names." : names.join(", "); - - container.appendChild(div); -} - -// ============================================================================= -// DEMO ACTIONS (forEach, map, filter) -// ============================================================================= - function showAllMentors() { document.getElementById("resultLabel").textContent = "Result: forEach – all mentors"; @@ -136,7 +174,8 @@ function showNamesOnly() { document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; - const mentorNames = mapHomemade(mentors, function (mentor) { + // MAP TODO: implement showing only the names of the mentors instead + const mentorNames = mentors.map(function (mentor) { return mentor.name; }); @@ -147,7 +186,8 @@ function showExperienced() { document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; - const experiencedMentors = filterHomemade(mentors, function (mentor) { + // FILTER TODO: implement showing only the mentors with more than 7 years of experience instead + const experiencedMentors = mentors.filter(function (mentor) { return mentor.yearOfExperience > 7; }); @@ -156,28 +196,16 @@ function showExperienced() { function showNamesStartingWithA() { document.getElementById("resultLabel").textContent = - 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + 'Result: filter (multiple conditions) – name starts with "A" and yearOfExperience > 2'; - const mentorsThatStartWithA = filterHomemade(mentors, function (mentor) { - return mentor.name[0] === "A"; - }); + // CHAINING TODO: implement – filter to name starts with "A", then filter to yearOfExperience > 2 + const mentorsThatStartWithA = mentors + .filter(function (mentor) { + return mentor.name[0] === "A" + }) + .filter(function (mentor) { + return mentor.yearOfExperience > 2; + }); renderCards(mentorsThatStartWithA); } - -// ============================================================================= -// BUTTON ACTIVE STATE (called from onclick in HTML) -// ============================================================================= - -function setActive(clickedBtn) { - const buttons = document.querySelectorAll(".actions button"); - - for (let i = 0; i < buttons.length; i++) { - buttons[i].classList.remove("active"); - } - - clickedBtn.classList.add("active"); -} - -// Default: show all mentors on load -showAllMentors(); diff --git a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js index 27440d73..5968b778 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js +++ b/courses/frontend/advanced-javascript/week1/session-materials/mentors-demo/index.js @@ -36,40 +36,105 @@ const mentors = [ ]; // ============================================================================= -// HOMEMADE ARRAY FUNCTIONS (our own implementations) +// RENDER HELPERS (already implemented – go through with trainees) +// ============================================================================= + +/** + * Create one card element for a mentor + */ +function renderMentorCard(mentor) { + const div = document.createElement("div"); + div.className = "card"; + + const subjectSpans = mentor.subjects.map(function (subject) { + return `${subject}`; + }); + const subjectsHtml = subjectSpans.join(""); + + div.innerHTML = ` +
${mentor.name}
+
${mentor.yearOfExperience} years experience
+
${subjectsHtml}
+ `; + + return div; +} + +/** + * Clear #result and show names as comma-separated text (or "No names." if empty). + */ +function renderNamesList(names) { + const container = document.getElementById("result"); + container.innerHTML = ""; + + const div = document.createElement("div"); + div.className = `names-list${names.length === 0 ? " empty" : ""}`; + div.textContent = names.length === 0 ? "No names." : names.join(", "); + + container.appendChild(div); +} + +/** + * Helper function to set the active button + */ +function setActive(clickedBtn) { + const buttons = document.querySelectorAll(".actions button"); + + for (let i = 0; i < buttons.length; i++) { + buttons[i].classList.remove("active"); + } + + clickedBtn.classList.add("active"); +} + +// Default: show all mentors on load +showAllMentors(); + +// ============================================================================= +// HOMEMADE ARRAY FUNCTIONS (implement these during the session for demostration) // ============================================================================= /** * Executes function for each item in the array, NO RETURN! * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach */ -function forEachHomemade(array, functionToExecute) {} +function forEachHomemade(array, functionToExecute) { + // FOREACH TODO: implement forEach +} /** * Changes/transforms the items in the array. Returns a new array with the transformed items. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map */ -function mapHomemade(array, functionToExecute) {} +function mapHomemade(array, functionToExecute) { + // MAP TODO: implement map +} /** * Changes the number of items in the array based on a condition. Returns a new array with only the items that satisfy the condition. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter */ -function filterHomemade(array, functionToExecute) {} +function filterHomemade(array, functionToExecute) { + // FILTER TODO: implement filter +} // ============================================================================= -// RENDER HELPERS +// DEMO ACTIONS (forEach, map, filter) // ============================================================================= -function renderMentorCard(mentor) {} +function renderCards(mentorsToShow) { + const container = document.getElementById("result"); + container.innerHTML = ""; -function renderCards(mentorsToShow) {} + const wrapper = document.createElement("div"); + wrapper.className = "cards"; -function renderNamesList(names) {} + // FOREACH TODO: implement showing all mentors as cards instead + const firstMentor = mentorsToShow[0]; + wrapper.appendChild(renderMentorCard(firstMentor)); -// ============================================================================= -// DEMO ACTIONS (forEach, map, filter) -// ============================================================================= + container.appendChild(wrapper); +} function showAllMentors() { document.getElementById("resultLabel").textContent = @@ -82,6 +147,7 @@ function showNamesOnly() { document.getElementById("resultLabel").textContent = "Result: map – mentor names (array of strings)"; + // MAP TODO: implement showing only the names of the mentors instead const mentorNames = []; renderNamesList(mentorNames); @@ -91,6 +157,7 @@ function showExperienced() { document.getElementById("resultLabel").textContent = "Result: filter – yearOfExperience > 7"; + // FILTER TODO: implement showing only the mentors with more than 7 years of experience instead const experiencedMentors = mentors; renderCards(experiencedMentors); @@ -98,26 +165,10 @@ function showExperienced() { function showNamesStartingWithA() { document.getElementById("resultLabel").textContent = - 'Result: filter – name[0] === "A" (note: "allan" is lowercase)'; + 'Result: filter (multiple conditions) – name starts with "A" and yearOfExperience > 2'; + // CHAINING TODO: implement – filter to name starts with "A", then filter to yearOfExperience > 2 const mentorsThatStartWithA = mentors; renderCards(mentorsThatStartWithA); } - -// ============================================================================= -// BUTTON ACTIVE STATE (called from onclick in HTML) -// ============================================================================= - -function setActive(clickedBtn) { - const buttons = document.querySelectorAll(".actions button"); - - for (let i = 0; i < buttons.length; i++) { - buttons[i].classList.remove("active"); - } - - clickedBtn.classList.add("active"); -} - -// Default: show all mentors on load -showAllMentors(); diff --git a/courses/frontend/advanced-javascript/week1/session-plan.md b/courses/frontend/advanced-javascript/week1/session-plan.md index faa6e38f..42c52dca 100644 --- a/courses/frontend/advanced-javascript/week1/session-plan.md +++ b/courses/frontend/advanced-javascript/week1/session-plan.md @@ -10,6 +10,10 @@ These are some examples of previously created materials by mentors that you can - [Notion Page Handout](https://dandy-birth-1b2.notion.site/HYF-Aarhus-JS-2-Week-2-cd0c1163d0264215824dc17580c97825?pvs=4) (by [Thomas](https://github.com/te-online)) +### Mentors demo + +The [Mentors demo](./session-materials/mentors-demo/) is an in-session demo for forEach, map, filter, and chained filter. Use **index.js** to implement the TODOs in class (FOREACH, MAP, FILTER, CHAINING). The [README](./session-materials/mentors-demo/README.md) describes the files, how tasks are marked, and how the code works. **index-solution.js** has the full implementation for reference. + ## Session Outline @@ -20,17 +24,21 @@ Write this code with traditional `function`s, no arrow functions yet! - Try to write your own `forEach`, `map` and `filter` with the trainees. Shows very precisely how it works! - `forEach` - Executes function for each item in the array, NO RETURN! + - [Mentors demo](./session-materials/mentors-demo/) – do **FOREACH TODO** - [Code inspiration](./session-materials/code-inspiration.md#foreach) - [forEach homemade](./session-materials/code-inspiration.md#foreach-homemade) - [Exercises](./session-materials/exercises.md#foreach) - `map` - Changes/transforms the items in the array + - [Mentors demo](./session-materials/mentors-demo/) – do **MAP TODO** - [Code inspiration](./session-materials/code-inspiration.md#map) - [map homemade](./session-materials/code-inspiration.md#map-homemade) - [Exercises](./session-materials/exercises.md#map) - `filter` - Changes the number of items in the array. Let the trainees investigate `filter` + - [Mentors demo](./session-materials/mentors-demo/) – do **FILTER TODO** - [Code inspiration](./session-materials/code-inspiration.md#filter) - [filter homemade](./session-materials/code-inspiration.md#filter-homemade) - Get help from trainees to write this - [Exercises](./session-materials/exercises.md#filter) +- [Mentors demo](./session-materials/mentors-demo/) – do **CHAINING TODO** (multiple filter conditions) - [Other example](./session-materials/code-inspiration.md#other-example) ### Arrow Functions From 4c4163a9fb2617e4c8f9147507048c115599387e Mon Sep 17 00:00:00 2001 From: markitosha Date: Mon, 9 Mar 2026 14:33:52 +0100 Subject: [PATCH 5/8] fix: linter --- .../week1/session-materials/exercises.md | 39 ++++++-------- .../session-materials/listings-demo/README.md | 54 +++++++++---------- .../listings-demo/index.html | 18 +++++-- .../session-materials/mentors-demo/README.md | 12 ++--- .../mentors-demo/index-solution.js | 2 +- .../advanced-javascript/week1/session-plan.md | 8 +-- package-lock.json | 2 +- 7 files changed, 71 insertions(+), 64 deletions(-) diff --git a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md index 465fde15..4b42ffea 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/exercises.md +++ b/courses/frontend/advanced-javascript/week1/session-materials/exercises.md @@ -2,58 +2,53 @@ Work in the [Listings demo](./listings-demo/). Open **index.html** in your browser to run the demo, and open **index.js** in your IDE to implement the tasks. In `index.js`, search for **`Task`** (e.g. Task 1.1, Task 2)—each task has a comment with explicit instructions about what code to write. This document describes the **end-result** you’re aiming for and how the exercise is set up. The generation of random listings and the render helpers (cards, price list) are already implemented; you only need to call them and add the logic that uses `.forEach()`, `.map()`, and `.filter()`. - ## Task 1: Generate and show listings (`forEach`) **What should happen:** When the user clicks **Generate listings (37) — Task 1**, 37 random listings are generated and displayed as cards on the page. **Listing** (each object in the generated array): -| Property | Type | Description | -|---------------|----------|-------------| -| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | -| `facilities` | string[] | e.g. `["Parkering", "Have"]` | -| `price` | number | 1–10000 | -| `hasGarden` | boolean | | -| `size` | number | m², 12–1000 | -| `img` | string | image URL | +| Property | Type | Description | +| ------------ | -------- | ----------------------------------------------------------- | +| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | +| `facilities` | string[] | e.g. `["Parkering", "Have"]` | +| `price` | number | 1–10000 | +| `hasGarden` | boolean | | +| `size` | number | m², 12–1000 | +| `img` | string | image URL | You implement the logic that (1) triggers generation and (2) renders the listings by iterating over the array and adding one card per listing. The code that builds each card and prepares the view is already there; you use `listings.forEach(...)` to show every listing. - ## Task 2: Show prices (`map`) **What should happen:** After generating listings, when the user clicks **Show prices — Task 2**, a list of all listing prices appears (e.g. comma-separated) in the prices area. You implement getting an array of prices from the current listings and passing it to the helper that displays numbers. Use `.map()` to turn the list of listings into a list of prices. - ## Task 3: Filter buttons (`filter`) **What should happen:** When the user clicks one of the filter buttons, the page shows only the matching listings (or their prices). - **Cheap listings — Task 3a:** Only “cheap” listings are shown as cards. You decide what “cheap” means (e.g. price below a certain number). Use `.filter()` and then the same render-as-cards logic as in Task 1. -- **Expensive prices — Task 3b:** Only the *prices* of “expensive” listings are shown (as numbers). Use `.filter()` to get expensive listings, then `.map()` to get their prices, and the helper that displays numbers. +- **Expensive prices — Task 3b:** Only the _prices_ of “expensive” listings are shown (as numbers). Use `.filter()` to get expensive listings, then `.map()` to get their prices, and the helper that displays numbers. - **With parking — Task 3c:** Only listings that have “Parkering” in their facilities are shown as cards. Use `.filter()` on the facilities. - ## Task: Arrow functions Rewrite the code you wrote for Tasks 1–3 to use arrow function syntax instead of `function`. - ## Task 4: Advanced filters (Listing project) -**What should happen:** The user can set type, min price, min size, facilities, and garden in the Advanced filters form. When they click **Apply filters — Task 4**, only listings that match *all* selected criteria are shown as cards. +**What should happen:** The user can set type, min price, min size, facilities, and garden in the Advanced filters form. When they click **Apply filters — Task 4**, only listings that match _all_ selected criteria are shown as cards. You implement **filterListings(listings, filter)**. Reading the form and building the `filter` object is already done; you receive an object with only the fields the user set. Return only listings that match every property present in `filter`. If a property is missing from `filter`, don’t filter by it. **Filter** (object passed to `filterListings`; only set fields are present): -| Property | Type | Meaning | -|---------------|----------|---------| -| `type` | string | `listing.type` must equal this. | -| `minPrice` | number | `listing.price` must be ≥ this. | -| `minSize` | number | `listing.size` must be ≥ this (m²). | -| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | -| `facilities` | string[] | `listing.facilities` must include every string in this array. | +| Property | Type | Meaning | +| ------------ | -------- | ------------------------------------------------------------- | +| `type` | string | `listing.type` must equal this. | +| `minPrice` | number | `listing.price` must be ≥ this. | +| `minSize` | number | `listing.size` must be ≥ this (m²). | +| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | +| `facilities` | string[] | `listing.facilities` must include every string in this array. | diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md index 27dadbe9..08c497df 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/README.md @@ -4,11 +4,11 @@ Exercise for **trainees** (Week 1). Generate random listings, render them as car ## Files -| File | Purpose | -|------|--------| -| **index.js** | Main file. Implement the task functions (search for `Task`). | +| File | Purpose | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| **index.js** | Main file. Implement the task functions (search for `Task`). | | **index.html** | Page with Generate button, Show prices, filters, advanced filters form, and result areas. Buttons pass `currentListings` into the task functions. | -| **style.css** | Styles for cards and layout. | +| **style.css** | Styles for cards and layout. | ## Where to find tasks @@ -20,14 +20,14 @@ Search for **`Task`** (e.g. Task 1.1, Task 2) in `index.js`. Each task has a com Each button is labeled with its task number in the UI. -| Element | Task | Action | Output | -|--------|------|--------|--------| -| **Generate listings (37) — Task 1** | 1 | `generateAndRenderListings()` | **#cards** | -| **Show prices — Task 2** | 2 | `showPrices(currentListings)` | **#prices** | -| **Cheap listings — Task 3a** | 3a | `showCheapListings(currentListings)` | **#cards** | -| **Expensive prices — Task 3b** | 3b | `showExpensivePrices(currentListings)` | **#expensive-prices** | -| **With parking — Task 3c** | 3c | `showListingsWithParking(currentListings)` | **#cards** | -| **Apply filters — Task 4** | 4 | `applyAdvancedFilters()` (form: type, min price, min size, facilities, garden) | **#cards** | +| Element | Task | Action | Output | +| ----------------------------------- | ---- | ------------------------------------------------------------------------------ | --------------------- | +| **Generate listings (37) — Task 1** | 1 | `generateAndRenderListings()` | **#cards** | +| **Show prices — Task 2** | 2 | `showPrices(currentListings)` | **#prices** | +| **Cheap listings — Task 3a** | 3a | `showCheapListings(currentListings)` | **#cards** | +| **Expensive prices — Task 3b** | 3b | `showExpensivePrices(currentListings)` | **#expensive-prices** | +| **With parking — Task 3c** | 3c | `showListingsWithParking(currentListings)` | **#cards** | +| **Apply filters — Task 4** | 4 | `applyAdvancedFilters()` (form: type, min price, min size, facilities, garden) | **#cards** | Other elements: **#count** (listing count), **#empty** (empty state message). @@ -44,23 +44,23 @@ Other elements: **#count** (listing count), **#empty** (empty state message). **Listing** (each object in the generated array): -| Property | Type | Description | -|-------------|-----------|-------------| -| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | -| `facilities`| string[] | e.g. `["Parkering", "Have"]` | -| `price` | number | 1–10000 | -| `hasGarden` | boolean | | -| `size` | number | m², 12–1000 | -| `img` | string | image URL | +| Property | Type | Description | +| ------------ | -------- | ----------------------------------------------------------- | +| `type` | string | e.g. `"House"`, `"Apartment"`, `"Shed"`, `"Dorm"`, `"Farm"` | +| `facilities` | string[] | e.g. `["Parkering", "Have"]` | +| `price` | number | 1–10000 | +| `hasGarden` | boolean | | +| `size` | number | m², 12–1000 | +| `img` | string | image URL | **Filter** (object passed to `filterListings`; built from the Advanced filters form; only set fields are present): -| Property | Type | Meaning | -|-------------|----------|---------| -| `type` | string | `listing.type` must equal this. | -| `minPrice` | number | `listing.price` must be ≥ this. | -| `minSize` | number | `listing.size` must be ≥ this (m²). | -| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | -| `facilities`| string[] | `listing.facilities` must include every string in this array. | +| Property | Type | Meaning | +| ------------ | -------- | ------------------------------------------------------------- | +| `type` | string | `listing.type` must equal this. | +| `minPrice` | number | `listing.price` must be ≥ this. | +| `minSize` | number | `listing.size` must be ≥ this (m²). | +| `hasGarden` | boolean | If `true`, `listing.hasGarden` must be `true`. | +| `facilities` | string[] | `listing.facilities` must include every string in this array. | If a filter property is missing, do not filter by it. Return listings that match every present property. diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html index 66055c35..4e8a064c 100644 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html +++ b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.html @@ -17,7 +17,11 @@

Listings demo – Generate and render

> Generate listings (37) — Task 1 - No listings yet. Click Generate. @@ -25,10 +29,18 @@

Listings demo – Generate and render

Filters (Task 3): - - - - No listings yet. Click Generate. -
- -
- Filters (Task 3): - - - -
- -
- Advanced filters (Task 4 – Listing project): -
- - -
-
- - -
-
- - -
-
- - - - - - -
-
- -
- -
- -
- Click “Generate listings” to create 37 random listings and render them as - cards. -
-
-
-
- - - - diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js deleted file mode 100644 index cc58af2e..00000000 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/index.js +++ /dev/null @@ -1,297 +0,0 @@ -// ============================================================================= -// DATA & HELPERS -// ============================================================================= - -let currentListings = []; - -function randomIntFromInterval(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -/* - * Listing structure (each object in the listings array): - * type {string} – e.g. "House", "Apartment", "Shed", "Dorm", "Farm" - * facilities {string[]} – e.g. ["Parkering", "Have"] - * price {number} – 1–10000 - * hasGarden {boolean} - * size {number} – m², 12–1000 - * img {string} – image URL - * - * Creates an array of random listing objects and assigns it to currentListings (global). - * Returns the same array. - */ -function generateListings(numberOfListings) { - const listings = []; - const listingType = ["House", "Apartment", "Shed", "Dorm", "Farm"]; - const listingFacilities = [ - "Parkering", - "Elevator", - "Altan", - "Have", - "Husdyr", - ]; - - for (let i = 0; i < numberOfListings; i++) { - const listing = {}; - const randomTypeIndex = randomIntFromInterval(0, listingType.length - 1); - const numberOfFacilities = randomIntFromInterval( - 1, - listingFacilities.length - 1, - ); - const facilities = []; - for (let j = 0; j < numberOfFacilities; j++) { - const randomIndexFacilities = randomIntFromInterval( - 0, - listingFacilities.length - 1, - ); - const randomFacility = listingFacilities[randomIndexFacilities]; - if (!facilities.includes(randomFacility)) { - facilities.push(randomFacility); - } - } - listing.type = listingType[randomTypeIndex]; - listing.facilities = facilities; - listing.price = randomIntFromInterval(1, 10000); - listing.hasGarden = Boolean(randomIntFromInterval(0, 1)); - listing.size = randomIntFromInterval(12, 1000); - listing.img = `https://loremflickr.com/200/200/${listing.type}`; - listings.push(listing); - } - - currentListings = listings; - - return listings; -} - -// ============================================================================= -// RENDER HELPERS (already implemented) -// ============================================================================= - -/** - * Build one card element for a single listing. - */ -function renderListingCard(listing) { - const card = document.createElement("div"); - card.className = "card"; - const badgeClass = function (f) { - return f === "Have" ? " garden" : ""; - }; - const badgesHtml = listing.facilities - .map(function (f) { - return '' + f + ""; - }) - .join(""); - card.innerHTML = - '
' + - (listing.img - ? '' - : listing.type) + - "
" + - '
' + - '
' + - listing.type + - "
" + - '
' + - listing.price.toLocaleString() + - " kr
" + - '
' + - listing.size + - " m²" + - (listing.hasGarden ? " · Garden" : "") + - "
" + - '
' + - badgesHtml + - "
" + - "
"; - return card; -} - -/** - * Update count and empty state, clear #cards. Returns the container or null if no listings. - */ -function prepareListingsView(listings) { - const container = document.getElementById("cards"); - const countEl = document.getElementById("count"); - const emptyEl = document.getElementById("empty"); - - countEl.textContent = - listings.length + " listing" + (listings.length !== 1 ? "s" : ""); - - if (listings.length === 0) { - container.innerHTML = ""; - - if (emptyEl) { - emptyEl.textContent = "No listings."; - emptyEl.style.display = "block"; - } - - return null; - } - - if (emptyEl) { - emptyEl.style.display = "none"; - } - - container.innerHTML = ""; - - return container; -} - -/** - * Display an array of numbers in a given element (e.g. #prices or #expensive-prices). - * Clears the element and shows the numbers as comma-separated text. - */ -function renderNumbersInElement(numbers, elementId) { - const el = document.getElementById(elementId); - - el.innerHTML = ""; - el.textContent = numbers.length === 0 ? "" : numbers.join(", "); -} - -// ============================================================================= -// ADVANCED FILTERS HELPERS -// ============================================================================= - -/** - * Read Advanced filters form and return a filter object. Only includes set values. - */ -function getFilterFromForm() { - const type = document.getElementById("advType").value || undefined; - const minPriceRaw = document.getElementById("advMinPrice").value; - const minPrice = minPriceRaw === "" ? undefined : parseInt(minPriceRaw, 10); - const minSizeRaw = document.getElementById("advMinSize").value; - const minSize = minSizeRaw === "" ? undefined : parseInt(minSizeRaw, 10); - const hasGarden = - document.getElementById("advHasGarden").checked || undefined; - const facilityCheckboxes = document.querySelectorAll( - 'input[name="advFacility"]:checked', - ); - const facilities = Array.from(facilityCheckboxes).map(function (cb) { - return cb.value; - }); - - const filter = {}; - - if (type) { - filter.type = type; - } - - if (minPrice != null && !isNaN(minPrice)) { - filter.minPrice = minPrice; - } - - if (minSize != null && !isNaN(minSize)) { - filter.minSize = minSize; - } - - if (hasGarden) { - filter.hasGarden = true; - } - - if (facilities.length > 0) { - filter.facilities = facilities; - } - - return filter; -} - -function applyAdvancedFilters() { - const filter = getFilterFromForm(); - const filtered = filterListings(currentListings, filter); - - renderListings(filtered); -} - -// ============================================================================= -// TASKS (implement the functions below – search for Task N) -// ============================================================================= - -/* - * Task 1. Implement two things: - * - * 1. generateAndRenderListings: call generateListings(37) (it generates listings for further use), then - * call renderListings(currentListings). - * - * 2. renderListings: use prepareListingsView(listings) first; if it returns null, return. - * Otherwise use the returned container: call listings.forEach(function (listing) { ... }) - * and append renderListingCard(listing) to the container. - */ -function generateAndRenderListings() { - /* - * Task 1.1: call generateListings(37) to generate the data, then call renderListings(currentListings) to render it - */ -} - -function renderListings(listings) { - const container = prepareListingsView(listings); - - if (!container) { - return; - } - - /* - * Task 1.2: add listings.forEach(...) here and append renderListingCard(listing) to container - */ -} - -/* - * Task 2. Implement showPrices using map: create an array of prices from listings - * (one price per listing), then call renderNumbersInElement(prices, "prices"). - */ -function showPrices(listings) { - /* - * Task 2: use map to get an array of prices from listings, then renderNumbersInElement(prices, "prices") - */ -} - -/* - * Task 3. Implement three filter functions: - * - * 3a. showCheapListings: use filter to get listings that are "cheap" (you define the condition, e.g. price below a number). - * Result: array of **objects**. Call renderListings with that array. - * - * 3b. showExpensivePrices: use filter to get expensive listings, then use map to get an array of their prices (numbers). - * Result: array of **numbers**. Call renderNumbersInElement(prices, "expensive-prices"). - * - * 3c. showListingsWithParking: use filter to get listings that have "Parkering" in facilities. - * Result: array of **objects**. Call renderListings with that array. - */ -function showCheapListings(listings) { - /* - * Task 3a: use filter on listings for cheap (e.g. by price), then renderListings(...) - */ -} - -function showExpensivePrices(listings) { - /* - * Task 3b: use filter on listings for expensive, then map to prices, then renderNumbersInElement(prices, "expensive-prices") - */ -} - -function showListingsWithParking(listings) { - /* - * Task 3c: use filter on listings for "Parkering" in facilities, then renderListings(...) - */ -} - -/* - * Task 4. Implement filterListings(listings, filter). - * - * The filter object (built from the Advanced filters form) can have these properties. Omit a property - * if the user left the field empty or unchecked – in that case, do not filter by it. - * - * filter.type {string} – listing.type must equal this (e.g. "Farm", "House"). - * filter.minPrice {number} – listing.price must be >= filter.minPrice. - * filter.minSize {number} – listing.size must be >= filter.minSize (m²). - * filter.hasGarden {boolean} – if true, listing.hasGarden must be true. - * filter.facilities {string[]} – listing.facilities must include every string in this array - * (e.g. ["Parkering", "Have"] means the listing must have both). - * - * Return only listings that match every present filter property. - */ -function filterListings(listings, filter) { - /* - * Task 4: return listings that match every key in filter (see comment above for format) - */ - return listings; -} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css b/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css deleted file mode 100644 index 36d38a02..00000000 --- a/courses/frontend/advanced-javascript/week1/session-materials/listings-demo/style.css +++ /dev/null @@ -1,278 +0,0 @@ -/* ============================================================================= - Listings demo – Base & layout - ============================================================================= */ - -* { - box-sizing: border-box; -} - -body { - font-family: "Segoe UI", system-ui, sans-serif; - margin: 0; - padding: 1.5rem; - background: #f0f2f5; - color: #1a1a1a; -} - -h1 { - margin-top: 0; - font-size: 1.5rem; -} - -/* ============================================================================= - Toolbar - ============================================================================= */ - -.toolbar { - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: flex-end; - margin-bottom: 1.5rem; - padding: 1rem; - background: #fff; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); -} - -.count { - font-size: 0.9rem; - color: #666; - margin-left: auto; - align-self: center; -} - -/* ============================================================================= - Buttons - ============================================================================= */ - -button { - padding: 0.6rem 1.2rem; - border: none; - border-radius: 6px; - font-size: 0.95rem; - font-weight: 600; - cursor: pointer; -} - -.btn-generate { - background: #2563eb; - color: #fff; -} - -.btn-generate:hover { - background: #1d4ed8; -} - -.btn-prices { - background: #e0e7ff; - color: #3730a3; -} - -.btn-prices:hover { - background: #c7d2fe; -} - -.btn-filter { - background: #d1fae5; - color: #065f46; -} - -.btn-filter:hover { - background: #a7f3d0; -} - -.btn-apply { - background: #7c3aed; - color: #fff; -} - -.btn-apply:hover { - background: #6d28d9; -} - -/* ============================================================================= - Filters block (Task 3) - ============================================================================= */ - -.filters { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: center; - margin-bottom: 1rem; - padding: 0.75rem 1rem; - background: #fff; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); -} - -.filters-label { - font-size: 0.9rem; - font-weight: 600; - color: #555; -} - -/* ============================================================================= - Advanced filters form (Task 4) - ============================================================================= */ - -.advanced-filters { - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: flex-end; - margin-bottom: 1rem; - padding: 1rem; - background: #fff; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); - border: 1px solid #e5e7eb; -} - -.advanced-filters .filters-label { - width: 100%; - margin-bottom: 0.25rem; -} - -.field { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.field label { - font-size: 0.85rem; - font-weight: 600; - color: #555; -} - -.facilities { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: center; -} - -.facilities label { - display: flex; - align-items: center; - gap: 0.35rem; - font-weight: normal; - font-size: 0.9rem; - cursor: pointer; -} - -select, -input[type="number"] { - padding: 0.5rem 0.75rem; - border: 1px solid #ccc; - border-radius: 6px; - font-size: 0.95rem; -} - -/* ============================================================================= - Cards grid - ============================================================================= */ - -.cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - gap: 1rem; -} - -.card { - background: #fff; - border-radius: 10px; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: - transform 0.2s, - box-shadow 0.2s; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); -} - -.card-image { - height: 140px; - background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%); - display: flex; - align-items: center; - justify-content: center; - color: #64748b; - font-size: 0.85rem; -} - -.card-image img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.card-body { - padding: 1rem; -} - -.card-type { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: #64748b; - margin-bottom: 0.25rem; -} - -.card-price { - font-size: 1.25rem; - font-weight: 700; - color: #1e293b; -} - -.card-meta { - font-size: 0.9rem; - color: #64748b; - margin-top: 0.5rem; -} - -.card-facilities { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - margin-top: 0.75rem; -} - -.badge { - font-size: 0.7rem; - padding: 0.2rem 0.5rem; - background: #e0e7ff; - color: #3730a3; - border-radius: 4px; -} - -.badge.garden { - background: #d1fae5; - color: #065f46; -} - -/* ============================================================================= - Empty state & prices output - ============================================================================= */ - -.empty { - text-align: center; - padding: 3rem; - color: #64748b; - background: #fff; - border-radius: 10px; -} - -.prices { - margin: 1rem 0; - padding: 1rem; - background: #fff; - border-radius: 8px; - font-size: 0.9rem; - color: #374151; -} diff --git a/courses/frontend/advanced-javascript/week1/session-materials/style.css b/courses/frontend/advanced-javascript/week1/session-materials/style.css deleted file mode 100644 index 6204ef44..00000000 --- a/courses/frontend/advanced-javascript/week1/session-materials/style.css +++ /dev/null @@ -1,37 +0,0 @@ -/* ============================================================================= - Session materials – Landing - ============================================================================= */ - -body { - font-family: system-ui, sans-serif; - padding: 2rem; - max-width: 600px; -} - -h1 { - font-size: 1.25rem; -} - -ul { - list-style: none; - padding: 0; -} - -li { - margin-bottom: 0.75rem; -} - -a { - color: #2563eb; - font-weight: 500; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -p { - color: #64748b; - font-size: 0.9rem; -} From f3d7169f63857347eb711b08e204d33398751bdd Mon Sep 17 00:00:00 2001 From: markitosha Date: Mon, 9 Mar 2026 20:15:56 +0100 Subject: [PATCH 7/8] fix: more clarify in assigment --- courses/frontend/advanced-javascript/week1/assignment.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/courses/frontend/advanced-javascript/week1/assignment.md b/courses/frontend/advanced-javascript/week1/assignment.md index c35a57c6..b08d8c47 100644 --- a/courses/frontend/advanced-javascript/week1/assignment.md +++ b/courses/frontend/advanced-javascript/week1/assignment.md @@ -1,8 +1,11 @@ # Assignment -Only **task 3** (movies) requires a frontend: HTML + CSS + JavaScript that runs in the browser, with the result visible in the page. **Tasks 1 and 2** do not: for task 1 submit the JavaScript (map/filter) solution (an HTML page is optional though); for task 2 just complete the Katas. +Tasks 1 and 2 are short warmups; task 3 is a larger, real-world-style task where everything is visible in one page or app. + +- **Task 1 (Doubling of number):** JavaScript only. Submit your solution using `map` and `filter` (and arrow functions). +- **Task 2 (Codewars):** Complete the Katas on Codewars. No frontend or repo submission needed. +- **Task 3 (Working with movies):** Frontend required. Build an HTML page with CSS and JavaScript that runs in the browser and shows the result of each movie sub-task in the page (e.g. sections or cards with the computed data). -The first task is a short warmup. The second is practice on Codewars. The third (movies) is a larger, real-world-style task: one page or app that shows all the movie results in the UI. ## 1. Doubling of number @@ -22,7 +25,7 @@ for (let i = 0; i < numbers.length; i++) { // expected result: [2, 6] ``` -Rewrite the above program using `map` and `filter`; don't forget to use arrow functions. Showing the result in a page is optional for this task (but nice if you do). +Rewrite the above program using `map` and `filter`; don't forget to use arrow functions. ## 2. Codewars! From 43cc4a2b69b12d00f36dac12c22fb3f8e9ba79d5 Mon Sep 17 00:00:00 2001 From: markitosha Date: Mon, 9 Mar 2026 20:16:23 +0100 Subject: [PATCH 8/8] fix: more clarify in assigment --- courses/frontend/advanced-javascript/week1/assignment.md | 1 - 1 file changed, 1 deletion(-) diff --git a/courses/frontend/advanced-javascript/week1/assignment.md b/courses/frontend/advanced-javascript/week1/assignment.md index b08d8c47..a5882d32 100644 --- a/courses/frontend/advanced-javascript/week1/assignment.md +++ b/courses/frontend/advanced-javascript/week1/assignment.md @@ -6,7 +6,6 @@ Tasks 1 and 2 are short warmups; task 3 is a larger, real-world-style task where - **Task 2 (Codewars):** Complete the Katas on Codewars. No frontend or repo submission needed. - **Task 3 (Working with movies):** Frontend required. Build an HTML page with CSS and JavaScript that runs in the browser and shows the result of each movie sub-task in the page (e.g. sections or cards with the computed data). - ## 1. Doubling of number Say you would like to write a program that **doubles the odd numbers** in an array and **throws away the even number**.