From ecbd98f63ecacbc9595e76bd1c13c0f528bb573c Mon Sep 17 00:00:00 2001 From: markitosha Date: Sun, 22 Mar 2026 09:19:40 +0100 Subject: [PATCH 1/3] updated week3 materials and fixed git problem with week 2 assigment --- SUMMARY.md | 1 + .../advanced-javascript/week2/assignment.md | 12 +- .../advanced-javascript/week3/README.md | 1 + .../advanced-javascript/week3/assignment.md | 14 +- .../week3/session-materials/console-order.md | 139 ++++++++ .../week3/session-materials/demo/README.md | 45 +++ .../session-materials/demo/index-solution.js | 151 ++++++++ .../week3/session-materials/demo/index.html | 12 + .../week3/session-materials/demo/index.js | 62 ++++ .../week3/session-materials/demo/style.css | 34 ++ .../event-loop-demo/event-loop.css | 330 ++++++++++++++++++ .../event-loop-demo/event-loop.html | 97 +++++ .../event-loop-demo/event-loop.js | 104 ++++++ .../week3/session-materials/exercises.md | 54 +++ .../advanced-javascript/week3/session-plan.md | 102 ++---- 15 files changed, 1071 insertions(+), 87 deletions(-) create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/console-order.md create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/demo/README.md create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/demo/index-solution.js create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/demo/index.html create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/demo/index.js create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/demo/style.css create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/exercises.md diff --git a/SUMMARY.md b/SUMMARY.md index 27b9a01c..0ac32e65 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -99,6 +99,7 @@ - [Week 3](courses/frontend/advanced-javascript/week3/README.md) - [Preparation](courses/frontend/advanced-javascript/week3/preparation.md) - [Session Plan](courses/frontend/advanced-javascript/week3/session-plan.md) + - [Exercises](courses/frontend/advanced-javascript/week3/session-materials/exercises.md) - [Assignment](courses/frontend/advanced-javascript/week3/assignment.md) - [Week 4](courses/frontend/advanced-javascript/week4/README.md) - [Preparation](courses/frontend/advanced-javascript/week4/preparation.md) diff --git a/courses/frontend/advanced-javascript/week2/assignment.md b/courses/frontend/advanced-javascript/week2/assignment.md index 85ea610b..836c04ca 100644 --- a/courses/frontend/advanced-javascript/week2/assignment.md +++ b/courses/frontend/advanced-javascript/week2/assignment.md @@ -11,25 +11,25 @@ The warmup is a **little abstract**, it will get more concrete later on! 1. Display the text `Called after 2.5 seconds` on the page 2.5 seconds after the script is loaded. 2. Create a function that takes 2 parameters: `delay` and `stringToLog`. Calling this function should display the `stringToLog` on the page after `delay` seconds. Call the function you have created with some different arguments. - ![second task](session-materials/carbon.png) + ![second task](./session-materials/carbon.png) 3. Create a button in html. When clicking this button, use the function you created in the previous task to display the text `Called after 5 seconds` on the page 5 seconds after the button is clicked. -![second task](session-materials/button-delay.gif) +![second task](./session-materials/button-delay.gif) 4. Create two functions and assign them to two different variables. One function displays `Earth` on the page, the other displays `Saturn`. Now create a new third function that has one parameter: `planetLogFunction`. The only thing the third function should do is call the provided parameter function. Try calling the third function once with the `Earth` function and once with the `Saturn` function. -![second task](session-materials/planet-log.png) +![second task](./session-materials/planet-log.png) 5. Create a button with the text "Log location". When this button is clicked, display the user's location (latitude, longitude) on the page using this [browser API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API). -![second task](session-materials/log-location.gif) +![second task](./session-materials/log-location.gif) 6. _Optional_ Now show that location on a map using e.g. the [Google maps api](https://developers.google.com/maps/documentation/javascript/tutorial) 7. Create a function called `runAfterDelay`. It has two parameters: `delay` and `callback`. When called the function should wait `delay` seconds and then call the provided callback function. Add an input in the HTML for the delay (in seconds) and a button; when the button is clicked, read the delay from the input and call `runAfterDelay` with that delay and a callback that displays something on the page. -![second task](session-materials/run-after-delay.png) +![second task](./session-materials/run-after-delay.png) 8. Check if the user has double-clicked on the page. A double click is two clicks within 0.5 seconds. If a double click is detected, display the text "double click!" on the page. @@ -57,7 +57,7 @@ A user specifies how long time the game should be, and presses **"start game!"** Here is a gif of how the site should work: -![session material](session-materials/fastest-clicker.gif =400x) +![Fastest presser game demo](./session-materials/fastest-clicker.gif) You can implement it exactly like you want to, but here is my recommended order: diff --git a/courses/frontend/advanced-javascript/week3/README.md b/courses/frontend/advanced-javascript/week3/README.md index 430dbb24..3ffd5ab9 100644 --- a/courses/frontend/advanced-javascript/week3/README.md +++ b/courses/frontend/advanced-javascript/week3/README.md @@ -6,6 +6,7 @@ In this session, you'll learn how to write asynchronous code that is both effici - [Preparation](./preparation.md) - [Session Plan](./session-plan.md) (for mentors) +- [Exercises](./session-materials/exercises.md) - [Assignment](./assignment.md) ## Session Learning Goals diff --git a/courses/frontend/advanced-javascript/week3/assignment.md b/courses/frontend/advanced-javascript/week3/assignment.md index 84430345..d8948746 100644 --- a/courses/frontend/advanced-javascript/week3/assignment.md +++ b/courses/frontend/advanced-javascript/week3/assignment.md @@ -1,11 +1,13 @@ # Assignment -The assignment for this week is to build a currency calculator using [this API](https://open.er-api.com/v6/latest/USD) +The assignment for this week is to build a currency calculator using [this API](https://open.er-api.com/v6/latest/USD). + +Deliverable: a small browser application, so the user can interact with it and see the converted amount on the page. ## Technical specifications -1. Make a request to the API and store the Exchange rates as well as a list of currencies for the dropdowns. -2. User can enter an amount -3. User can choose a currency to convert from(default should be EUR) -4. User can choose a currency to convert to(Default should be DKK) -5. Whenever amount, currency from or currency to changes we show what the amount translates to in the to currency +1. Make a request to the API and use the response to obtain exchange rates and to populate the currency dropdowns. +2. The user can enter an amount. +3. The user can choose a currency to convert from (default: EUR). +4. The user can choose a currency to convert to (default: DKK). +5. When the amount, the "from" currency, or the "to" currency changes, show the equivalent amount in the "to" currency. diff --git a/courses/frontend/advanced-javascript/week3/session-materials/console-order.md b/courses/frontend/advanced-javascript/week3/session-materials/console-order.md new file mode 100644 index 00000000..9344678f --- /dev/null +++ b/courses/frontend/advanced-javascript/week3/session-materials/console-order.md @@ -0,0 +1,139 @@ +# Promise chaining – what is logged? + +Use these in class: show the code, ask “What will appear in the console, and in what order?”, then run it and compare. + +--- + +## Task 1 — basic: sync vs `.then` + +```js +console.log("A"); + +Promise.resolve().then(() => { + console.log("B"); +}); + +console.log("C"); +``` + +
+Answer + +Order: A, C, B + +Synchronous code runs first (A, then C). Callbacks passed to `.then` are scheduled as microtasks and run after the current script finishes, so B appears last. + +
+ +--- + +## Task 2 — values through the chain + +```js +Promise.resolve(1) + .then((x) => { + console.log(x); + return x + 1; + }) + .then((y) => { + console.log(y); + }); +``` + +
+Answer + +Logs: `1` then `2` + +Each `.then` receives the value returned by the previous handler. Returning a plain value wraps it in a resolved promise for the next step. + +
+ +--- + +## Task 3 — returning a Promise (flattening) + +```js +Promise.resolve("go") + .then((s) => { + console.log("a:", s); + return Promise.resolve("step"); + }) + .then((t) => { + console.log("b:", t); + }); +``` + +
+Answer + +Logs: `a: go` then `b: step` + +When a handler returns a Promise, the chain waits for it and passes its settled value to the next `.then` (the inner Promise is “flattened”). + +
+ +--- + +## Task 4 — rejection, skipped handlers, `.catch`, recovery + +```js +Promise.resolve() + .then(() => { + console.log("1"); + throw new Error("oops"); + }) + .then(() => { + console.log("2"); + }) + .catch(() => { + console.log("3"); + }) + .then(() => { + console.log("4"); + }); +``` + +
+Answer + +Logs: `1`, `3`, `4` + +The error skips the next `.then` (so `2` never runs). `.catch` handles the rejection; a successful `catch` returns a fulfilled promise, so the following `.then` still runs (`4`). + +
+ +--- + +## Task 5 — multiple `.catch` and `.then` in one chain + +```js +Promise.resolve() + .then(() => { + console.log("1"); + throw "first-error"; + }) + .catch((err) => { + console.log("catch-A", err); + return "recovered"; + }) + .then((value) => { + console.log("2", value); + throw "second-error"; + }) + .catch((err) => { + console.log("catch-B", err); + }) + .then(() => { + console.log("3"); + }); +``` + +
+Answer + +Logs: `1`, `catch-A first-error`, `2 recovered`, `catch-B second-error`, `3` + +The first `throw` is handled by `catch-A`, which returns `"recovered"`, so the chain continues fulfilled and `2` runs with that value. The next `throw` is handled by `catch-B`; a successful `catch` still yields a fulfilled promise, so the final `.then` runs (`3`). The second `.catch` never sees `first-error` because `catch-A` already handled it. + +
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/demo/README.md b/courses/frontend/advanced-javascript/week3/session-materials/demo/README.md new file mode 100644 index 00000000..57b27aec --- /dev/null +++ b/courses/frontend/advanced-javascript/week3/session-materials/demo/README.md @@ -0,0 +1,45 @@ +# Mentors demo – Promises & `async`/`await` + +In-session live coding for **Week 3** (Advanced JavaScript). The demo walks through `fetch` with **JSONPlaceholder**, `async`/`await`, consuming promises with `.then()` / `.catch()`, creating promises with `new Promise`, `try` / `catch` with async code, `Promise.all`, and an optional promise microtask loop. You implement the worksheet during class; the solution file is the finished version. + +--- + +## Files in this folder + +| File | Purpose | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **index.js** | Worksheet: section banners, `// Task:` lines, and `// Next:` hints. Only `getUser` and `promiseLoop` are declared; you add the rest while teaching. Use this file when leading the session. | +| **index-solution.js** | Full implementation: `showOutput`, `getUser`, promise consumption, timed and pizza promises, `try` / `catch` fetch, `Promise.all`, and `promiseLoop`. | +| **index.html** | Minimal page that loads `index.js`. Add markup (e.g. `
`) when you want on-page output; the solution’s `showOutput` writes to `#out`.                                          |
+| **style.css**         | Basic layout and styles for `#out` (and `main` if you use it).                                                                                                                              |
+
+---
+
+## Where to find tasks and how they are marked
+
+Everything lives in **index.js**. Search for `// ==========` for section breaks, `// Task:` for what to build, and `// Next:` for suggested links to the trainee exercises.
+
+---
+
+## How the code works
+
+### URLs (JSONPlaceholder)
+
+- **`USER_URL`** – `https://jsonplaceholder.typicode.com/users/1`
+- **`POST_URL`** – `https://jsonplaceholder.typicode.com/posts/1`
+- **`TODO_URL`** – `https://jsonplaceholder.typicode.com/todos/1` (optional extra)
+
+### Solution-only helpers and functions
+
+- **`showOutput(text)`** – Sets `textContent` on `#out` when that element exists.
+- **`getUser()`** – `async` `fetch` of **`USER_URL`**, then `.json()`, then `showOutput` with stringified user data.
+- **`loadOneResourceWithThen()`** – Same resource with `.then` / `.catch` only (no `async`/`await`).
+- **`oneSecondMessage()`** – Promise that resolves after one second, then shows `"It worked"`.
+- **`demoOrderPizza()`** – Delayed resolve or reject, then shows pizza or error text.
+- **`getUserWithTryCatch()`** – Same fetch pattern as `getUser` with `try` / `catch` and errors on the page.
+- **`demoPromiseAll()`** – Fetches **`USER_URL`** and **`POST_URL`** in parallel, then shows a short two-line summary.
+- **`promiseLoop()`** – Schedules endless microtasks (illustration only; can freeze the tab if called).
+
+### Callback hell
+
+Not implemented in these files; use the session plan (e.g. npm `q` or your own example on the board).
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/demo/index-solution.js b/courses/frontend/advanced-javascript/week3/session-materials/demo/index-solution.js
new file mode 100644
index 00000000..cd5cedab
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/demo/index-solution.js
@@ -0,0 +1,151 @@
+// Week 3 demo – Promises & async/await (solution)
+
+// JSONPlaceholder only (reliable in the browser). Session plan may mention Open Notify —
+// same async ideas, different URL.
+const USER_URL = "https://jsonplaceholder.typicode.com/users/1";
+const POST_URL = "https://jsonplaceholder.typicode.com/posts/1";
+const TODO_URL = "https://jsonplaceholder.typicode.com/todos/1";
+
+function showOutput(text) {
+  const el = document.getElementById("out");
+  if (el) {
+    el.textContent = text;
+  }
+}
+
+// =============================================================================
+// Async/await – simple usage
+// =============================================================================
+// Task: Load USER_URL with async/await
+
+async function getUser() {
+  const response = await fetch(USER_URL);
+  const user = await response.json();
+  showOutput(JSON.stringify(user, null, 2));
+}
+
+// Next: Exercise 1
+
+// =============================================================================
+// Why use Promises? :: Callback Hell
+// =============================================================================
+// Show Callback Hell example in https://www.npmjs.com/package/q
+
+// =============================================================================
+// Promise consumption
+// =============================================================================
+// Task: Load one of the resources (e.g. USER_URL); show success or error on the page using .then / .catch only.
+
+function loadOneResourceWithThen() {
+  showOutput("Loading…");
+  fetch(USER_URL)
+    .then((response) => response.json())
+    .then((data) => {
+      showOutput(JSON.stringify(data, null, 2));
+    })
+    .catch((error) => {
+      showOutput(String(error));
+    });
+}
+
+// Next: Chaining examples
+// Next: Exercise 2
+
+// =============================================================================
+// Promise creation
+// =============================================================================
+// Task: Create a Promise that resolves after 1 second and shows "It worked" on the page.
+// Task: Create demoOrderPizza: a pizza-order Promise — after a 'baking' delay it either resolves with a pizza you can eat (show that on the page) or rejects if baking failed (show the failure on the page).
+
+function oneSecondMessage() {
+  showOutput("…");
+  const oneSecondTimeoutPromise = new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, 1000);
+  });
+
+  oneSecondTimeoutPromise.then(() => {
+    showOutput("It worked");
+  });
+}
+
+function demoOrderPizza() {
+  showOutput("Baking… (3s for demo)");
+  const pizzaMakingTime = 3000;
+  const didPizzaBakingSucceed = true;
+  const pizza = "Macaroni pizza";
+
+  const orderPizzaPromise = new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (didPizzaBakingSucceed) {
+        resolve(pizza);
+      } else {
+        reject("The pizza was a mess");
+      }
+    }, pizzaMakingTime);
+  });
+
+  orderPizzaPromise
+    .then((p) => {
+      showOutput(`Let's eat the ${p}`);
+    })
+    .catch((error) => {
+      showOutput(`Let's eat nothing: ${error}`);
+    });
+}
+
+// Next: Exercise 3
+// Next: Exercise 4
+
+// =============================================================================
+// Back to async/await (try / catch)
+// =============================================================================
+// Task: improve getUser to use try/catch to handle errors and show the error on the page.
+
+async function getUserWithTryCatch() {
+  try {
+    const response = await fetch(USER_URL);
+    const user = await response.json();
+    showOutput(JSON.stringify(user, null, 2));
+  } catch (err) {
+    showOutput(String(err));
+  }
+}
+
+// Next: Exercise 5
+
+// =============================================================================
+// Promise.all
+// =============================================================================
+
+async function demoPromiseAll() {
+  showOutput("Loading both…");
+  try {
+    const [userRes, postRes] = await Promise.all([
+      fetch(USER_URL),
+      fetch(POST_URL),
+    ]);
+    const [user, post] = await Promise.all([userRes.json(), postRes.json()]);
+    const summary = [
+      "User: " + user.name + " (" + user.email + ")",
+      "Post: " + post.title,
+    ].join("\n");
+    showOutput(summary);
+  } catch (e) {
+    showOutput(String(e));
+  }
+}
+
+// Next: Exercise 6
+
+// =============================================================================
+// (Optional) Infinite loop via Promises
+// =============================================================================
+
+function promiseLoop() {
+  return Promise.resolve().then(() => {
+    console.log("tick");
+    return promiseLoop();
+  });
+}
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/demo/index.html b/courses/frontend/advanced-javascript/week3/session-materials/demo/index.html
new file mode 100644
index 00000000..cae179b8
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/demo/index.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    Demo – Week 3
+    
+  
+  
+    
+  
+
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/demo/index.js b/courses/frontend/advanced-javascript/week3/session-materials/demo/index.js
new file mode 100644
index 00000000..be2447e4
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/demo/index.js
@@ -0,0 +1,62 @@
+// Week 3 demo – Promises & async/await (worksheet for class)
+
+// JSONPlaceholder only (reliable in the browser). Session plan may mention Open Notify —
+// same async ideas, different URL.
+const USER_URL = "https://jsonplaceholder.typicode.com/users/1";
+const POST_URL = "https://jsonplaceholder.typicode.com/posts/1";
+const TODO_URL = "https://jsonplaceholder.typicode.com/todos/1";
+
+// =============================================================================
+// Async/await – simple usage
+// =============================================================================
+// Task: Load USER_URL with async/await
+
+async function getUser() {}
+
+// Next: Exercise 1
+
+// =============================================================================
+// Why use Promises? :: Callback Hell
+// =============================================================================
+// Show Callback Hell example in https://www.npmjs.com/package/q
+
+// =============================================================================
+// Promise consumption
+// =============================================================================
+// Task: Load one of the resources (e.g. USER_URL); show success or error on the page using .then / .catch only.
+
+// Next: Chaining examples
+// Next: Exercise 2
+
+// =============================================================================
+// Promise creation
+// =============================================================================
+// Task: Create a Promise that resolves after 1 second and shows "It worked" on the page.
+// Task: Create demoOrderPizza: a pizza-order Promise — after a 'baking' delay it either resolves with a pizza you can eat (show that on the page) or rejects if baking failed (show the failure on the page).
+
+// Next: Exercise 3
+// Next: Exercise 4
+
+// =============================================================================
+// Back to async/await (try / catch)
+// =============================================================================
+// Task: improve getUser to use try/catch to handle errors and show the error on the page.
+
+// Next: Exercise 5
+
+// =============================================================================
+// Promise.all
+// =============================================================================
+
+// Next: Exercise 6
+
+// =============================================================================
+// (Optional) Infinite loop via Promises
+// =============================================================================
+
+function promiseLoop() {
+  return Promise.resolve().then(() => {
+    console.log("tick");
+    return promiseLoop();
+  });
+}
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/demo/style.css b/courses/frontend/advanced-javascript/week3/session-materials/demo/style.css
new file mode 100644
index 00000000..ab055fd9
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/demo/style.css
@@ -0,0 +1,34 @@
+/* Week 3 demo – minimal */
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  font-family: system-ui, sans-serif;
+  line-height: 1.5;
+}
+
+main {
+  max-width: 40rem;
+  margin: 0 auto;
+  padding: 1rem;
+}
+
+#out {
+  margin: 0.25rem 0 1rem;
+  padding: 0.5rem;
+  border: 1px solid #ccc;
+  font-family: ui-monospace, monospace;
+  font-size: 0.8rem;
+  white-space: pre-wrap;
+  word-break: break-word;
+  min-height: 1.25rem;
+}
+
+#out:empty {
+  display: none;
+}
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css
new file mode 100644
index 00000000..65914c0f
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css
@@ -0,0 +1,330 @@
+/* Event loop + microtasks visualization – light theme */
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  font-family:
+    system-ui,
+    -apple-system,
+    sans-serif;
+  line-height: 1.5;
+  background: #f5f5f5;
+  color: #222;
+}
+
+main {
+  max-width: 72rem;
+  margin: 0 auto;
+  padding: 1.5rem;
+}
+
+h1 {
+  margin: 0 0 0.5rem;
+  font-size: 1.5rem;
+  color: #1967d2;
+}
+
+.intro {
+  margin: 0 0 1.25rem;
+  font-size: 0.9rem;
+  color: #5f6368;
+  max-width: 52rem;
+}
+
+.intro code {
+  font-size: 0.85em;
+  padding: 0.1em 0.35em;
+  background: #e8eaed;
+  border-radius: 4px;
+}
+
+/* Code section – blocks at the start */
+.code-section {
+  margin-bottom: 1.5rem;
+}
+
+.section-label {
+  font-weight: 600;
+  font-size: 0.875rem;
+  margin-bottom: 0.5rem;
+  color: #5f6368;
+}
+
+/* Tooltip: hover on ? to show definition */
+.tooltip-trigger {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 1.1em;
+  height: 1.1em;
+  margin-left: 0.25em;
+  font-size: 0.85em;
+  font-weight: 700;
+  line-height: 1;
+  color: #5f6368;
+  background: #e8eaed;
+  border-radius: 50%;
+  cursor: help;
+  position: relative;
+  vertical-align: middle;
+}
+
+.tooltip-trigger:hover,
+.tooltip-trigger:focus {
+  color: #1967d2;
+  background: #e8f0fe;
+  outline: none;
+}
+
+.tooltip {
+  position: absolute;
+  left: 50%;
+  bottom: calc(100% + 0.5em);
+  transform: translateX(-50%);
+  width: 18em;
+  max-width: 90vw;
+  padding: 0.6em 0.75em;
+  font-size: 0.8rem;
+  font-weight: 400;
+  line-height: 1.4;
+  color: #222;
+  background: #fff;
+  border: 1px solid #dadce0;
+  border-radius: 6px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
+  white-space: normal;
+  visibility: hidden;
+  opacity: 0;
+  transition:
+    opacity 0.15s ease,
+    visibility 0.15s ease;
+  z-index: 20;
+  pointer-events: none;
+}
+
+.tooltip code {
+  font-size: 0.9em;
+}
+
+.tooltip-trigger:hover .tooltip,
+.tooltip-trigger:focus .tooltip {
+  visibility: visible;
+  opacity: 1;
+}
+
+.tooltip::after {
+  content: "";
+  position: absolute;
+  top: 100%;
+  left: 50%;
+  margin-left: -6px;
+  border: 6px solid transparent;
+  border-top-color: #dadce0;
+}
+
+.code-blocks {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+  min-height: 3rem;
+  padding: 0.75rem;
+  background: #fff;
+  border: 1px solid #dadce0;
+  border-radius: 8px;
+}
+
+.code-block,
+.task {
+  cursor: grab;
+}
+
+.code-block:active,
+.task:active {
+  cursor: grabbing;
+}
+
+.code-block.dragging,
+.task.dragging {
+  opacity: 0.6;
+}
+
+.code-block {
+  padding: 0.4rem 0.75rem;
+  border-radius: 4px;
+  font-size: 0.8rem;
+  font-family: ui-monospace, monospace;
+  font-weight: 500;
+  transition: opacity 0.3s ease;
+}
+
+.code-block.sync {
+  background: #e8f0fe;
+  color: #1967d2;
+  border: 1px solid #aecbfa;
+}
+
+.code-block.timeout,
+.code-block.timeout-1 {
+  background: #e6f4ea;
+  color: #137333;
+  border: 1px solid #81c995;
+}
+
+.code-block.timeout-2 {
+  background: #fef7e0;
+  color: #b06000;
+  border: 1px solid #f9d57e;
+}
+
+.code-block.timeout-0 {
+  background: #e8e0ec;
+  color: #7c4dff;
+  border: 1px solid #b39ddb;
+}
+
+.code-block.event {
+  background: #f1f3f4;
+  color: #5f6368;
+  border: 1px solid #dadce0;
+}
+
+/* Promise → microtask queue (same colour in every zone) */
+.code-block.promise-then,
+.code-block.promise-catch {
+  background: #fce8e6;
+  color: #c5221f;
+  border: 1px solid #f5aea8;
+}
+
+.diagram {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 1rem;
+  align-items: stretch;
+  min-height: 10rem;
+  margin-bottom: 1.5rem;
+}
+
+.zone {
+  flex: 1 1 14rem;
+  border: 2px solid #dadce0;
+  border-radius: 8px;
+  padding: 0.75rem;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+}
+
+.zone-label {
+  font-weight: 600;
+  font-size: 0.875rem;
+  margin-bottom: 0.5rem;
+  color: #5f6368;
+}
+
+.call-stack .zone-label {
+  color: #1967d2;
+}
+
+.microtask-queue .zone-label {
+  color: #c5221f;
+}
+
+.timer .zone-label {
+  color: #b06000;
+}
+
+.async-queue .zone-label {
+  color: #137333;
+}
+
+.zone-tasks {
+  flex: 1;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+  align-content: flex-start;
+  align-items: center;
+  min-height: 3.5rem;
+}
+
+.task {
+  padding: 0.35rem 0.6rem;
+  border-radius: 4px;
+  font-size: 0.8rem;
+  font-family: ui-monospace, monospace;
+  font-weight: 500;
+  transition: opacity 0.3s ease;
+}
+
+.task.sync {
+  background: #e8f0fe;
+  color: #1967d2;
+  border: 1px solid #aecbfa;
+}
+
+.task.timeout-1 {
+  background: #e6f4ea;
+  color: #137333;
+  border: 1px solid #81c995;
+}
+
+.task.timeout-2 {
+  background: #fef7e0;
+  color: #b06000;
+  border: 1px solid #f9d57e;
+}
+
+.task.timeout-0 {
+  background: #e8e0ec;
+  color: #7c4dff;
+  border: 1px solid #b39ddb;
+}
+
+.task.event {
+  background: #f1f3f4;
+  color: #5f6368;
+  border: 1px solid #dadce0;
+}
+
+.task.promise-then,
+.task.promise-catch {
+  background: #fce8e6;
+  color: #c5221f;
+  border: 1px solid #f5aea8;
+}
+
+.controls {
+  display: flex;
+  gap: 0.5rem;
+}
+
+.controls button {
+  padding: 0.5rem 1rem;
+  font-size: 0.875rem;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  background: #1967d2;
+  color: #fff;
+  font-weight: 500;
+}
+
+.controls button:hover {
+  background: #1557b0;
+}
+
+.controls button#btn-reset {
+  background: #fff;
+  color: #5f6368;
+  border: 1px solid #dadce0;
+}
+
+.controls button#btn-reset:hover {
+  background: #f1f3f4;
+}
diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html
new file mode 100644
index 00000000..75f112e1
--- /dev/null
+++ b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html
@@ -0,0 +1,97 @@
+
+
+  
+    
+    
+    Event loop & microtasks – Week 3
+    
+  
+  
+    
+

