From 689cdf2480823c8adc03b82ff984040586cf09e0 Mon Sep 17 00:00:00 2001
From: Kristofer Koishigawa
<2051070+scissorsneedfoodtoo@users.noreply.github.com>
Date: Sat, 31 May 2025 01:30:19 +0900
Subject: [PATCH] feat: add usememo usecallback shopping list app workshop
(#60101)
Co-authored-by: Kolade Chris <65571316+Ksound22@users.noreply.github.com>
Co-authored-by: yoko <25644062+sidemt@users.noreply.github.com>
---
client/i18n/locales/english/intro.json | 7 +-
.../workshop-search-app/index.md | 9 +
.../workshop-shopping-list-app/meta.json | 133 +++++
.../67f63e58ce657ae05908d6c8.md | 166 ++++++
.../67f647f3ce657ae05908d6c9.md | 187 +++++++
.../67fcd561ae787aba05a9d917.md | 226 ++++++++
.../67fcd83e40fa32ca2c6af073.md | 193 +++++++
.../67fcdbea6714f8df73b83c86.md | 201 +++++++
.../67fcde1677d474ebe61b4f2e.md | 206 ++++++++
.../67fcdef983296ff197e9a1cf.md | 206 ++++++++
.../67fe3fe8910ff7e2106ff340.md | 228 ++++++++
.../67fe42e8e6bbaaf3f128004c.md | 203 ++++++++
.../67fe45cc404a5604ea3a1eb9.md | 206 ++++++++
.../67fe49ea92b9fc1c09b6e314.md | 224 ++++++++
.../67fe4adca39a9b224b72d642.md | 214 ++++++++
.../67fe4fa5e660453e884d6710.md | 219 ++++++++
.../67fe512b066c4a479f82d6bd.md | 240 +++++++++
.../67fe53c21e2f8956dcaef1c5.md | 231 ++++++++
.../67fe5810b61de16e891dd02f.md | 216 ++++++++
.../67fe5f1eaea66710b08c03f1.md | 247 +++++++++
.../67fe626cc464f1238514cd8b.md | 259 +++++++++
.../67fe66d2490f143c510038bf.md | 235 +++++++++
.../67fe694ebcafe94ab7451a8f.md | 230 ++++++++
.../67ff7500255008598207a70c.md | 225 ++++++++
.../67ff7e6d04d17507e8923f46.md | 227 ++++++++
.../67ff7f5515db250dc015f128.md | 242 +++++++++
.../67ff8059bac7f0146857c2b1.md | 234 +++++++++
.../67ff838da1cb2927ae18a3f5.md | 268 ++++++++++
.../67ff89b745907449e0e5bae4.md | 226 ++++++++
.../67ff8defd6747d621c4b838c.md | 242 +++++++++
.../67ff8eacd3d965670c9121f9.md | 493 ++++++++++++++++++
.../6808baa8f8dcaf4f50a7acaa.md | 229 ++++++++
.../6808bcac6bd35f568a19f21a.md | 211 ++++++++
.../superblock-structure/full-stack.json | 3 +
34 files changed, 7085 insertions(+), 1 deletion(-)
create mode 100644 client/src/pages/learn/full-stack-developer/workshop-search-app/index.md
create mode 100644 curriculum/challenges/_meta/workshop-shopping-list-app/meta.json
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f63e58ce657ae05908d6c8.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f647f3ce657ae05908d6c9.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd561ae787aba05a9d917.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd83e40fa32ca2c6af073.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdbea6714f8df73b83c86.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcde1677d474ebe61b4f2e.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdef983296ff197e9a1cf.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe3fe8910ff7e2106ff340.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe42e8e6bbaaf3f128004c.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe45cc404a5604ea3a1eb9.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe49ea92b9fc1c09b6e314.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4adca39a9b224b72d642.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4fa5e660453e884d6710.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe512b066c4a479f82d6bd.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe53c21e2f8956dcaef1c5.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5810b61de16e891dd02f.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5f1eaea66710b08c03f1.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe626cc464f1238514cd8b.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe66d2490f143c510038bf.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe694ebcafe94ab7451a8f.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7500255008598207a70c.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7e6d04d17507e8923f46.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7f5515db250dc015f128.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8059bac7f0146857c2b1.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff838da1cb2927ae18a3f5.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff89b745907449e0e5bae4.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8defd6747d621c4b838c.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8eacd3d965670c9121f9.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808baa8f8dcaf4f50a7acaa.md
create mode 100644 curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808bcac6bd35f568a19f21a.md
diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json
index 1be019935e7..cfb61106491 100644
--- a/client/i18n/locales/english/intro.json
+++ b/client/i18n/locales/english/intro.json
@@ -3616,7 +3616,12 @@
"In these lecture videos, you will learn about data fetching and memoization in React."
]
},
- "ffpt": { "title": "279", "intro": [] },
+ "workshop-shopping-list-app": {
+ "title": "Build a Shopping List App",
+ "intro": [
+ "In this workshop, you'll use the useMemo() and useCallback() hooks in React to build a simple shopping list app. You'll learn more about state and the lifecycle of React components, and how to use memoization to reduce re-renders and make your apps more efficient."
+ ]
+ },
"lab-currency-converter": {
"title": "Build a Currency Converter",
"intro": [
diff --git a/client/src/pages/learn/full-stack-developer/workshop-search-app/index.md b/client/src/pages/learn/full-stack-developer/workshop-search-app/index.md
new file mode 100644
index 00000000000..ea92214199c
--- /dev/null
+++ b/client/src/pages/learn/full-stack-developer/workshop-search-app/index.md
@@ -0,0 +1,9 @@
+---
+title: Introduction to the Build a Shopping List App Workshop
+block: workshop-shopping-list-app
+superBlock: full-stack-developer
+---
+
+## Introduction to the Build a Shopping List App Workshop
+
+In this workshop, you'll use the `useMemo()` and `useCallback()` hooks in React to build a simple shopping list app. You'll learn more about state and the lifecycle of React components, and how to use memoization to reduce re-renders and make your apps more efficient.
diff --git a/curriculum/challenges/_meta/workshop-shopping-list-app/meta.json b/curriculum/challenges/_meta/workshop-shopping-list-app/meta.json
new file mode 100644
index 00000000000..13a97b7040e
--- /dev/null
+++ b/curriculum/challenges/_meta/workshop-shopping-list-app/meta.json
@@ -0,0 +1,133 @@
+{
+ "name": "Build a Shopping List App",
+ "blockType": "workshop",
+ "blockLayout": "challenge-grid",
+ "isUpcomingChange": false,
+ "usesMultifileEditor": true,
+ "hasEditableBoundaries": true,
+ "dashedName": "workshop-shopping-list-app",
+ "superBlock": "full-stack-developer",
+ "challengeOrder": [
+ {
+ "id": "67f63e58ce657ae05908d6c8",
+ "title": "Step 1"
+ },
+ {
+ "id": "67f647f3ce657ae05908d6c9",
+ "title": "Step 2"
+ },
+ {
+ "id": "67fcd561ae787aba05a9d917",
+ "title": "Step 3"
+ },
+ {
+ "id": "67fcd83e40fa32ca2c6af073",
+ "title": "Step 4"
+ },
+ {
+ "id": "67fcdbea6714f8df73b83c86",
+ "title": "Step 5"
+ },
+ {
+ "id": "67fcde1677d474ebe61b4f2e",
+ "title": "Step 6"
+ },
+ {
+ "id": "67fcdef983296ff197e9a1cf",
+ "title": "Step 7"
+ },
+ {
+ "id": "67fe3fe8910ff7e2106ff340",
+ "title": "Step 8"
+ },
+ {
+ "id": "67fe42e8e6bbaaf3f128004c",
+ "title": "Step 9"
+ },
+ {
+ "id": "67fe45cc404a5604ea3a1eb9",
+ "title": "Step 10"
+ },
+ {
+ "id": "67fe49ea92b9fc1c09b6e314",
+ "title": "Step 11"
+ },
+ {
+ "id": "67fe4adca39a9b224b72d642",
+ "title": "Step 12"
+ },
+ {
+ "id": "67fe4fa5e660453e884d6710",
+ "title": "Step 13"
+ },
+ {
+ "id": "67fe512b066c4a479f82d6bd",
+ "title": "Step 14"
+ },
+ {
+ "id": "67fe53c21e2f8956dcaef1c5",
+ "title": "Step 15"
+ },
+ {
+ "id": "67fe5810b61de16e891dd02f",
+ "title": "Step 16"
+ },
+ {
+ "id": "6808baa8f8dcaf4f50a7acaa",
+ "title": "Step 17"
+ },
+ {
+ "id": "6808bcac6bd35f568a19f21a",
+ "title": "Step 18"
+ },
+ {
+ "id": "67fe5f1eaea66710b08c03f1",
+ "title": "Step 19"
+ },
+ {
+ "id": "67fe626cc464f1238514cd8b",
+ "title": "Step 20"
+ },
+ {
+ "id": "67fe66d2490f143c510038bf",
+ "title": "Step 21"
+ },
+ {
+ "id": "67fe694ebcafe94ab7451a8f",
+ "title": "Step 22"
+ },
+ {
+ "id": "67ff7500255008598207a70c",
+ "title": "Step 23"
+ },
+ {
+ "id": "67ff838da1cb2927ae18a3f5",
+ "title": "Step 24"
+ },
+ {
+ "id": "67ff89b745907449e0e5bae4",
+ "title": "Step 25"
+ },
+ {
+ "id": "67ff7e6d04d17507e8923f46",
+ "title": "Step 26"
+ },
+ {
+ "id": "67ff8defd6747d621c4b838c",
+ "title": "Step 27"
+ },
+ {
+ "id": "67ff7f5515db250dc015f128",
+ "title": "Step 28"
+ },
+ {
+ "id": "67ff8059bac7f0146857c2b1",
+ "title": "Step 29"
+ },
+ {
+ "id": "67ff8eacd3d965670c9121f9",
+ "title": "Step 30"
+ }
+ ],
+ "helpCategory": "JavaScript"
+}
\ No newline at end of file
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f63e58ce657ae05908d6c8.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f63e58ce657ae05908d6c8.md
new file mode 100644
index 00000000000..2f9a186c3ee
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f63e58ce657ae05908d6c8.md
@@ -0,0 +1,166 @@
+---
+id: 67f63e58ce657ae05908d6c8
+title: Step 1
+challengeType: 0
+dashedName: step-1
+demoType: onLoad
+---
+
+# --description--
+
+In this workshop, you'll build a simple shopping list app to practice using the `useMemo()` and `useCallback()` hooks in React. You'll learn how to use these hooks to optimize the performance of your app by memoizing potentially expensive calculations and functions.
+
+All the basic HTML and CSS you'll need has been provided for you, along with the basic structure of a React component named `ShoppingList`.
+
+Within the `ShoppingList` component, return an empty pair of parentheses for now.
+
+# --hints--
+
+You should use the `export` keyword to export the `ShoppingList` component.
+
+```js
+assert.match(code, /export\s+(const|let|var|function)\s+ShoppingList\s*(=\s*)?\(\)\s*(=>\s*)?\{\s*/);
+```
+
+You should return an empty pair of round parentheses inside the `ShoppingList` function.
+
+```js
+assert.match(code, /export\s+(const|let|var|function)\s+ShoppingList\s*(=\s*)?\(\)\s*(=>\s*)?\{\s*return\s*\(\s*\)\s*;?\s*\}/)
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+--fcc-editable-region--
+const { useState } = React;
+
+export const ShoppingList = () => {
+
+};
+--fcc-editable-region--
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f647f3ce657ae05908d6c9.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f647f3ce657ae05908d6c9.md
new file mode 100644
index 00000000000..5b1ed2f78b7
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67f647f3ce657ae05908d6c9.md
@@ -0,0 +1,187 @@
+---
+id: 67f647f3ce657ae05908d6c9
+title: Step 2
+challengeType: 0
+dashedName: step-2
+---
+
+# --description--
+
+Within the `return` statement of the `ShoppingList` component, add a `div` element with a `className` of `container`.
+
+Inside the `div`, nest an `h1` element with the text `Shopping List`, and below that, nest an empty `form` element.
+
+# --hints--
+
+Your `ShoppingList` component should return a `div` element with a `className` of `container`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ assert.equal(testElem.firstElementChild?.tagName, 'DIV');
+ assert.isTrue(
+ [...testElem.firstElementChild.classList].includes('container')
+ );
+}
+```
+
+Your `div` should contain an `h1` element with the text `Shopping List`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const h1 = testElem.querySelector('h1');
+ assert.exists(h1);
+ assert.equal(h1.textContent.toLowerCase().trim(), 'shopping list');
+}
+```
+
+Your `div` should also contain an empty `form` element.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const form = testElem.querySelector('form');
+ assert.exists(form);
+ assert.equal(form.tagName, 'FORM');
+}
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+--fcc-editable-region--
+const { useState } = React;
+
+export const ShoppingList = () => {
+ return (
+
+ );
+};
+--fcc-editable-region--
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd561ae787aba05a9d917.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd561ae787aba05a9d917.md
new file mode 100644
index 00000000000..94305bfe44b
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd561ae787aba05a9d917.md
@@ -0,0 +1,226 @@
+---
+id: 67fcd561ae787aba05a9d917
+title: Step 3
+challengeType: 0
+dashedName: step-3
+---
+
+# --description--
+
+Within the `form` element, add a `label` element with the text `Search for an item:`. Next, give the `label` an `htmlFor` attribute set to `search`. This will associate the label with an `input` element you'll add next.
+
+Below the `label`, add an `input` element with the `type` and `id` attributes set to `search`. Also, give the `input` a placeholder of `Search...`, and an `aria-describedby` attribute set to `search-description`.
+
+Finally, add a `p` element below the `input`, and give it the text `Type to filter the list below:` and an `id` of `search-description`. This will provide additional context for screen readers.
+
+# --hints--
+
+Your `form` element should contain a `label` element with the text `Search for an item:`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const label = testElem.querySelector('form label');
+ assert.exists(label);
+ assert.equal(label.textContent.toLowerCase().trim(), 'search for an item:');
+}
+```
+
+Your `label` should have an `htmlFor` attribute set to `search`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const label = testElem.querySelector('form label');
+ assert.equal(label.getAttribute('for'), 'search');
+}
+```
+
+Your `form` element should contain an `input` element with the `type` and `id` attributes set to `search`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const input = testElem.querySelector('form input');
+ assert.exists(input);
+ assert.equal(input.getAttribute('type'), 'search');
+ assert.equal(input.getAttribute('id'), 'search');
+}
+```
+
+Your `input` should have a placeholder of `Search...` and an `aria-describedby` attribute set to `search-description`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const input = testElem.querySelector('form input');
+ assert.equal(input.getAttribute('placeholder').toLowerCase().trim(), 'search...');
+ assert.equal(input.getAttribute('aria-describedby'), 'search-description');
+}
+```
+
+Your `form` element should contain a `p` element with the text `Type to filter the list below:`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const p = testElem.querySelector('form p');
+ assert.exists(p);
+ assert.equal(p.textContent.toLowerCase().trim(), 'type to filter the list below:');
+}
+```
+
+Your `p` element should have an `id` of `search-description`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const p = testElem.querySelector('form p');
+ assert.equal(p.getAttribute('id'), 'search-description');
+}
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+export const ShoppingList = () => {
+ return (
+
+
Shopping List
+
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd83e40fa32ca2c6af073.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd83e40fa32ca2c6af073.md
new file mode 100644
index 00000000000..13351bf0691
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcd83e40fa32ca2c6af073.md
@@ -0,0 +1,193 @@
+---
+id: 67fcd83e40fa32ca2c6af073
+title: Step 4
+challengeType: 0
+dashedName: step-4
+---
+
+# --description--
+
+Now that you have the basic structure of the app, it's time to add some items and get the search functionality working.
+
+Create an array and assign it to a variable named `items`. Within the array, add the strings `Apples`, `Bananas`, `Strawberries`, `Blueberries`, `Mangoes`, `Pineapple`, `Lettuce`, `Broccoli`, `Paper Towels`, and `Dish Soap`. This will be the list of items that will be displayed in the app.
+
+# --hints--
+
+`items` is an array.
+
+```js
+assert.match(code, /(const|let|var)\s+items\s*=\s*\[/i);
+```
+
+Your `items` array should contain the strings `Apples`, `Bananas`, `Strawberries`, `Blueberries`, `Mangoes`, `Pineapple`, `Lettuce`, `Broccoli`, `Paper Towels`, and `Dish Soap`.
+
+```js
+const expected = [
+ "Apples", "Bananas", "Strawberries", "Blueberries", "Mangoes",
+ "Pineapple", "Lettuce", "Broccoli", "Paper Towels", "Dish Soap"
+];
+const itemArrContentsString = code.match(/(?:const|let|var)\s+items\s*=\s*\[\s*([\s\S]*?)\s*\]/)?.[1] ?? null;
+const itemArr = itemArrContentsString ? itemArrContentsString.split(",").map(item => item.replace(/['"]/g, "")) : [];
+const normalizeArray = (arr) => arr.map((str) => str.toLowerCase().replace(/\W/g, "")).filter(Boolean);
+const normalizedItemArr = normalizeArray(itemArr);
+const normalizedExpected = normalizeArray(expected);
+
+assert.isTrue(
+ normalizedItemArr.length === normalizedExpected.length &&
+ normalizedItemArr.every((item) => normalizedExpected.includes(item))
+);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+--fcc-editable-region--
+
+--fcc-editable-region--
+
+export const ShoppingList = () => {
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdbea6714f8df73b83c86.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdbea6714f8df73b83c86.md
new file mode 100644
index 00000000000..159e81267ac
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdbea6714f8df73b83c86.md
@@ -0,0 +1,201 @@
+---
+id: 67fcdbea6714f8df73b83c86
+title: Step 5
+challengeType: 0
+dashedName: step-5
+---
+
+# --description--
+
+Next, you need to create a state variable, a setter function, and set the initial state with the `useState()` hook to store the user's input.
+
+As a reminder, here's how to create a state variable named `age`, a setter function to update it named `setAge`, and initialize `age` to the number `0` with the `useState()` hook:
+
+```js
+const [age, setAge] = useState(0);
+```
+
+At the top of the `ShoppingList` component, use the `useState()` hook to create a state variable named `query` and a setter function to update it named `setQuery`. Initialize `query` to an empty string. This will be used to store the user's search input.
+
+# --hints--
+
+You should create a state variable named `query` and a setter function named `setQuery`.
+
+```js
+assert.match(code, /(const|let|var)\s+\[\s*query\s*,\s*setQuery\s*\]\s*=\s*useState\(/);
+```
+
+The initial value of `query` should be an empty string.
+
+```js
+const queryValue = code.match(/(?:const|let|var)\s+\[\s*query\s*,\s*setQuery\s*\]\s*=\s*useState\(\s*(['"])(.*?)\1\s*\)/)?.[2] ?? null;
+assert.typeOf(queryValue, "string");
+assert.lengthOf(queryValue, 0);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcde1677d474ebe61b4f2e.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcde1677d474ebe61b4f2e.md
new file mode 100644
index 00000000000..b2a1d0e18cc
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcde1677d474ebe61b4f2e.md
@@ -0,0 +1,206 @@
+---
+id: 67fcde1677d474ebe61b4f2e
+title: Step 6
+challengeType: 0
+dashedName: step-6
+---
+
+# --description--
+
+In the input element, add a `value` attribute and set it to the `query` state variable. This will allow the input field to display the current value of `query`. Also, remember that you need to use curly braces (`{}`) to embed JavaScript expressions like `query` in JSX.
+
+Then, add an `onChange` event handler to the input element. Pass it an anonymous function that takes `e` as an argument, which is the event object. Inside the anonymous function, call `setQuery()` and pass it `e.target.value`. This will update the `query` state variable with the current value of the input field whenever the user types in it.
+
+# --hints--
+
+You should add a `value` attribute to the input element and set it to the `query` state variable.
+
+```js
+assert.match(code, / ]*value={\s*query\s*}[^>]*\/?>/);
+```
+
+Your `onChange` event handler should be a function that takes `e` as an argument.
+
+```js
+const input = document.querySelector('input');
+const key = Object.keys(input).find(key => key.startsWith("__reactProps"));
+const reactInput = input[key];
+assert.match(reactInput.onChange.toString(), /onChange\s*\(\s*e\s*\)/);
+```
+
+Your `onChange` event handler should call `setQuery()` with `e.target.value`.
+
+```js
+const input = document.querySelector('input');
+const key = Object.keys(input).find(key => key.startsWith("__reactProps"));
+const reactInput = input[key];
+assert.match(reactInput.onChange.toString(), /setQuery\s*\(\s*e\.target\.value\s*\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdef983296ff197e9a1cf.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdef983296ff197e9a1cf.md
new file mode 100644
index 00000000000..348166a1e89
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fcdef983296ff197e9a1cf.md
@@ -0,0 +1,206 @@
+---
+id: 67fcdef983296ff197e9a1cf
+title: Step 7
+challengeType: 0
+dashedName: step-7
+---
+
+# --description--
+
+Create a variable named `filteredItems` and assign it the result of filtering the `items` array with the `filter()` method. Pass an anonymous function to the `filter()` method that takes `item` as an argument. Inside the anonymous function, just return `item` for now.
+
+# --hints--
+
+You should create a variable named `filteredItems`.
+
+```js
+assert.match(code, /(const|let|var)\s+filteredItems\s*=/);
+```
+
+You should use the `filter()` method on the `items` array.
+
+```js
+assert.match(code, /items\.filter\s*\(/);
+```
+
+Your `filter()` method should take an anonymous function that takes `item` as an argument and returns `item`.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item;?\s*}\s*\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe3fe8910ff7e2106ff340.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe3fe8910ff7e2106ff340.md
new file mode 100644
index 00000000000..c44e7e5e0ad
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe3fe8910ff7e2106ff340.md
@@ -0,0 +1,228 @@
+---
+id: 67fe3fe8910ff7e2106ff340
+title: Step 8
+challengeType: 0
+dashedName: step-8
+---
+
+# --description--
+
+While you're not really filtering anything yet, and `filteredItems` matches the `items` array, it would be nice to see the list of items on the page.
+
+First, add a `ul` element below the `p` element in the `form`. Inside the unordered list, use the `map()` method to iterate over the `filteredItems` array, and pass an anonymous function to it. The anonymous function should take `item` as an argument, and return an `li` element for each item. Each `li` should display the `item` text.
+
+# --hints--
+
+Your `form` element should contain a `ul` element.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ assert.exists(testElem.querySelector('form ul'));
+}
+```
+
+You should use the `map()` method on the `filteredItems` array within the `ul` element.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(/);
+```
+
+Your `map()` method should take an anonymous function that takes `item` as an argument.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(\s*item\s*\)\s*{/);
+```
+
+The anonymous function for `map()` should return an `li` element with the `item` text for each element in the `filteredItems` array.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(\s*item\s*\)\s*{\s*.+React.createElement\("li", null, item\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+ const filteredItems = items.filter((item) => item);
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe42e8e6bbaaf3f128004c.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe42e8e6bbaaf3f128004c.md
new file mode 100644
index 00000000000..c5ae9258f7f
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe42e8e6bbaaf3f128004c.md
@@ -0,0 +1,203 @@
+---
+id: 67fe42e8e6bbaaf3f128004c
+title: Step 9
+challengeType: 0
+dashedName: step-9
+---
+
+# --description--
+
+If you check the console, you'll see a warning about a missing `key` prop. This is because React needs a unique key for each element in a list to help it identify which items have changed, are added, or are removed. It is also important for performance reasons.
+
+To fix this, add a `key` prop to the `li` element inside the `map()` method. Set the `key` to the value of `item`. Generally, you should use a unique identifier like an id as the `key`, but since the items are all unique strings, you can use the string itself in this case.
+
+# --hints--
+
+The `key` prop for your `li` element should be set to the value of `item`.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /React.createElement\("li", {\s*key:\s+item\s*}, item\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+ const filteredItems = items.filter((item) => item);
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe45cc404a5604ea3a1eb9.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe45cc404a5604ea3a1eb9.md
new file mode 100644
index 00000000000..76734dd693e
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe45cc404a5604ea3a1eb9.md
@@ -0,0 +1,206 @@
+---
+id: 67fe45cc404a5604ea3a1eb9
+title: Step 10
+challengeType: 0
+dashedName: step-10
+---
+
+# --description--
+
+Now it's time to finish the search functionality.
+
+The search input should be case-insensitive, and should also match partial strings. For example, if a user types in `app`, it should match both `Apples` and `Pineapples`. This is because the lowercase version of `app` is a substring of the lowercase version of both `Apples` and `Pineapples`.
+
+First, inside the `filter()` method, use the `toLowerCase()` method to convert `item` into a lowercase string.
+
+# --hints--
+
+You should use the `toLowerCase()` method on `item` within the `filter()` method.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\);?\s*}\s*\)/);
+
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+ --fcc-editable-region--
+ const filteredItems = items.filter((item) => item);
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe49ea92b9fc1c09b6e314.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe49ea92b9fc1c09b6e314.md
new file mode 100644
index 00000000000..5f26e5c88b8
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe49ea92b9fc1c09b6e314.md
@@ -0,0 +1,224 @@
+---
+id: 67fe49ea92b9fc1c09b6e314
+title: Step 11
+challengeType: 0
+dashedName: step-11
+---
+
+# --description--
+
+Next, use the `includes()` method to check if the lowercase version of `item` includes the lowercase version of `query` as a substring.
+
+For example, here's how to use the `toLowerCase()` and `includes()` methods to check if the string `freeCodeCamp` includes `code`:
+
+```js
+const str1 = "freeCodeCamp";
+const str2 = "code";
+str1.toLowerCase().includes(str2.toLowerCase()); // true
+```
+
+Chain the `includes()` method to the lowercase `item` and check if it includes the lowercase `query`. Remember to lowercase `query` as well.
+
+# --hints--
+
+You should chain the `includes()` method to the lowercase `item`.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\).includes\(\s*/);
+
+```
+
+You should check if the lowercase `item` string includes the lowercase `query` as a substring.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\)\.includes\(\s*query\.toLowerCase\(\s*\)\s*\);?\s*}\s*\)/);
+
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+
+--fcc-editable-region--
+ const filteredItems = items.filter((item) => item.toLowerCase());
+--fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4adca39a9b224b72d642.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4adca39a9b224b72d642.md
new file mode 100644
index 00000000000..c96b7854ff2
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4adca39a9b224b72d642.md
@@ -0,0 +1,214 @@
+---
+id: 67fe4adca39a9b224b72d642
+title: Step 12
+challengeType: 0
+dashedName: step-12
+---
+
+# --description--
+
+Now the search input should work as expected, and filter the shopping list based on the user's input.
+
+Next, we'll add a way to check off items in the list so users can keep track of what they've already purchased.
+
+To do this, we'll need another state variable and a setter function. Use the `useState` hook to create a state variable named `selectedItems` and a setter function named `setSelectedItems`. Initialize `selectedItems` as an empty array. This array will store the items the user has selected, and we'll apply some styling to those selected items later.
+
+# --hints--
+
+You should create a state variable named `selectedItems` and a setter function named `setSelectedItems`.
+
+```js
+assert.match(code, /(const|let)\s+\[\s*selectedItems\s*,\s*setSelectedItems\s*\]\s*=\s*useState\(/);
+```
+
+The initial value of `selectedItems` should be an empty array.
+
+```js
+const selectedItemsValue = code.match(/(?:const|let)\s+\[\s*selectedItems\s*,\s*setSelectedItems\s*\]\s*=\s*useState\(\s*(\[\s*(.*?)\s*\])\s*\)/)?.[1] ?? null;
+
+assert.isTrue(
+ selectedItemsValue &&
+ selectedItemsValue?.replace(/\s/g, "") === "[]"
+);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4fa5e660453e884d6710.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4fa5e660453e884d6710.md
new file mode 100644
index 00000000000..a43a083d9ce
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe4fa5e660453e884d6710.md
@@ -0,0 +1,219 @@
+---
+id: 67fe4fa5e660453e884d6710
+title: Step 13
+challengeType: 0
+dashedName: step-13
+---
+
+# --description--
+
+Create a function named `toggleItem` that takes `item` as an argument.
+
+Inside the function, just call the `setSelectedItems()` function for now. We'll come back a bit later to implement the logic to toggle selected items.
+
+# --hints--
+
+You should have a function named `toggleItem` that takes `item` as an argument.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /function\s+toggleItem\s*\(\s*item\s*\)\s*{/);
+```
+
+You should call the `setSelectedItems()` function inside the `toggleItem()` function.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /function\s+toggleItem\s*\(\s*item\s*\)\s*{(.|\n)*setSelectedItems\s*\(\s*\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe512b066c4a479f82d6bd.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe512b066c4a479f82d6bd.md
new file mode 100644
index 00000000000..3d84f56486c
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe512b066c4a479f82d6bd.md
@@ -0,0 +1,240 @@
+---
+id: 67fe512b066c4a479f82d6bd
+title: Step 14
+challengeType: 0
+dashedName: step-14
+---
+
+# --description--
+
+Within the `li` element, above the `item` text, add an `input` element with the `type` attribute set to `checkbox`. Also, set its `onChange` attribute to an anonymous function that calls `toggleItem()` with `item` as an argument.
+
+Finally, wrap the `input` and the `item` text in a `label` element for better accessibility.
+
+# --hints--
+
+You should add another `input` element with the `type` attribute set to `checkbox`.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const checkbox = testElem.querySelector('input[type="checkbox"]');
+
+ assert.exists(checkbox);
+}
+```
+
+Your new `input` should have an `onChange` attribute set to an anonymous function that calls `toggleItem()` with `item` as an argument.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const checkbox = testElem.querySelector('input[type="checkbox"]');
+ const key = checkbox && Object.keys(checkbox).find(key => key.startsWith("__reactProps"));
+ const reactCheckbox = checkbox && checkbox[key];
+ const onChangeString = reactCheckbox && reactCheckbox?.onChange?.toString();
+
+ assert.match(onChangeString, /function onChange\s*\(\s*\)\s*{\s*(return)?\s+toggleItem\s*\(\s*item\s*\);?\s*\}/);
+}
+```
+
+Your new `input` and the `item` text should be wrapped in a `label` element.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const label = testElem.querySelector('li label');
+ const key = label && Object.keys(label)?.find(key => key.startsWith("__reactProps"));
+ const reactLabel = label && label[key];
+ const reactLabelChildren = reactLabel?.children || [];
+
+ assert.lengthOf(reactLabelChildren, 2);
+ assert.isTrue(reactLabelChildren.some((item => item.type === 'input')));
+ assert.isTrue(reactLabelChildren.some((item => typeof item === 'string')));
+}
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems();
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe53c21e2f8956dcaef1c5.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe53c21e2f8956dcaef1c5.md
new file mode 100644
index 00000000000..816a1ae4b8c
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe53c21e2f8956dcaef1c5.md
@@ -0,0 +1,231 @@
+---
+id: 67fe53c21e2f8956dcaef1c5
+title: Step 15
+challengeType: 0
+dashedName: step-15
+---
+
+# --description--
+
+Back to the `toggleItem()` function.
+
+If you recall from a past lecture, you can compare past and present states in React, and use that to determine an upcoming state. In this case, you'll use the past and present states to either add or remove items from the `selectedItems` array.
+
+First, call the `setSelectedItems()` function, and pass it an anonymous function as an argument. This anonymous function should take `prev` as an argument, which is the previous state of `selectedItems`.
+
+Inside the anonymous function, use the `includes` method to check if `prev` includes `item` and return the result.
+
+# --hints--
+
+You should pass an anonymous function that takes `prev` as an argument to the `setSelectedItems()` function.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /function\s*toggleItem\s*\(\s*item\s*\)\s*{(.|\n)*setSelectedItems\s*\(function\s*\(\s*prev\s*\)\s*{/);
+```
+
+Use the `includes()` method inside the anonymous function to check if `prev` includes `item`, and return the result of that check.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /function\s*\(\s*prev\s*\)\s*{\s*return\s+prev\.includes\s*\(\s*item\s*\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ --fcc-editable-region--
+ const toggleItem = (item) => {
+ setSelectedItems();
+ };
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5810b61de16e891dd02f.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5810b61de16e891dd02f.md
new file mode 100644
index 00000000000..8e568df8c09
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5810b61de16e891dd02f.md
@@ -0,0 +1,216 @@
+---
+id: 67fe5810b61de16e891dd02f
+title: Step 16
+challengeType: 0
+dashedName: step-16
+---
+
+# --description--
+
+Rather than just returning `prev.includes(item)`, return a ternary operator with `prev.includes(item)` as the condition. Just return `null` for both cases for now.
+
+# --hints--
+
+Return a ternary operator that checks if `prev.includes(item)` is true. Return `null` for both the truthy and falsy cases.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /setSelectedItems\s*\(function\s*\(\s*prev\s*\)\s*{\s*return\s+prev\.includes\s*\(\s*item\s*\)\s*\?\s*null\s*:\s*null;?\s*}/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ --fcc-editable-region--
+ prev.includes(item)
+ --fcc-editable-region--
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5f1eaea66710b08c03f1.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5f1eaea66710b08c03f1.md
new file mode 100644
index 00000000000..fc7fd8f5b97
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe5f1eaea66710b08c03f1.md
@@ -0,0 +1,247 @@
+---
+id: 67fe5f1eaea66710b08c03f1
+title: Step 19
+challengeType: 0
+dashedName: step-19
+---
+
+# --description--
+
+Now you'll work on some logic to cross off any selected items on the list.
+
+First off, since everything inside of this `filteredItems.map()` call is being implicitly returned, wrap all the code within it in curly braces, and return the `li` element explicitly.
+
+For example, here's how you would do this with a simple component that renders a list of names:
+
+```jsx
+const names = ["Abbey", "Beau", "Quincy"];
+
+const NameList = () => {
+ return (
+
+ {names.map((name) => {
+ return (
+ {name}
+ );
+ })}
+
+ );
+};
+```
+
+# --hints--
+
+You should wrap the code within the `filteredItems.map()` call in curly braces.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(code, /{\s*filteredItems\.map\(\s*(function)?\s*\(?\s*item\s*\)?\s*\)\s*(=>)?\s*{(.|\n)*}\s*\);?\s*}/);
+```
+
+You should return the `li` element explicitly.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(code, /{\s*filteredItems\.map\(\s*(function)?\s*\(?\s*item\s*\)?\s*\)\s*(=>)?\s*{\s*return\s*\(\s*(.|\n)*}\s*\);?\s*}/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe626cc464f1238514cd8b.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe626cc464f1238514cd8b.md
new file mode 100644
index 00000000000..df0cfc9d35e
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe626cc464f1238514cd8b.md
@@ -0,0 +1,259 @@
+---
+id: 67fe626cc464f1238514cd8b
+title: Step 20
+challengeType: 0
+dashedName: step-20
+---
+
+# --description--
+
+Before the `return` statement within the `filteredItems.map()` call, create a variable called `isChecked` and use the `includes()` method to check if the current `item` is in the `selectedItems` array.
+
+Then, add a `style` prop to the `li` element, and use a ternary operator to set the `textDecoration` property to `line-through` if `isChecked` is true, or `none` if it is false. Remember that you need to wrap the ternary operator in curly braces since it's a JavaScript expression.
+
+For example, here's how you can use a ternary operator to conditionally set the color of a paragraph in a simple component:
+
+```jsx
+const MyComponent = () => {
+ const isActive = true;
+
+ return (
+ This text is conditionally styled.
+ );
+};
+```
+
+# --hints--
+
+You should create a variable called `isChecked` which uses the `includes()` method on the `selectedItems` array to check if the current `item` is in the array.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*isChecked\s*=\s*selectedItems\.includes\s*\(\s*item\s*\)/);
+```
+
+You should add a `style` prop to the `li` element.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const li = testElem.querySelector('li');
+ const key = li && Object.keys(li)?.find(key => key.startsWith("__reactProps"));
+ const reactLi = li && li[key];
+
+ assert.exists(reactLi.style);
+}
+```
+
+Your `style` prop should include a ternary operator that sets the `textDecoration` property to `line-through` if `isChecked` is true, or `none` if it is false.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /"li"\s*,\s*{(.|\n)*style:\s*{\s*textDecoration\s*:\s*isChecked\s*\?\s*("|')line-through("|')\s*:\s*("|')none("|')/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe66d2490f143c510038bf.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe66d2490f143c510038bf.md
new file mode 100644
index 00000000000..6f3104fe515
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe66d2490f143c510038bf.md
@@ -0,0 +1,235 @@
+---
+id: 67fe66d2490f143c510038bf
+title: Step 21
+challengeType: 0
+dashedName: step-21
+---
+
+# --description--
+
+Then, in the checkbox `input` element, add a `checked` attribute and set it to `isChecked`. While it's not strictly necessary to set the `checked` attribute, it's a good practice to ensure that each checkbox reflects the current state of the `selectedItems` array.
+
+# --hints--
+
+You should add a `checked` attribute to the checkbox `input` element.
+
+```js
+async () => {
+ const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
+ const checkbox = testElem.querySelector('input[type="checkbox"]');
+ const key = checkbox && Object.keys(checkbox)?.find(key => key.startsWith("__reactProps"));
+ const reactCheckbox = checkbox && checkbox[key];
+
+ assert.exists(reactCheckbox.checked);
+}
+```
+
+You should set the `checked` attribute to `isChecked`.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /"input"\s*,\s*{(.|\n)*checked:\s*isChecked/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe694ebcafe94ab7451a8f.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe694ebcafe94ab7451a8f.md
new file mode 100644
index 00000000000..04a40fa8397
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67fe694ebcafe94ab7451a8f.md
@@ -0,0 +1,230 @@
+---
+id: 67fe694ebcafe94ab7451a8f
+title: Step 22
+challengeType: 0
+dashedName: step-22
+---
+
+# --description--
+
+Your app works! Go ahead and test it out.
+
+However, it's not as efficient as it could be. While React is already very performant, there are some cases where it can be better to cache the results of potentially expensive calculations, or to ensure that functions are not recreated on every render.
+
+We'll improve the performance of your app over the following steps. But first, let's add some logging so you can see the lifecycle of this component more clearly.
+
+Above `filteredItems`, add a `console.log()` statement that logs the string `Filtering items...` to the console.
+
+# --hints--
+
+You should add a `console.log()` statement above `filteredItems` that logs the string `Filtering items...` to the console.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /console\.log\s*\(\s*("|'|`)\s*Filtering\s+items\s*\.\.\.\s*("|'|`)\s*\);?/i);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7500255008598207a70c.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7500255008598207a70c.md
new file mode 100644
index 00000000000..5acd1f5dcb6
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7500255008598207a70c.md
@@ -0,0 +1,225 @@
+---
+id: 67ff7500255008598207a70c
+title: Step 23
+challengeType: 0
+dashedName: step-23
+---
+
+# --description--
+
+Now you will see `Filtering items...` in the console every time you type in the search bar and check or uncheck an item. This is because those actions update the state of the component, which causes it to re-render.
+
+This isn't usually a problem. But if you have a lot of items in the list, or if you're fetching a lot of data from an API and manipulating it, your app can feel slow.
+
+To improve performance, you can use the `useMemo()` hook to memoize, or in other words, cache, the result of the filtering operation. Then, React will only re-run the filtering operation when its dependency changes, rather than on every render.
+
+First, destructure the `useMemo()` hook from React at the top of your file.
+
+# --hints--
+
+You should destructure `useMemo` from `React` at the top of the file. Make sure you aren't removing the `useState` hook you're already using.
+
+```js
+assert.match(code, /(const|let|var)\s*{[^}]*\buse(State|Memo)\b[^}]*\buse(Memo|State)\b[^}]*}\s*=\s*React/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+--fcc-editable-region--
+const { useState } = React;
+--fcc-editable-region--
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ console.log("Filtering items...");
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7e6d04d17507e8923f46.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7e6d04d17507e8923f46.md
new file mode 100644
index 00000000000..1cb40947454
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7e6d04d17507e8923f46.md
@@ -0,0 +1,227 @@
+---
+id: 67ff7e6d04d17507e8923f46
+title: Step 26
+challengeType: 0
+dashedName: step-26
+---
+
+# --description--
+
+Another thing React does each time it re-renders a component is recreate the functions inside of it. Here, every time you check or uncheck an item, the `toggleItem()` function is recreated. This is not a problem in most cases, but can lead to performance issues in larger apps.
+
+Let's add some logging to track this.
+
+Above the `ShoppingList` component, use `let` to create a variable named `prevToggleItem` and set it to `null`. You'll use this to track the function definition of `toggleItem()` across renders.
+
+# --hints--
+
+You should use `let` to create a variable named `prevToggleItem` and set it to `null`.
+
+```js
+assert.match(code, /let\s+prevToggleItem\s*=\s*null/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+--fcc-editable-region--
+
+--fcc-editable-region--
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7f5515db250dc015f128.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7f5515db250dc015f128.md
new file mode 100644
index 00000000000..52e4bc62eb7
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff7f5515db250dc015f128.md
@@ -0,0 +1,242 @@
+---
+id: 67ff7f5515db250dc015f128
+title: Step 28
+challengeType: 0
+dashedName: step-28
+---
+
+# --description--
+
+Now that you can see when the `toggleItem()` function definition is first set or changes, you should also log when the function is the same across renders.
+
+Add an `else` statement to the `if` statement you just wrote. Inside the `else` block, log the string `Current toggleItem function` to the console.
+
+# --hints--
+
+You should add an `else` statement to the `if` statement you wrote in the last step.
+
+```js
+assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*}\s*else\s*{/si);
+```
+
+You should log the string `Current toggleItem function` to the console inside the `else` statement.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx")?.innerText || '';
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList?.toString() || '';
+
+console.log(shoppingListString);
+assert.match(shoppingListString, /else\s*{\s*console\.log\s*\(\s*("|')\s*Current\s+toggleItem\s+function\s*("|')\s*\);?/si);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+let prevToggleItem = null;
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ --fcc-editable-region--
+ if (prevToggleItem !== toggleItem) {
+ console.log("New toggleItem function");
+ prevToggleItem = toggleItem;
+ }
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8059bac7f0146857c2b1.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8059bac7f0146857c2b1.md
new file mode 100644
index 00000000000..ee765b34447
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8059bac7f0146857c2b1.md
@@ -0,0 +1,234 @@
+---
+id: 67ff8059bac7f0146857c2b1
+title: Step 29
+challengeType: 0
+dashedName: step-29
+---
+
+# --description--
+
+If you use the app now, you'll see that the `toggleItem()` function is recreated every time you check or uncheck an item, or type in the search bar.
+
+To improve performance, you can use the `useCallback()` hook to memoize or cache the `toggleItem()` function. Then React will only recreate the function when its dependencies change.
+
+First, destructure the `useCallback()` hook from React at the top of your file.
+
+# --hints--
+
+You should destructure `useCallback` from `React` at the top of the file. Make sure you aren't removing any other hooks you're already using.
+
+```js
+assert.match(code, /(const|let|var)\s*{[^}]*\buse(State|Memo|Callback)\b[^}]*\buse(State|Memo|Callback)\b[^}]*\buse(State|Memo|Callback)\b[^}]*}\s*=\s*React/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+--fcc-editable-region--
+const { useState, useMemo } = React;
+--fcc-editable-region--
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+let prevToggleItem = null;
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ if (prevToggleItem !== toggleItem) {
+ console.log("New toggleItem function");
+ prevToggleItem = toggleItem;
+ } else {
+ console.log("Current toggleItem function");
+ }
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff838da1cb2927ae18a3f5.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff838da1cb2927ae18a3f5.md
new file mode 100644
index 00000000000..7801293722d
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff838da1cb2927ae18a3f5.md
@@ -0,0 +1,268 @@
+---
+id: 67ff838da1cb2927ae18a3f5
+title: Step 24
+challengeType: 0
+dashedName: step-24
+---
+
+# --description--
+
+`useMemo()` takes two arguments: a function that returns the value you want to memoize, and an array of dependencies. The memoized value will only be recomputed when one or more of its dependencies change.
+
+Here's the basic syntax for `useMemo()`:
+
+```js
+const memoizedValue = useMemo(() => {
+ // Some expensive calculation
+ return value;
+}, [dependency1, dependency2]);
+```
+
+Set `filteredItems` equal to `useMemo()`, and pass it an anonymous function with curly braces. Inside the curly braces, use a `return` statement to explicitly return your existing filtering logic. Also, add `query` as the only dependency in the dependencies array. This ensures that the filtering operation is only re-run when `query` changes.
+
+# --hints--
+
+You should set `filteredItems` equal to calling the `useMemo()` hook.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(/);
+```
+
+You should pass an anonymous function to `useMemo()` with curly braces.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{/);
+```
+
+You should use a `return` statement to explicitly return your existing filtering logic.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{\s*return\s+items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\).includes\(\s*query\.toLowerCase\(\s*\)\s*\);?\s*}\s*\);?\s*}/);
+```
+
+You should add `query` as the only dependency in the dependencies array.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{(.|\n)*},\s*\[\s*query\s*\]\s*\);?/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ console.log("Filtering items...");
+ --fcc-editable-region--
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ --fcc-editable-region--
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff89b745907449e0e5bae4.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff89b745907449e0e5bae4.md
new file mode 100644
index 00000000000..f9ff0a3b5e4
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff89b745907449e0e5bae4.md
@@ -0,0 +1,226 @@
+---
+id: 67ff89b745907449e0e5bae4
+title: Step 25
+challengeType: 0
+dashedName: step-25
+---
+
+# --description--
+
+Finally, move your `console.log()` statement inside the `useMemo()` callback function, just above the `return` statement.
+
+# --hints--
+
+You should move your `Filtering items...` log before the `return` statement inside the `useMemo()` callback function.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{\s*console\.log\s*\(\s*("|'|`)\s*Filtering\s+items\s*\.\.\.\s*("|'|`)\s*\);?\s*return\s+items\.filter/i);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ --fcc-editable-region--
+ console.log("Filtering items...");
+ const filteredItems = useMemo(() => {
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+ --fcc-editable-region--
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8defd6747d621c4b838c.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8defd6747d621c4b838c.md
new file mode 100644
index 00000000000..6c98d9bc052
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8defd6747d621c4b838c.md
@@ -0,0 +1,242 @@
+---
+id: 67ff8defd6747d621c4b838c
+title: Step 27
+challengeType: 0
+dashedName: step-27
+---
+
+# --description--
+
+Next, write an `if` statement to check if `prevToggleItem` is not strictly equal to `toggleItem`. If they are not equal, log the string `New toggleItem function` to the console. Then, set `prevToggleItem` equal to `toggleItem`.
+
+# --hints--
+
+You should write an `if` statement to check if `prevToggleItem` is not strictly equal to `toggleItem`.
+
+```js
+assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)/);
+```
+
+You should log the string `New toggleItem function` to the console inside the `if` statement if the condition is met.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx")?.innerText || '';
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList?.toString() || '';
+
+assert.match(shoppingListString, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*console\.log\s*\(\s*("|')\s*New\s+toggleItem\s+function\s*("|')\s*\);?/si);
+```
+
+You should set `prevToggleItem` equal to `toggleItem` inside the `if` statement if the condition is met.
+
+```js
+assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*prevToggleItem\s*=\s*toggleItem;?\s*/si);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+let prevToggleItem = null;
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+
+ --fcc-editable-region--
+
+ --fcc-editable-region--
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8eacd3d965670c9121f9.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8eacd3d965670c9121f9.md
new file mode 100644
index 00000000000..6d1daca3f34
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/67ff8eacd3d965670c9121f9.md
@@ -0,0 +1,493 @@
+---
+id: 67ff8eacd3d965670c9121f9
+title: Step 30
+challengeType: 0
+dashedName: step-30
+---
+
+# --description--
+
+The `useCallback()` hook takes two arguments: a function and an array of dependencies. The function will only be recreated if one of the dependencies changes.
+
+Here's the basic syntax for `useCallback()`:
+
+```js
+const memoizedCallback = useCallback(() => {
+ // Your function logic
+ return value;
+}, [dependency1, dependency2]);
+```
+
+Set `toggleItem` equal to `useCallback()`, and pass it an anonymous function that takes `item` as an argument. Move your existing `toggleItem()` function logic inside the anonymous function, where `setSelectedItems` is called. Finally, add `setSelectedItems` as a dependency in the dependencies array.
+
+After that, you're done optimizing! Your app should still work the same way, but you'll only see your console logs when the `query` state changes or when the `toggleItem()` function is recreated.
+
+# --hints--
+
+You should set `toggleItem` equal to calling the `useCallback()` hook.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(/);
+```
+
+You should pass an anonymous function that takes `item` as an argument to `useCallback()`.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{/);
+```
+
+You should move your existing `toggleItem()` function logic, starting with the call to `setSelectedItems()`, inside the anonymous function.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{\s*(return)?\s+setSelectedItems\s*\(\s*function\s*\(prev\)\s*{\s*return\s+prev\.includes\(item\)\s*\?\s*prev\.filter\(function\s*\(i\)\s*\{\s*return\s+i\s!==\sitem;\s*\}\s*\)\s*:\s*\[\].concat\(_toConsumableArray\(prev\),\s+\[item\]\);\s*}\);?/);
+```
+
+You should add `setSelectedItems` as the only dependency in the dependencies array.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{.*},\s*\[\s*setSelectedItems\s*\]\s*\);?/si);
+```
+
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo, useCallback } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+let prevToggleItem = null;
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ --fcc-editable-region--
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ };
+ --fcc-editable-region--
+
+ if (prevToggleItem !== toggleItem) {
+ console.log("New toggleItem function");
+ prevToggleItem = toggleItem;
+ } else {
+ console.log("Current toggleItem function");
+ }
+
+ return (
+
+ );
+};
+
+```
+
+# --solutions--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState, useMemo, useCallback } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+let prevToggleItem = null;
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = useMemo(() => {
+ console.log("Filtering items...");
+ return items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }, [query]);
+
+ const toggleItem = useCallback(
+ (item) => {
+ setSelectedItems((prev) =>
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
+ );
+ },
+ [setSelectedItems]
+ );
+
+ if (prevToggleItem !== toggleItem) {
+ console.log("New toggleItem function");
+ prevToggleItem = toggleItem;
+ } else {
+ console.log("Current toggleItem function");
+ }
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808baa8f8dcaf4f50a7acaa.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808baa8f8dcaf4f50a7acaa.md
new file mode 100644
index 00000000000..44b6ee60495
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808baa8f8dcaf4f50a7acaa.md
@@ -0,0 +1,229 @@
+---
+id: 6808baa8f8dcaf4f50a7acaa
+title: Step 17
+challengeType: 0
+dashedName: step-17
+---
+
+# --description--
+
+For the truthy condition, if `prev` includes `item`, return a filtered array with `item` removed.
+
+Chain the `filter()` method to `prev` and pass it an anonymous function that takes `i` as an argument. Inside the function, check that `i` is not strictly equal to `item`. This will return a new array with all items except `item`.
+
+# --hints--
+
+You should chain the `filter()` method to `prev` and pass it an anonymous function that takes `i` as an argument.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /prev\.includes\s*\(\s*item\s*\)\s*\?\s*prev\.filter\s*\(function\s*\(\s*i\s*\)\s*{/);
+```
+
+Inside the anonymous `filter()` function, you should check that `i` is not strictly equal to `item`. Remember to return the result of that check.
+
+```js
+const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
+const exports = {};
+const a = eval(script);
+const shoppingListString = exports.ShoppingList.toString();
+
+assert.match(shoppingListString, /prev\.includes\s*\(\s*item\s*\)\s*\?\s*prev\.filter\s*\(function\s*\(\s*i\s*\)\s*{\s*return\s+i\s*!==\s*item;?\s*}\s*\)/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ --fcc-editable-region--
+ prev.includes(item) ? null : null
+ --fcc-editable-region--
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808bcac6bd35f568a19f21a.md b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808bcac6bd35f568a19f21a.md
new file mode 100644
index 00000000000..445766a581b
--- /dev/null
+++ b/curriculum/challenges/english/25-front-end-development/workshop-shopping-list-app/6808bcac6bd35f568a19f21a.md
@@ -0,0 +1,211 @@
+---
+id: 6808bcac6bd35f568a19f21a
+title: Step 18
+challengeType: 0
+dashedName: step-18
+---
+
+# --description--
+
+For the falsy condition, use the spread operator to return a copy of the `prev` array with `item` appended to the end. This will add `item` to the `selectedItems` array.
+
+# --hints--
+
+You should use the spread operator to clone the `prev` array with `item` appended to the end.
+
+```js
+assert.match(code, /\:\s*\[\s*\.\.\.prev\s*\,\s*item\s*\]/);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+
+
+
+ Shopping List
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```css
+:root {
+ --dark-grey: #1b1b32;
+ --light-grey: #f5f6f7;
+ --dark-orange: #f89808;
+ --yellow: #f1be32;
+ --golden-yellow: #feac32;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--dark-grey);
+ background-color: var(--dark-grey);
+}
+
+main,
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.container {
+ background-color: var(--light-grey);
+ width: 90%;
+ margin: 20px;
+ padding: 10px;
+}
+
+h1 {
+ color: var(--dark-grey);
+}
+
+form {
+ text-align: center;
+}
+
+button {
+ cursor: pointer;
+}
+
+button {
+ cursor: pointer;
+ width: 100px;
+ margin: 3px;
+ color: var(--dark-grey);
+ background-color: var(--golden-yellow);
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-color: var(--golden-yellow);
+ border-width: 3px;
+}
+
+button:hover {
+ background-image: linear-gradient(#ffcc4c, #f89808);
+}
+
+input {
+ color: var(--dark-grey);
+ margin-left: 5px;
+ padding: 3px;
+}
+
+li {
+ text-align: left;
+ margin: 10px 0;
+}
+
+.selected-item {
+ font-weight: bold;
+}
+
+@media (min-width: 425px) {
+ .container {
+ width: 400px;
+ }
+}
+
+```
+
+```jsx
+const { useState } = React;
+
+const items = [
+ "Apples",
+ "Bananas",
+ "Strawberries",
+ "Blueberries",
+ "Mangoes",
+ "Pineapple",
+ "Lettuce",
+ "Broccoli",
+ "Paper Towels",
+ "Dish Soap",
+];
+
+export const ShoppingList = () => {
+ const [query, setQuery] = useState("");
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const filteredItems = items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const toggleItem = (item) => {
+ setSelectedItems((prev) =>
+ --fcc-editable-region--
+ prev.includes(item) ? prev.filter((i) => i !== item) : null
+ --fcc-editable-region--
+ );
+ };
+
+ return (
+
+ );
+};
+
+```
diff --git a/curriculum/superblock-structure/full-stack.json b/curriculum/superblock-structure/full-stack.json
index 1bcecd96ce4..d5ab4dc7bff 100644
--- a/curriculum/superblock-structure/full-stack.json
+++ b/curriculum/superblock-structure/full-stack.json
@@ -1078,6 +1078,9 @@
{
"dashedName": "lecture-working-with-data-fetching-and-memoization-in-react"
},
+ {
+ "dashedName": "workshop-shopping-list-app"
+ },
{
"dashedName": "lab-currency-converter"
},