Event loop & microtasks

+

+ Drag blocks where they belong. Same idea as week 2, with one extra box: + work from a Promise (like .then / + .catch) waits in the microtask queue and + runs before the next setTimeout + callback. +

+ +
+ +
+
+ +
+
+
+ Call stack + + ? + Where normal synchronous code runs. When it finishes, + microtasks run, then one callback from the task queue. + +
+
+
+
+
+ Microtask queue + + ? + When a promise settles, the engine queues its + .then / .catch work here. This queue + is emptied before the next setTimeout (or other + task-queue) callback. + +
+
+
+
+
+ Task queue + + ? + Callbacks waiting to run after microtasks — for example from + setTimeout. + +
+
+
+
+
+ Timer (Web APIs) + + ? + When you call setTimeout(fn, ms), the timer waits, + then the callback is queued as a macrotask. Part of the + browser’s Web APIs. + +
+
+
+
+ +
+ +
+
+ + + diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js new file mode 100644 index 00000000..4505037c --- /dev/null +++ b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js @@ -0,0 +1,104 @@ +// Event loop: draggable blocks → Call stack, Microtask queue, Task queue, Timer +// setTimeout: full label in Timer (registration), "fn" elsewhere (the callback) — like week 2. +// Sync and Promise blocks always keep their full label. + +var codeBlocks = document.getElementById("code-blocks"); +var callstackTasks = document.getElementById("callstack-tasks"); +var microtaskTasks = document.getElementById("microtask-tasks"); +var timerTasks = document.getElementById("timer-tasks"); +var asyncTasks = document.getElementById("async-tasks"); +var btnReset = document.getElementById("btn-reset"); + +// Same shape as week 2, plus two promise blocks for the microtask queue. +var INITIAL_BLOCKS = [ + { label: "sync 1", type: "sync" }, + { label: "sync 2", type: "sync" }, + { label: "setTimeout(fn, 2s)", type: "timeout-1" }, + { label: "sync 3", type: "sync" }, + { label: "setTimeout(fn, 0)", type: "timeout-0" }, + { label: "sync 4", type: "sync" }, + { label: "Promise .then(fn)", type: "promise-then" }, + { label: "setTimeout(fn, 4s)", type: "timeout-2" }, + { label: "sync 5", type: "sync" }, + { label: "Promise .catch(fn)", type: "promise-catch" }, +]; + +function createBlock(label, type, isInCodeArea) { + var el = document.createElement("div"); + el.draggable = true; + el.dataset.type = type; + el.dataset.originalLabel = label; + el.textContent = label; + el.className = isInCodeArea ? "code-block " + type : "task " + type; + el.addEventListener("dragstart", onDragStart); + return el; +} + +function isTimeoutType(type) { + return type === "timeout-0" || type === "timeout-1" || type === "timeout-2"; +} + +function onDragStart(ev) { + ev.dataTransfer.effectAllowed = "move"; + ev.dataTransfer.setData("text/plain", ""); + ev.target.classList.add("dragging"); +} + +function onDragEnd(ev) { + ev.target.classList.remove("dragging"); +} + +function makeDropZone(zoneEl, isCodeArea, zoneKind) { + zoneEl.addEventListener("dragover", function (ev) { + ev.preventDefault(); + ev.dataTransfer.dropEffect = "move"; + }); + zoneEl.addEventListener("drop", function (ev) { + ev.preventDefault(); + var dragEl = document.querySelector(".dragging"); + if (!dragEl || zoneEl.contains(dragEl)) return; + zoneEl.appendChild(dragEl); + if (isCodeArea) { + dragEl.className = "code-block " + (dragEl.dataset.type || "sync"); + dragEl.textContent = dragEl.dataset.originalLabel || dragEl.textContent; + } else { + dragEl.className = "task " + (dragEl.dataset.type || "sync"); + if (zoneKind === "timer") { + dragEl.textContent = dragEl.dataset.originalLabel || dragEl.textContent; + } else if (isTimeoutType(dragEl.dataset.type || "")) { + dragEl.textContent = "fn"; + } + } + }); +} + +function setupDropZones() { + makeDropZone(codeBlocks, true, "code"); + makeDropZone(callstackTasks, false, "callstack"); + makeDropZone(microtaskTasks, false, "microtask"); + makeDropZone(asyncTasks, false, "async"); + makeDropZone(timerTasks, false, "timer"); +} + +function generateCodeBlocks() { + codeBlocks.innerHTML = ""; + INITIAL_BLOCKS.forEach(function (item, i) { + var block = createBlock(item.label, item.type, true); + block.id = "block-" + i; + block.addEventListener("dragend", onDragEnd); + codeBlocks.appendChild(block); + }); +} + +function reset() { + callstackTasks.innerHTML = ""; + microtaskTasks.innerHTML = ""; + timerTasks.innerHTML = ""; + asyncTasks.innerHTML = ""; + generateCodeBlocks(); +} + +setupDropZones(); +generateCodeBlocks(); + +btnReset.addEventListener("click", reset); diff --git a/courses/frontend/advanced-javascript/week3/session-materials/exercises.md b/courses/frontend/advanced-javascript/week3/session-materials/exercises.md new file mode 100644 index 00000000..056292b2 --- /dev/null +++ b/courses/frontend/advanced-javascript/week3/session-materials/exercises.md @@ -0,0 +1,54 @@ +# Exercises + +Work through these in order. + +## Exercise 1 + +Using async await + +1. `fetch` yes or no from this api: `https://yesno.wtf/api`. Show the answer on the page. + +## Exercise 2 + +Using promises + +1. `fetch` yes or no from this api: `https://yesno.wtf/api`. Show the answer on the page. +2. Try fetching a url that rejects e.g. `https://knajskdskj.jasdk`. Show the error message on the page. + +## Exercise 3 + +1. Create a promise that resolves after 4 seconds. Use this promise to show the text `hello` on the page after 4 seconds. +2. Now make the promise fail by rejecting it with an error message instead of resolving it, and show the error message on the page. + +## Exercise 4 + +Create a function that returns a promise, that you can use like this: + +```js +// YesNoFail4Seconds should wait 4 seconds before it does one of the following 3 things: +// resolves with a yes +// resolves with a no +// or rejects +// Look into Math.random() +YesNoFail4Seconds() + .then((data) => { + // Show on the page: The answer is ${data} + }) + .catch((error) => { + // Show on the page: the error + }); +``` + +The above example show how to consume the promise using promises. Now try consume the `YesNoFail4Seconds` using async/await + +## Exercise 5 + +Using async await + +1. Fetch a user from JSONPlaceholder (for example `https://jsonplaceholder.typicode.com/users/1`) +2. After that succeeds, fetch movies using [this api](https://gist.githubusercontent.com/pankaj28843/08f397fcea7c760a99206bcb0ae8d0a4/raw/02d8bc9ec9a73e463b13c44df77a87255def5ab9/movies.json) +3. Show the movies on the page + +## Exercise 6 + +Get the JSONPlaceholder user and the movies at the same time. Show the movies and the battery status on the page when the related promises have resolved. diff --git a/courses/frontend/advanced-javascript/week3/session-plan.md b/courses/frontend/advanced-javascript/week3/session-plan.md index 8af8c832..02400cb2 100644 --- a/courses/frontend/advanced-javascript/week3/session-plan.md +++ b/courses/frontend/advanced-javascript/week3/session-plan.md @@ -7,6 +7,8 @@ These are some examples of previously created materials by mentors that you can use yourself, or for inspiration. - [Notion Page Handout](https://dandy-birth-1b2.notion.site/HYF-Aarhus-JS-3-Week-2-0287dd1293df4a0a92171e62ce12f5c8?pvs=4) (by [Thomas](https://github.com/te-online)) +- [Demo](./session-materials/demo/) – In-session live coding from **Code inspiration** below (not the trainee exercises). **index.js** = worksheet stubs; **index-solution.js** = reference. [README](./session-materials/demo/README.md). +- [Event loop demo](./session-materials/event-loop-demo/event-loop.html) – Drag-and-drop diagram; week 2 version plus **microtask queue** and promise blocks. ## Session Outline @@ -20,78 +22,29 @@ First when they fully understand one part of promises, I move on! Don't over-com - Quickly recap asynchronicity - Ask the trainees what it means that some code is asynchronous - Practical example of async/await - - [Exercises 1](#exercise-1) + - [Exercises 1](./session-materials/exercises.md#exercise-1) - Promise - Why do we use promises? - So important to explain this, the trainees always ask this! [Is there specific functionality that can only be done with promises in JS?](https://stackoverflow.com/questions/39004567/why-do-we-need-promise-in-js) - Consumption - [Code inspiration](#promise-consumption) - Example, call some function that returns a promise (like fetch) - - [Exercises 2](#exercise-2) + - [Exercises 2](./session-materials/exercises.md#exercise-2) - Creation - [Code inspiration](#promise-creation) - - [Exercises 3](#exercise-3) and then [Exercises 4](#exercise-4) + - [Exercises 3](./session-materials/exercises.md#exercise-3) and then [Exercises 4](./session-materials/exercises.md#exercise-4) - Async await - - [Exercises 5](#exercise-5) + - [Exercises 5](./session-materials/exercises.md#exercise-5) - `Promise.all` - Let trainees investigate - Optional - Chaining. Calling `.then` returns a promise. Only get to here when they understand async/await and promise consumption and creation. - [Reason for promise](https://mobile.twitter.com/addyosmani/status/1097035418657144832?s=19) - - [Exercises 5](#exercise-5) and [Exercises 6](#exercise-6) + - [Exercises 5](./session-materials/exercises.md#exercise-5) and [Exercises 6](./session-materials/exercises.md#exercise-6) ## Exercises - +See [Exercises](./session-materials/exercises.md). Trainees show results on the page (update the DOM), not in the console. -### Exercise 1 - -Using async await - -1. `fetch` yes or no from this api: `https://yesno.wtf/api`. log out the answer - -### Exercise 2 - -Using promises - -1. `fetch` yes or no from this api: `https://yesno.wtf/api`. log out the answer -2. Try fetching a url that rejects e.g. `https://knajskdskj.jasdk`. Log out the error message - -### Exercise 3 - -1. Create a promise that resolves after 4 seconds. Use this promise to log out the text 'hello' after 4 seconds. -2. Now make the promise fail by rejecting it with an error message instead of resolving it, and log the error message to the console. - -### Exercise 4 - -Create a function that returns a promise, that you can use like this: - -```js -// YesNoFail4Seconds should wait 4 seconds before it does one of the following 3 things: -// resolves with a yes -// resolves with a no -// or rejects -// Look into Math.random() -YesNoFail4Seconds() - .then((data) => { - console.log(`The answer is ${data}`); - }) - .catch((error) => { - console.log(error); - }); -``` - -The above example show how to consume the promise using promises. Now try consume the `YesNoFail4Seconds` using async/await - -### Exercise 5 - -Using async await - -1. Fetch the astronauts -2. After the astronauts has been fetched, fetch movies using [this api](https://gist.githubusercontent.com/pankaj28843/08f397fcea7c760a99206bcb0ae8d0a4/raw/02d8bc9ec9a73e463b13c44df77a87255def5ab9/movies.json) -3. Log out the movies - -### Exercise 6 - -Get the astronauts and the movies at the same time. Log out the movies and the battery status when both promises has been resolved. +[Console order](./session-materials/console-order.md) – Optional “what is logged?” promise chaining. ## Code inspiration @@ -108,22 +61,21 @@ Get the astronauts and the movies at the same time. Log out the movies and the b // await waits until we have fetched the data from the api. Or said in another way await waits until fetch has resolved with the data from the api // write async before a function for await to work. What does it mean that something is asynchronous? -async function getAstronauts() { +// JSONPlaceholder works reliably in the browser (same idea as Open Notify / astronauts, different URL). +async function getJsonPlaceholderUser() { // await waits until we have data from fetch before it runs the next line. No need for callbacks 🤯 console.log("Before we fetch data"); - const astronautsResponse = await fetch( - "http://api.open-notify.org/astros.json", - ); + const response = await fetch("https://jsonplaceholder.typicode.com/users/1"); console.log( "This is logged out after some time, even though the code looks synchronous! 🤯", ); - const astronauts = await astronautsResponse.json(); + const user = await response.json(); console.log("This is logged out after some time! 🤯"); - console.log(astronauts); - return astronauts; + console.log(user); + return user; } -getAstronauts(); +getJsonPlaceholderUser(); ``` ### Promise consumption @@ -138,10 +90,10 @@ The trainees should be able to answer these questions: // How would you explain your mom what resolved and rejected means? ```js -fetch("http://api.open-notify.org/astros.json") - .then((astronautsResponse) => astronautsResponse.json()) - .then((astronauts) => { - console.log(astronauts); +fetch("https://jsonplaceholder.typicode.com/users/1") + .then((response) => response.json()) + .then((user) => { + console.log(user); }) .catch((error) => console.log(error)); @@ -215,19 +167,19 @@ console.log(test()); So writing `async` in front of a function makes it return a promise! The keyword `await` makes JavaScript wait until that promise resolved and returns its result. ```js -async function getAstronauts() { +async function getJsonPlaceholderUserSafe() { try { - const astronautsResponse = await fetch( - "http://api.open-notify.org/astros.json", + const response = await fetch( + "https://jsonplaceholder.typicode.com/users/1", ); - const astronauts = await astronautsResponse.json(); - return astronauts; + const user = await response.json(); + return user; } catch (err) { - throw "Fetching the astronauts went wrong"; + throw "Fetching the user went wrong"; } } -const astronauts = getAstronauts(); +const userPromise = getJsonPlaceholderUserSafe(); ``` ### Function that returns a promise From 6b2ef0af64715dcfab87bb08ed35fc685dee9e11 Mon Sep 17 00:00:00 2001 From: markitosha Date: Sat, 28 Mar 2026 14:17:26 +0100 Subject: [PATCH 2/3] updated matherials for week 4 --- SUMMARY.md | 1 + .../frontend/advanced-javascript/README.md | 2 +- .../week3/session-materials/Promises.pdf | Bin 0 -> 42522 bytes .../event-loop-demo/event-loop.css | 330 ------------------ .../event-loop-demo/event-loop.html | 97 ----- .../event-loop-demo/event-loop.js | 104 ------ .../advanced-javascript/week3/session-plan.md | 3 +- .../advanced-javascript/week4/README.md | 36 +- .../advanced-javascript/week4/assignment.md | 31 +- .../session-materials/code-inspiration.md | 184 ++++++++++ .../week4/session-materials/demo/README.md | 63 ++++ .../session-materials/demo/index-solution.js | 207 +++++++++++ .../week4/session-materials/demo/index.html | 24 ++ .../week4/session-materials/demo/index.js | 119 +++++++ .../week4/session-materials/demo/style.css | 130 +++++++ .../week4/session-materials/exercises.md | 89 +++++ .../advanced-javascript/week4/session-plan.md | 222 +----------- 17 files changed, 877 insertions(+), 765 deletions(-) create mode 100644 courses/frontend/advanced-javascript/week3/session-materials/Promises.pdf delete mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css delete mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html delete mode 100644 courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/demo/README.md create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/demo/index-solution.js create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/demo/index.html create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/demo/index.js create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/demo/style.css create mode 100644 courses/frontend/advanced-javascript/week4/session-materials/exercises.md diff --git a/SUMMARY.md b/SUMMARY.md index 0ac32e65..fe12b483 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -104,6 +104,7 @@ - [Week 4](courses/frontend/advanced-javascript/week4/README.md) - [Preparation](courses/frontend/advanced-javascript/week4/preparation.md) - [Session Plan](courses/frontend/advanced-javascript/week4/session-plan.md) + - [Exercises](courses/frontend/advanced-javascript/week4/session-materials/exercises.md) - [Assignment](courses/frontend/advanced-javascript/week4/assignment.md) - [React](courses/frontend/react/README.md) - [Week 1](courses/frontend/react/week1/README.md) diff --git a/courses/frontend/advanced-javascript/README.md b/courses/frontend/advanced-javascript/README.md index 95241b80..95883666 100644 --- a/courses/frontend/advanced-javascript/README.md +++ b/courses/frontend/advanced-javascript/README.md @@ -11,7 +11,7 @@ In this module, you will advance your JavaScript expertise to build interactive | 1. | [Array functions & Arrow functions](./week1/README.md) | [Preparation](./week1/preparation.md) | [Session Plan](./week1/session-plan.md) (for mentors) | [Assignment](./week1/assignment.md) | | 2. | [Callback functions & Asynchronous code](./week2/README.md) | [Preparation](./week2/preparation.md) | [Session Plan](./week2/session-plan.md) (for mentors) | [Assignment](./week2/assignment.md) | | 3. | [Promises & `async`/`await`](./week3/README.md) | [Preparation](./week3/preparation.md) | [Session Plan](./week3/session-plan.md) (for mentors) | [Assignment](./week3/assignment.md) | -| 4. | [Classes & Advanced Promises](./week4/README.md) | [Preparation](./week4/preparation.md) | [Session Plan](./week4/session-plan.md) (for mentors) | [Assignment](./week4/assignment.md) | +| 4. | [Classes & Object-Oriented Programming](./week4/README.md) | [Preparation](./week4/preparation.md) | [Session Plan](./week4/session-plan.md) (for mentors) | [Assignment](./week4/assignment.md) | ## Module Learning Goals diff --git a/courses/frontend/advanced-javascript/week3/session-materials/Promises.pdf b/courses/frontend/advanced-javascript/week3/session-materials/Promises.pdf new file mode 100644 index 0000000000000000000000000000000000000000..852575db57484ffc1c018e9542cba235f70ef9e2 GIT binary patch literal 42522 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 literal 0 HcmV?d00001 diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css deleted file mode 100644 index 65914c0f..00000000 --- a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.css +++ /dev/null @@ -1,330 +0,0 @@ -/* Event loop + microtasks visualization – light theme */ - -*, -*::before, -*::after { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: - system-ui, - -apple-system, - sans-serif; - line-height: 1.5; - background: #f5f5f5; - color: #222; -} - -main { - max-width: 72rem; - margin: 0 auto; - padding: 1.5rem; -} - -h1 { - margin: 0 0 0.5rem; - font-size: 1.5rem; - color: #1967d2; -} - -.intro { - margin: 0 0 1.25rem; - font-size: 0.9rem; - color: #5f6368; - max-width: 52rem; -} - -.intro code { - font-size: 0.85em; - padding: 0.1em 0.35em; - background: #e8eaed; - border-radius: 4px; -} - -/* Code section – blocks at the start */ -.code-section { - margin-bottom: 1.5rem; -} - -.section-label { - font-weight: 600; - font-size: 0.875rem; - margin-bottom: 0.5rem; - color: #5f6368; -} - -/* Tooltip: hover on ? to show definition */ -.tooltip-trigger { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.1em; - height: 1.1em; - margin-left: 0.25em; - font-size: 0.85em; - font-weight: 700; - line-height: 1; - color: #5f6368; - background: #e8eaed; - border-radius: 50%; - cursor: help; - position: relative; - vertical-align: middle; -} - -.tooltip-trigger:hover, -.tooltip-trigger:focus { - color: #1967d2; - background: #e8f0fe; - outline: none; -} - -.tooltip { - position: absolute; - left: 50%; - bottom: calc(100% + 0.5em); - transform: translateX(-50%); - width: 18em; - max-width: 90vw; - padding: 0.6em 0.75em; - font-size: 0.8rem; - font-weight: 400; - line-height: 1.4; - color: #222; - background: #fff; - border: 1px solid #dadce0; - border-radius: 6px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); - white-space: normal; - visibility: hidden; - opacity: 0; - transition: - opacity 0.15s ease, - visibility 0.15s ease; - z-index: 20; - pointer-events: none; -} - -.tooltip code { - font-size: 0.9em; -} - -.tooltip-trigger:hover .tooltip, -.tooltip-trigger:focus .tooltip { - visibility: visible; - opacity: 1; -} - -.tooltip::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -6px; - border: 6px solid transparent; - border-top-color: #dadce0; -} - -.code-blocks { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - min-height: 3rem; - padding: 0.75rem; - background: #fff; - border: 1px solid #dadce0; - border-radius: 8px; -} - -.code-block, -.task { - cursor: grab; -} - -.code-block:active, -.task:active { - cursor: grabbing; -} - -.code-block.dragging, -.task.dragging { - opacity: 0.6; -} - -.code-block { - padding: 0.4rem 0.75rem; - border-radius: 4px; - font-size: 0.8rem; - font-family: ui-monospace, monospace; - font-weight: 500; - transition: opacity 0.3s ease; -} - -.code-block.sync { - background: #e8f0fe; - color: #1967d2; - border: 1px solid #aecbfa; -} - -.code-block.timeout, -.code-block.timeout-1 { - background: #e6f4ea; - color: #137333; - border: 1px solid #81c995; -} - -.code-block.timeout-2 { - background: #fef7e0; - color: #b06000; - border: 1px solid #f9d57e; -} - -.code-block.timeout-0 { - background: #e8e0ec; - color: #7c4dff; - border: 1px solid #b39ddb; -} - -.code-block.event { - background: #f1f3f4; - color: #5f6368; - border: 1px solid #dadce0; -} - -/* Promise → microtask queue (same colour in every zone) */ -.code-block.promise-then, -.code-block.promise-catch { - background: #fce8e6; - color: #c5221f; - border: 1px solid #f5aea8; -} - -.diagram { - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: stretch; - min-height: 10rem; - margin-bottom: 1.5rem; -} - -.zone { - flex: 1 1 14rem; - border: 2px solid #dadce0; - border-radius: 8px; - padding: 0.75rem; - display: flex; - flex-direction: column; - background: #fff; -} - -.zone-label { - font-weight: 600; - font-size: 0.875rem; - margin-bottom: 0.5rem; - color: #5f6368; -} - -.call-stack .zone-label { - color: #1967d2; -} - -.microtask-queue .zone-label { - color: #c5221f; -} - -.timer .zone-label { - color: #b06000; -} - -.async-queue .zone-label { - color: #137333; -} - -.zone-tasks { - flex: 1; - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-content: flex-start; - align-items: center; - min-height: 3.5rem; -} - -.task { - padding: 0.35rem 0.6rem; - border-radius: 4px; - font-size: 0.8rem; - font-family: ui-monospace, monospace; - font-weight: 500; - transition: opacity 0.3s ease; -} - -.task.sync { - background: #e8f0fe; - color: #1967d2; - border: 1px solid #aecbfa; -} - -.task.timeout-1 { - background: #e6f4ea; - color: #137333; - border: 1px solid #81c995; -} - -.task.timeout-2 { - background: #fef7e0; - color: #b06000; - border: 1px solid #f9d57e; -} - -.task.timeout-0 { - background: #e8e0ec; - color: #7c4dff; - border: 1px solid #b39ddb; -} - -.task.event { - background: #f1f3f4; - color: #5f6368; - border: 1px solid #dadce0; -} - -.task.promise-then, -.task.promise-catch { - background: #fce8e6; - color: #c5221f; - border: 1px solid #f5aea8; -} - -.controls { - display: flex; - gap: 0.5rem; -} - -.controls button { - padding: 0.5rem 1rem; - font-size: 0.875rem; - border: none; - border-radius: 6px; - cursor: pointer; - background: #1967d2; - color: #fff; - font-weight: 500; -} - -.controls button:hover { - background: #1557b0; -} - -.controls button#btn-reset { - background: #fff; - color: #5f6368; - border: 1px solid #dadce0; -} - -.controls button#btn-reset:hover { - background: #f1f3f4; -} diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html deleted file mode 100644 index 75f112e1..00000000 --- a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - Event loop & microtasks – Week 3 - - - -
-

Event loop & microtasks

-

- Drag blocks where they belong. Same idea as week 2, with one extra box: - work from a Promise (like .then / - .catch) waits in the microtask queue and - runs before the next setTimeout - callback. -

- -
- -
-
- -
-
-
- Call stack - - ? - Where normal synchronous code runs. When it finishes, - microtasks run, then one callback from the task queue. - -
-
-
-
-
- Microtask queue - - ? - When a promise settles, the engine queues its - .then / .catch work here. This queue - is emptied before the next setTimeout (or other - task-queue) callback. - -
-
-
-
-
- Task queue - - ? - Callbacks waiting to run after microtasks — for example from - setTimeout. - -
-
-
-
-
- Timer (Web APIs) - - ? - When you call setTimeout(fn, ms), the timer waits, - then the callback is queued as a macrotask. Part of the - browser’s Web APIs. - -
-
-
-
- -
- -
-
- - - diff --git a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js b/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js deleted file mode 100644 index 4505037c..00000000 --- a/courses/frontend/advanced-javascript/week3/session-materials/event-loop-demo/event-loop.js +++ /dev/null @@ -1,104 +0,0 @@ -// Event loop: draggable blocks → Call stack, Microtask queue, Task queue, Timer -// setTimeout: full label in Timer (registration), "fn" elsewhere (the callback) — like week 2. -// Sync and Promise blocks always keep their full label. - -var codeBlocks = document.getElementById("code-blocks"); -var callstackTasks = document.getElementById("callstack-tasks"); -var microtaskTasks = document.getElementById("microtask-tasks"); -var timerTasks = document.getElementById("timer-tasks"); -var asyncTasks = document.getElementById("async-tasks"); -var btnReset = document.getElementById("btn-reset"); - -// Same shape as week 2, plus two promise blocks for the microtask queue. -var INITIAL_BLOCKS = [ - { label: "sync 1", type: "sync" }, - { label: "sync 2", type: "sync" }, - { label: "setTimeout(fn, 2s)", type: "timeout-1" }, - { label: "sync 3", type: "sync" }, - { label: "setTimeout(fn, 0)", type: "timeout-0" }, - { label: "sync 4", type: "sync" }, - { label: "Promise .then(fn)", type: "promise-then" }, - { label: "setTimeout(fn, 4s)", type: "timeout-2" }, - { label: "sync 5", type: "sync" }, - { label: "Promise .catch(fn)", type: "promise-catch" }, -]; - -function createBlock(label, type, isInCodeArea) { - var el = document.createElement("div"); - el.draggable = true; - el.dataset.type = type; - el.dataset.originalLabel = label; - el.textContent = label; - el.className = isInCodeArea ? "code-block " + type : "task " + type; - el.addEventListener("dragstart", onDragStart); - return el; -} - -function isTimeoutType(type) { - return type === "timeout-0" || type === "timeout-1" || type === "timeout-2"; -} - -function onDragStart(ev) { - ev.dataTransfer.effectAllowed = "move"; - ev.dataTransfer.setData("text/plain", ""); - ev.target.classList.add("dragging"); -} - -function onDragEnd(ev) { - ev.target.classList.remove("dragging"); -} - -function makeDropZone(zoneEl, isCodeArea, zoneKind) { - zoneEl.addEventListener("dragover", function (ev) { - ev.preventDefault(); - ev.dataTransfer.dropEffect = "move"; - }); - zoneEl.addEventListener("drop", function (ev) { - ev.preventDefault(); - var dragEl = document.querySelector(".dragging"); - if (!dragEl || zoneEl.contains(dragEl)) return; - zoneEl.appendChild(dragEl); - if (isCodeArea) { - dragEl.className = "code-block " + (dragEl.dataset.type || "sync"); - dragEl.textContent = dragEl.dataset.originalLabel || dragEl.textContent; - } else { - dragEl.className = "task " + (dragEl.dataset.type || "sync"); - if (zoneKind === "timer") { - dragEl.textContent = dragEl.dataset.originalLabel || dragEl.textContent; - } else if (isTimeoutType(dragEl.dataset.type || "")) { - dragEl.textContent = "fn"; - } - } - }); -} - -function setupDropZones() { - makeDropZone(codeBlocks, true, "code"); - makeDropZone(callstackTasks, false, "callstack"); - makeDropZone(microtaskTasks, false, "microtask"); - makeDropZone(asyncTasks, false, "async"); - makeDropZone(timerTasks, false, "timer"); -} - -function generateCodeBlocks() { - codeBlocks.innerHTML = ""; - INITIAL_BLOCKS.forEach(function (item, i) { - var block = createBlock(item.label, item.type, true); - block.id = "block-" + i; - block.addEventListener("dragend", onDragEnd); - codeBlocks.appendChild(block); - }); -} - -function reset() { - callstackTasks.innerHTML = ""; - microtaskTasks.innerHTML = ""; - timerTasks.innerHTML = ""; - asyncTasks.innerHTML = ""; - generateCodeBlocks(); -} - -setupDropZones(); -generateCodeBlocks(); - -btnReset.addEventListener("click", reset); diff --git a/courses/frontend/advanced-javascript/week3/session-plan.md b/courses/frontend/advanced-javascript/week3/session-plan.md index 02400cb2..50b95edf 100644 --- a/courses/frontend/advanced-javascript/week3/session-plan.md +++ b/courses/frontend/advanced-javascript/week3/session-plan.md @@ -8,7 +8,7 @@ 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-3-Week-2-0287dd1293df4a0a92171e62ce12f5c8?pvs=4) (by [Thomas](https://github.com/te-online)) - [Demo](./session-materials/demo/) – In-session live coding from **Code inspiration** below (not the trainee exercises). **index.js** = worksheet stubs; **index-solution.js** = reference. [README](./session-materials/demo/README.md). -- [Event loop demo](./session-materials/event-loop-demo/event-loop.html) – Drag-and-drop diagram; week 2 version plus **microtask queue** and promise blocks. +- [Promises chaining diagram (PDF)](./session-materials/Promises.pdf) – Hand-drawn sketch you can project or redraw on the board when explaining how `.then()` chains. ## Session Outline @@ -37,6 +37,7 @@ First when they fully understand one part of promises, I move on! Don't over-com - [Exercises 5](./session-materials/exercises.md#exercise-5) - `Promise.all` - Let trainees investigate - Optional - Chaining. Calling `.then` returns a promise. Only get to here when they understand async/await and promise consumption and creation. + - I found that drawing/demoing how it works under the hood useful when explaining how promises feed into the next `.then()`. You can find example in Session matherials. - [Reason for promise](https://mobile.twitter.com/addyosmani/status/1097035418657144832?s=19) - [Exercises 5](./session-materials/exercises.md#exercise-5) and [Exercises 6](./session-materials/exercises.md#exercise-6) diff --git a/courses/frontend/advanced-javascript/week4/README.md b/courses/frontend/advanced-javascript/week4/README.md index 67b810bd..75d2dd93 100644 --- a/courses/frontend/advanced-javascript/week4/README.md +++ b/courses/frontend/advanced-javascript/week4/README.md @@ -1,4 +1,4 @@ -# Classes & Advanced Promises (Week 4) +# Classes & Object-Oriented Programming (Week 4) In this session, you'll learn how to use JavaScript classes to create reusable templates for objects that share common properties and behaviors. By mastering classes and inheritance, you'll be able to organize your code more efficiently and implement object-oriented programming principles. These skills will help you write cleaner, more maintainable code and understand the differences between classes and objects in JavaScript. @@ -6,13 +6,14 @@ In this session, you'll learn how to use JavaScript classes to create reusable t - [Preparation](./preparation.md) - [Session Plan](./session-plan.md) (for mentors) +- [Exercises](./session-materials/exercises.md) - [Assignment](./assignment.md) ## Session Learning Goals By the end of this session, you will be able to: -- [ ] Use **classes** to easily create similar objects. +- [ ] Use **classes** to create objects with consistent structure and built-in behavior - [ ] Declare a class using `class`, `constructor`, and `this` - [ ] Instantiate objects from classes using `new` - [ ] Use Methods and constructors @@ -21,19 +22,30 @@ By the end of this session, you will be able to: - [ ] Understand the difference between classes vs objects ```js -// Example of declaring a Person class -class Person { - constructor(name, age) { - this.name = name; - this.age = age; +class Comment { + constructor(username, text) { + this.username = username; + this.text = text; + this.likes = 0; } - greet() { - console.log(`Hi, I'm ${this.name}`); + like() { + this.likes++; + this.render(); + } + + render() { + // data, behavior, and rendering live together + const div = document.createElement("div"); + div.innerHTML = ` + @${this.username} +

${this.text}

+ + `; + return div; } } -// Example of an actual person created from the Person class -const alice = new Person("Alice", 25); -alice.greet(); +const comment = new Comment("alice", "Great post!"); +document.body.appendChild(comment.render()); ``` diff --git a/courses/frontend/advanced-javascript/week4/assignment.md b/courses/frontend/advanced-javascript/week4/assignment.md index 34b00ad3..1374fbd2 100644 --- a/courses/frontend/advanced-javascript/week4/assignment.md +++ b/courses/frontend/advanced-javascript/week4/assignment.md @@ -1,27 +1,34 @@ # Assignment -For this week's assignment we will create a web applications that generates a screenshot of a website based on a url. We will combine two API's one to generate the screenshot and one to allow the user to save the screenshot. +For this week's assignment we will create a web application that generates a screenshot of a website based on a URL. We will combine two APIs: one to generate the screenshot and one to allow the user to save the screenshot. We use [Rapid API](https://rapidapi.com/apishub/api/website-screenshot6/?utm_source=RapidAPI.com%2Fguides&utm_medium=DevRel&utm_campaign=DevRel) to generate a screenshot and the [crudcrud API](https://crudcrud.com/) to save the screenshot. -Technical specifications. +## Technical specifications 1. User can enter a URL for a website and it will send back a screenshot of the website using the website-screenshot API 2. User can hit a button to save the screenshot. It will then save the screenshot and the URL as a resource on crudcrud 3. User can get a list of all screenshots that they have saved 4. User can delete a screenshot that they have saved -## Optional Tasks/Assignments +### Class requirements + +1. **Model your UI as classes** with a `render()` method that puts content on the page. +2. **Build an error system** with custom error classes that `extend Error`, each with a way to show a user-friendly message (e.g. `toUserMessage()`). +3. **Handle errors** with `try/catch` and use `instanceof` to treat different error types differently. + +Look at your interface and think about what parts can be modeled as classes — if something has data and behavior that go together, or if it can be reused, make it a class. For example, you could create a `Screenshot` class that holds the URL and image data, knows how to render itself as a card on the page, and has a method for deleting itself from crudcrud. -1. Create another resource called users which takes in an email and password. Create one user. -2. Get back a list of users -3. First show a login form -4. If the email and password matches the one user we created we show the applications else we show an error message. +For the error system, think about what kinds of errors can happen in your app — what if the user submits an empty URL? What if the API returns a bad response? What if the network is down? You might end up with classes like `ValidationError`, `ApiError`, or something else entirely — it's up to you. + +## Optional Tasks/Assignments -Extra +> **Note:** Users do not need to be stored in a database or API — just keep them in memory (e.g. an array of instances in your JavaScript). No need to persist them anywhere. -1. Create another user -2. When saving a screenshot also save the user email(or another unique identifier) -3. Make sure we are only showing screenshots that the user that is logged in has uploaded +1. Create a user object with an email and password. Keep it in a variable or array. +2. Show a login form first. +3. If the email and password match the user you created, show the application. Otherwise show an error message. +4. Create another user. When saving a screenshot, also save the user email (or another unique identifier). +5. Make sure you only show screenshots that the logged-in user has uploaded. -Keep in mind the API key for the website-screenshot and the uuid for crudcrud should be in a secret.js file which is not committed to git +Keep in mind the API key for the website-screenshot and the uuid for crudcrud should be in a secret.js file which is not committed to git. diff --git a/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md b/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md new file mode 100644 index 00000000..c19cdebd --- /dev/null +++ b/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md @@ -0,0 +1,184 @@ +# Code inspiration + +Snippets aligned with the in-session [demo](./demo/README.md): same `Comment` shape (`username`, `text`), plain-object motivation, `render()` / `like()` / `hasSwearWord()`, then `Error` / `ValidationError` and a Web Components sketch. Use these on the board or as copy-paste shortcuts; the demo files are the full runnable version. + +## Motivation (plain objects) + +Why classes: repeated object literals are easy to get wrong; rendering and behavior live outside the data. + +```js +const comment1 = { username: "alice", text: "Nice!", date: new Date(), likes: 0 }; +const comment2 = { userName: "bob", text: "Hi" }; // typo: userName → UI shows undefined +const comment3 = { username: "carol", content: "Oops" }; // wrong key for text + +function renderComment(comment) { + const div = document.createElement("div"); + div.className = "comment"; + div.innerHTML = ` +

@${comment.username}

+

${comment.text}

+ + `; + return div; +} + +function likeComment(comment) { + comment.likes++; // data changes, but the button on screen does not update +} +``` + +## Constructor + +```js +class Comment { + constructor(username, text) { + this.username = username; + this.text = text; + this.date = new Date(); + this.likes = 0; + this.element = null; + } +} +``` + +## Instance + +```js +const c1 = new Comment("dave_dev", "Hello!"); +const c2 = new Comment("eve_codes", "Second comment"); + +console.log(c1); +``` + +## Methods + +`render()` keeps DOM in sync with state; `like()` updates and re-renders; `hasSwearWord()` drives a CSS class (e.g. `comment--flagged`) for moderation-style UI. + +```js +class Comment { + constructor(username, text) { + this.username = username; + this.text = text; + this.date = new Date(); + this.likes = 0; + this.element = null; + } + + like() { + this.likes++; + this.render(); + } + + hasSwearWord() { + const swearWords = ["crap", "damn", "stupid"]; + const words = this.text.toLowerCase().split(" "); + return swearWords.some((swear) => words.includes(swear)); + } + + render() { + if (!this.element) { + this.element = document.createElement("div"); + } + + this.element.className = this.hasSwearWord() + ? "comment comment--flagged" + : "comment"; + + this.element.innerHTML = ` +
+ @${this.username} + ${this.date.toLocaleDateString()} +
+

${this.text}

+ + `; + + this.element + .querySelector(".like-btn") + .addEventListener("click", () => this.like()); + + return this.element; + } +} + +document.getElementById("comments-class").appendChild(c1.render()); +``` + +## Static methods + +A **static method** belongs to the class itself, not to instances. Useful for factory functions (creating instances from raw data) or utility operations. + +```js +class Comment { + constructor(username, text) { + this.username = username; + this.text = text; + this.date = new Date(); + this.likes = 0; + } + + static fromJSON(data) { + return new Comment(data.username, data.text); + } + + // ... render(), like(), etc. +} + +// Called on the class, not on an instance: +const apiData = { username: "grace_api", text: "Loaded from JSON!" }; +const comment = Comment.fromJSON(apiData); +``` + +**You already use static methods — `Promise` is a class!** + +```js +// Instance methods (called on an instance): +const p = new Promise((resolve) => resolve("done")); +p.then((value) => console.log(value)); +p.catch((err) => console.log(err)); + +// Static methods (called on the class itself): +Promise.resolve("instant value"); +Promise.all([fetch("/a"), fetch("/b")]); +Promise.race([fetch("/a"), fetch("/b")]); +``` + +## (Optional) Extending built-ins: Error and Web Components + +`Error` is a built-in class; custom errors use `extends` and `super()` like any other subclass. Web Components apply the same “class + lifecycle + HTML” idea to the platform. + +```js +const err = new Error("something went wrong"); +console.log(err.message); +console.log(err.stack); + +class ValidationError extends Error { + constructor(field, message) { + super(message); + this.name = "ValidationError"; + this.field = field; + } + + toUserMessage() { + return `❌ ${this.field}: ${this.message}`; + } +} + +try { + throw new ValidationError("username", "Cannot be empty"); +} catch (error) { + console.log(error.toUserMessage()); + console.log(error instanceof ValidationError); + console.log(error instanceof Error); +} + +// Web Components — same pattern, browser APIs (not required to run in the demo): +// +// class CommentElement extends HTMLElement { +// connectedCallback() { +// this.innerHTML = `
...
`; +// } +// } +// customElements.define("my-comment", CommentElement); +// +``` diff --git a/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md b/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md new file mode 100644 index 00000000..1c364275 --- /dev/null +++ b/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md @@ -0,0 +1,63 @@ +# Mentors demo – Classes + +In-session live coding for **Week 4** (Advanced JavaScript). The page shows a **comment section** twice: first with plain objects (motivation: inconsistent keys, disconnected rendering), then with a **`Comment` class** (constructor, instances, `render()`, `like()`, `hasSwearWord()` and flagged styling). Part 4 walks through **built-in `Error`**, a **`ValidationError extends Error`** example, and a short **Web Components** comparison (comments only — not implemented in the browser). Implement the worksheet during class; the solution file is the finished version. + +--- + +## Files in this folder + +| File | Purpose | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **index.js** | Worksheet: Part 1 runs on load; Parts 2–4 use `// Task:` lines, `// Next: Exercise N` pauses, and `// ==========` section banners. Use this file when leading the session. | +| **index-solution.js** | Full implementation: `Comment` with `render` / `like` / `hasSwearWord`, DOM append, `ValidationError` + `try` / `catch`, and the Web Components sketch as comments. | +| **index.html** | Two sections and containers for plain vs class comments; loads **index.js**. To preview the solution, change the ` + + diff --git a/courses/frontend/advanced-javascript/week4/session-materials/demo/index.js b/courses/frontend/advanced-javascript/week4/session-materials/demo/index.js new file mode 100644 index 00000000..7318b206 --- /dev/null +++ b/courses/frontend/advanced-javascript/week4/session-materials/demo/index.js @@ -0,0 +1,119 @@ +// ============================================================================= +// PART 1 — MOTIVATION: Comment section with plain objects +// ============================================================================= + +const comment1 = { + username: "alice_dev", + text: "Great post! Really enjoyed reading this.", + date: new Date("2025-03-15"), + likes: 3, +}; +const comment2 = { + userName: "bob_codes", + text: "Thanks for sharing, very helpful!", + date: new Date("2025-03-16"), + likes: 1, +}; +const comment3 = { + username: "carol_js", + content: "I have a question about the third paragraph.", + date: new Date("2025-03-17"), +}; + +const plainComments = [comment1, comment2, comment3]; + +function renderComment(comment) { + const div = document.createElement("div"); + div.className = "comment"; + div.innerHTML = ` +
+ @${comment.username} + ${comment.date.toLocaleDateString()} +
+

${comment.text}

+ + `; + return div; +} + +function likeComment(comment) { + comment.likes++; +} + +const plainContainer = document.getElementById("comments-plain"); + +plainComments.forEach(function (comment) { + plainContainer.appendChild(renderComment(comment)); +}); + +// ============================================================================= +// PART 2 — CONSTRUCTOR & INSTANCE +// ============================================================================= +// Task: Create a Comment class with constructor(username, text) +// It should also initialize: date = new Date(), likes = 0, element = null + +// Task: Create at least 3 instances using `new` and log them + +// Next: Exercise 1 +// Next: Exercise 2 + +// ============================================================================= +// PART 3 — METHODS +// ============================================================================= +// Task: Add a render() method that creates and returns a DOM element for the comment + +// Task: Add a like() method that increments likes and re-renders + +// Task: Add a hasSwearWord() method that checks the text against a list of banned words +// If true, render() should add a "comment--flagged" CSS class to highlight the comment + +// Task: Add a static method Comment.fromJSON(data) that takes a plain object +// (e.g. from an API response) and returns a new Comment instance + +// --- You already use static methods! --- +// Promise is a class. You've been using its static methods since Week 3: +// new Promise(...) — constructor (creates an instance) +// promise.then() — instance method (called on an instance) +// promise.catch() — instance method +// Promise.all([...]) — static method (called on the class itself) +// Promise.resolve("hello") — static method +// Promise.race([...]) — static method + +// Task: Display the class-based comments on the page (make sure at least one has a swear word!) +// const classContainer = document.getElementById("comments-class"); + +// Next: Exercise 3 + +// ============================================================================= +// PART 4 — CLASSES IN THE REAL WORLD +// ============================================================================= + +// --- Errors are classes! --- +// Error is a built-in class. You already use it: +// const err = new Error("something went wrong"); +// console.log(err.message); // constructor set this +// console.log(err.stack); // built-in method/property + +// Task: Create a ValidationError class that extends Error +// - constructor accepts (field, message), calls super(message), sets this.name and this.field +// - add a toUserMessage() method that returns a user-friendly string + +// Task: Try throwing and catching it — use instanceof to check the error type + +// --- Web Components use this exact pattern --- +// What we built (Comment class with render()) is very close to how Web Components work: +// +// class CommentElement extends HTMLElement { +// connectedCallback() { +// // called when the element appears on the page — similar to our render() +// this.innerHTML = `
...
`; +// } +// } +// customElements.define("my-comment", CommentElement); +// +// // Then in HTML: +// +// Same ideas: a class, extends a built-in, has lifecycle methods, renders HTML. +// Frameworks like React, Lit, and Angular all build on this mental model. + +// Next: Exercise 4 \ No newline at end of file diff --git a/courses/frontend/advanced-javascript/week4/session-materials/demo/style.css b/courses/frontend/advanced-javascript/week4/session-materials/demo/style.css new file mode 100644 index 00000000..e572c553 --- /dev/null +++ b/courses/frontend/advanced-javascript/week4/session-materials/demo/style.css @@ -0,0 +1,130 @@ +/* ============================================================================= + Mentors demo – Classes + ============================================================================= */ + +* { + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", system-ui, sans-serif; + margin: 0; + padding: 1.5rem 2rem; + background: #f8fafc; + color: #1e293b; +} + +h1 { + margin-top: 0; + font-size: 1.5rem; +} + +.intro { + color: #64748b; + margin-bottom: 2rem; + font-size: 0.95rem; +} + +/* ============================================================================= + Sections + ============================================================================= */ + +section { + margin-bottom: 2.5rem; +} + +section h2 { + font-size: 1.15rem; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #e2e8f0; +} + +/* ============================================================================= + Comment cards + ============================================================================= */ + +.comments { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.comment { + background: #fff; + border-radius: 10px; + padding: 1rem 1.25rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + border: 1px solid #e2e8f0; +} + +.comment-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.comment-username { + font-weight: 700; + color: #0f172a; + font-size: 0.95rem; +} + +.comment-date { + font-size: 0.8rem; + color: #94a3b8; +} + +.comment-text { + margin: 0 0 0.75rem; + font-size: 0.95rem; + line-height: 1.5; + color: #334155; +} + +/* ============================================================================= + Flagged comments (swear word detected) + ============================================================================= */ + +.comment--flagged { + border-color: #fca5a5; + background: #fef2f2; +} + +.comment--flagged .comment-text { + color: #991b1b; +} + +.comment--flagged .comment-header::after { + content: "⚠️ flagged"; + font-size: 0.75rem; + color: #dc2626; + font-weight: 600; + margin-left: auto; +} + +/* ============================================================================= + Like button + ============================================================================= */ + +.like-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.75rem; + border: 1px solid #e2e8f0; + border-radius: 20px; + background: #fff; + font-size: 0.85rem; + cursor: pointer; + color: #64748b; + transition: all 0.15s ease; +} + +.like-btn:hover { + background: #fef2f2; + border-color: #fca5a5; + color: #ef4444; +} diff --git a/courses/frontend/advanced-javascript/week4/session-materials/exercises.md b/courses/frontend/advanced-javascript/week4/session-materials/exercises.md new file mode 100644 index 00000000..18b1bad4 --- /dev/null +++ b/courses/frontend/advanced-javascript/week4/session-materials/exercises.md @@ -0,0 +1,89 @@ +# Exercises + +Work through these in order. + +### 1. Create a user class + +The class should have 2 properties: `firstName` and `lastName`. Hint: Use `this` and `constructor`. + +### 2. Create an instance of the class + +Use the `new` keyword and assign the instance in a variable. + +Add a **`renderUserCard(user)`** function that accepts a **`User`** instance and renders a user card on the page (e.g. a `div` with `firstName` and `lastName`). + +### 3. Create a class method + +1. Add **`getFullName`**: it should return the combined first and last name of the user. Use string concatenation or template literals and **`this`** to read the properties. + +2. Add **`render()`** on **`User`**: it should render the user card on the page (same job as **`renderUserCard(user)`** in exercise 2, but as an instance method). Use **`this.getFullName()`** for the name you show on the card. + +3. Call **`myUser.render()`** so the card appears on the page (you can stop using **`renderUserCard`** once this works). + +### 4. Creating a CV class + +The CV that we will be making uses three classes: `Job`, `Education` and +`CV`. The `CV` class we have made for you (with some missing functionality). The `Job` and `Education` classes you need to create. + +#### Part 1 + +Create the classes `Job` and `Education`. + +- `Job` has five properties: `id`, `title`, `description`, `startDate` and `endDate` (the dates can be strings or actual `Date` objects). +- `Education` has six properties: `id`, `title`, `school`, `address`, `startDate` and `endDate`. + +```js +class Job { + ///... +} + +class Education { + ///... +} +``` + +#### Part 2 + +Now add the functionality for the methods in the `CV` class. + +_Remember_: jobs and educations are just arrays of class instances. So use your array manipulation knowledge for the add and remove methods. + +```js +class CV { + constructor(email) { + this.jobs = []; + this.educations = []; + //this.email = ? + } + + addJob(job) { + // add functionality here + } + + removeJob(job) { + // add functionality here + } + + addEducation(education) { + // add functionality here + } + + removeEducation(education) { + // add functionality here + } +} +``` + +#### Part 3 + +1. Create a new `CV` instance using the `new` keyword, and save it in a variable called `myCV`. + +2. Apply the methods you have created on the `myCV` object. Create a few `Job` and `Education` objects and add them to your CV. + +3. Remove a job and an education from `myCV`. + +4. Log `myCV` to the console, again, and check that the objects were removed correctly. + +#### Part 4 + +Add a method to the `CV` class called `renderCV()`. This method should render out the CV using HTML. Make sure, that view updates, when data is changed. diff --git a/courses/frontend/advanced-javascript/week4/session-plan.md b/courses/frontend/advanced-javascript/week4/session-plan.md index 0b94a43f..a3be223f 100644 --- a/courses/frontend/advanced-javascript/week4/session-plan.md +++ b/courses/frontend/advanced-javascript/week4/session-plan.md @@ -2,12 +2,7 @@ ## Session Materials - - - +- [Demo](./session-materials/demo/) – In-session live coding: plain-object motivation, `Comment` class, methods, flagged comments, then Errors / Web Components as “real world” context. **index.js** = worksheet; **index-solution.js** = reference. [README](./session-materials/demo/README.md). ## Session Outline @@ -16,214 +11,25 @@ Start VERY simple. Just a class that has few fields, no methods. Explain the diff from object to class. Explain instance etc. When they get that move on to class methods. **Only teach extends if they really are on top of things** otherwise just get them comfortable with classes :) if you can repeat a bit of promise, maybe when working with class that would be great. - Constructor - - [Code inspiration](#constructor) - - [Exercise](#1-create-a-user-class) + - [Code inspiration](./session-materials/code-inspiration.md#constructor) + - [Exercise](./session-materials/exercises.md#1-create-a-user-class) - Instance - - [Code inspiration](#instance) - - [Exercise](#2-create-an-instance-of-the-class) -- Methods - - [Code inspiration](#methods) - - [Exercise](#3-create-a-class-method) + - [Code inspiration](./session-materials/code-inspiration.md#instance) + - [Exercise](./session-materials/exercises.md#2-create-an-instance-of-the-class) +- Methods (instance + static) + - [Code inspiration](./session-materials/code-inspiration.md#methods) + - [Code inspiration — static methods](./session-materials/code-inspiration.md#static-methods) (Promise as "you already use this") + - [Exercise](./session-materials/exercises.md#3-methods-on-user-getfullname-and-render) - `this` - Refers to the instance of the class. Do go into too much detail and edge cases. Avoid mentioning `bind`, `apply`, etc unless you find it super important, the trainees will just forget it anyway! -- [Exercise](#4-creating-a-cv-class) +- [Exercise](./session-materials/exercises.md#4-creating-a-cv-class) - Extend (Only if time!) - -### Constructor - -```js -class Comment { - constructor(username, content, time) { - this.username = username; - this.content = content; - this.time = time; - } -} -``` - -### Instance - -```js -const comment1 = new Comment("test", "post", new Date()); -``` - -### Methods - -```js -class Comment { - constructor(username, content, time) { - this.username = username; - this.content = content; - this.time = time; - } - - // Get help from trainees to write this method! - getTimeSincePost() { - return new Date().getTime() - this.time.getTime(); - } - - // Get help from trainees to write this method! - hasSwearWord() { - const swearWords = ["crap", "damn"]; - const postWords = this.content.split(" "); - const hasSwearWord = swearWords.find((swearWord) => - postWords.includes(swearWord), - ); - - return Boolean(hasSwearWord); - } -} - -const comment1 = new Comment("test", "post", new Date()); - -console.log(comment1.hasSwearWord()); -comment1.content = "shit crap"; -console.log(comment1.hasSwearWord()); -setTimeout(() => { - console.log(comment1.getTimeSincePost()); -}, 1000); - -// data -// username, content, time - -// functionality -// getTimeSincePost, hasSwearWord -``` - -### Class post - -```js -class Post { - // setup - constructor(username, content, postTime, likes, comments, shares) { - this.username = username; - this.content = content; - this.postTime = postTime; - this.likes = likes; - this.comments = comments; - this.shares = shares; - } - - addLike(username, time) { - const like = { - username: username, - time: time, - }; - - this.likes.push(like); - } - - addComment(username, content, time) { - this.comments.push(new Comment(username, content, time)); - } - - doShare() {} - - save() {} - - logThis() { - console.log(this.username); - } -} - -const post1 = new Post("benna100", "asd", "10/02/1019", [], [], []); -const post2 = new Post("habsdhjd", "asdajhdb", "10/02/1019", [], [], []); - -post1.addLike("bennaasdasd", "14:07"); -console.log(post1.likes); - -post1.addComment("ugg", "Great post", "14:16"); -console.log(post1.comments); - -post1.logThis(); -post2.logThis(); -``` + - [Code inspiration](./session-materials/code-inspiration.md#extending-built-ins-error-and-web-components) (`Error`, `ValidationError`, Web Components sketch — matches demo Part 4) ## Exercises - - -### 1. Create a user class - -The class should have 2 properties: firstName and lastName. Hint: Use `this` and `constructor`. - -### 2. Create an instance of the class - -Use the `new` keyword and assign the instance in a variable. - -- Try to log out the instance of the `User` to the console. -- Try to log out the users `firstName` - -### 3. Create a class method - -The method should be called `getFullName`, and should return the combined first name and last name of the user. Use string concatenation or template literals. Remember to use the `this` keyword to access the attributes on the class instance. - -Call the `getFullName` method and log the result to the console. - -### 4. Creating a CV class - -The CV that we will be making uses three classes: `Job`, `Education` and -`CV`. The `CV` class we have made for you (with some missing functionality). The `Job` and `Education` classes you need to create. - -#### Part 1 - -Create the classes `Job` and `Education`. - -- `Job` has five properties: `id`, `title`, `description`, `startDate` and `endDate` (the dates can be strings or actual `Date` objects). -- `Education` has six properties: `id`, `title`, `school`, `address`, `startDate` and `endDate`. - -```js -class Job { - ///... -} - -class Education { - ///... -} -``` - -#### Part 2 - -Now add the functionality for the methods in the `CV` class. - -_Remember_: jobs and educations are just arrays of class instances. So use your array manipulation knowledge for the add and remove methods. - -```js -class CV { - constructor(email) { - this.jobs = []; - this.educations = []; - //this.email = ? - } - - addJob(job) { - // add functionality here - } - - removeJob(job) { - // add functionality here - } - - addEducation(education) { - // add functionality here - } - - removeEducation(education) { - // add functionality here - } -} -``` - -#### Part 3 - -1. Create a new `CV` instance using the `new` keyword, and save it in a variable called `myCV`. - -2. Apply the methods you have created on the `myCV` object. Create a few `Job` and `Education` objects and add them to your CV. - -3. Remove a job and an education from `myCV`. - -4. Log `myCV` to the console, again, and check that the objects were removed correctly. +See the separate [Exercises](./session-materials/exercises.md) document. -#### Part 4 +## Code inspiration -Add a method to the `CV` class called `renderCV()`. This method should render out the CV using HTML. Use `document.getElementById("")` and `document.createElement("")`, as well as `element.appendChild()` to build your HTML using JavaScript. +See the separate [Code inspiration](./session-materials/code-inspiration.md) document. From 0f3a7e12aba74b222b32e33c0ba0aa690d69b26f Mon Sep 17 00:00:00 2001 From: markitosha Date: Sat, 28 Mar 2026 14:22:15 +0100 Subject: [PATCH 3/3] fix: linter --- courses/frontend/advanced-javascript/README.md | 2 +- .../week4/session-materials/code-inspiration.md | 7 ++++++- .../week4/session-materials/demo/README.md | 12 ++++++------ .../session-materials/demo/index-solution.js | 15 ++++++++++++--- .../week4/session-materials/demo/index.js | 2 +- .../week4/session-materials/exercises.md | 16 ++++++++-------- .../advanced-javascript/week4/session-plan.md | 2 +- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/courses/frontend/advanced-javascript/README.md b/courses/frontend/advanced-javascript/README.md index 95883666..d3847af8 100644 --- a/courses/frontend/advanced-javascript/README.md +++ b/courses/frontend/advanced-javascript/README.md @@ -11,7 +11,7 @@ In this module, you will advance your JavaScript expertise to build interactive | 1. | [Array functions & Arrow functions](./week1/README.md) | [Preparation](./week1/preparation.md) | [Session Plan](./week1/session-plan.md) (for mentors) | [Assignment](./week1/assignment.md) | | 2. | [Callback functions & Asynchronous code](./week2/README.md) | [Preparation](./week2/preparation.md) | [Session Plan](./week2/session-plan.md) (for mentors) | [Assignment](./week2/assignment.md) | | 3. | [Promises & `async`/`await`](./week3/README.md) | [Preparation](./week3/preparation.md) | [Session Plan](./week3/session-plan.md) (for mentors) | [Assignment](./week3/assignment.md) | -| 4. | [Classes & Object-Oriented Programming](./week4/README.md) | [Preparation](./week4/preparation.md) | [Session Plan](./week4/session-plan.md) (for mentors) | [Assignment](./week4/assignment.md) | +| 4. | [Classes & Object-Oriented Programming](./week4/README.md) | [Preparation](./week4/preparation.md) | [Session Plan](./week4/session-plan.md) (for mentors) | [Assignment](./week4/assignment.md) | ## Module Learning Goals diff --git a/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md b/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md index c19cdebd..4df83aab 100644 --- a/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md +++ b/courses/frontend/advanced-javascript/week4/session-materials/code-inspiration.md @@ -7,7 +7,12 @@ Snippets aligned with the in-session [demo](./demo/README.md): same `Comment` sh Why classes: repeated object literals are easy to get wrong; rendering and behavior live outside the data. ```js -const comment1 = { username: "alice", text: "Nice!", date: new Date(), likes: 0 }; +const comment1 = { + username: "alice", + text: "Nice!", + date: new Date(), + likes: 0, +}; const comment2 = { userName: "bob", text: "Hi" }; // typo: userName → UI shows undefined const comment3 = { username: "carol", content: "Oops" }; // wrong key for text diff --git a/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md b/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md index 1c364275..77da1a73 100644 --- a/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md +++ b/courses/frontend/advanced-javascript/week4/session-materials/demo/README.md @@ -6,12 +6,12 @@ In-session live coding for **Week 4** (Advanced JavaScript). The page shows a ** ## Files in this folder -| File | Purpose | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **index.js** | Worksheet: Part 1 runs on load; Parts 2–4 use `// Task:` lines, `// Next: Exercise N` pauses, and `// ==========` section banners. Use this file when leading the session. | -| **index-solution.js** | Full implementation: `Comment` with `render` / `like` / `hasSwearWord`, DOM append, `ValidationError` + `try` / `catch`, and the Web Components sketch as comments. | -| **index.html** | Two sections and containers for plain vs class comments; loads **index.js**. To preview the solution, change the `