--- id: 69b868127999e97f1903f8e1 title: Build a Flashcard Quiz App challengeType: 25 dashedName: lab-flashcard-quiz-app demoType: onClick saveSubmissionToDB: true --- # --description-- In this lab, you will create an app that displays and stores flashcard data that can be retrieved later. **Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab. **User Stories:** 1) You should have an HTML element with an id of `flashcard`. 2) You should have an interface called `FlashCard`. 3) The `FlashCard` interface should contain the following properties `questionText` and `questionAnswer` both of type `string`. 4) You should have a collection of `FlashCard` elements called `currentCards`. 5) When the `#flashcard` element is clicked, the card should have the `flipped` class. 6) You should have an element with an id of `delete-btn`. 7) When the `#delete-btn` element is clicked, it should remove current card and display the previous card data. 8) You should create an entry form with an id of `entry-form` to be able to add more flashcards to the `currentCards` collection on submit. 9) The two elements inside of the form should have an id of `front-text` and `back-text` respectively. 10) You should create and call an `InvalidUserInputError` when either the question text or question answer is empty in the entry form. # --hints-- You should have an HTML element with an id of `flashcard`. ```js const element = document.querySelector("#flashcard"); assert.exists(element); ``` You should have a `FlashCard` interface. ```js const explorer = await __helpers.Explorer(code); console.log(explorer.interfaces.FlashCard) assert.exists(explorer.interfaces.FlashCard); ``` The `FlashCard` interface should have a `questionText` property of type `string`. ```js const explorer = await __helpers.Explorer(code); const { FlashCard } = explorer.interfaces; assert.isTrue(FlashCard.hasTypeProps([{ name: "questionText", type: "string" }])); ``` The `FlashCard` interface should have a `questionAnswer` property of type `string`. ```js const explorer = await __helpers.Explorer(code); const { FlashCard } = explorer.interfaces; assert.isTrue(FlashCard.hasTypeProps([{ name: "questionAnswer", type: "string" }])); ``` You should have a collection of `FlashCard` elements called `currentCards` with the type `FlashCard[]`. ```js const explorer = await __helpers.Explorer(code); assert.exists(explorer.variables.currentCards); assert.isTrue(explorer.variables.currentCards.annotation.matches('FlashCard[]')); ``` When the `#flashcard` element is first clicked, the element should receive the `flipped` class. ```js const element = document.querySelector("#flashcard"); element.click(); assert.isTrue(element.classList.contains("flipped")); ``` You should have an element with an id of `delete-btn`. ```js const element = document.querySelector("#flashcard"); assert.exists(element); ``` When the `delete-btn` is clicked, a flashcard element should be removed from the `currentCards` collection. ```js const entryForm = document.querySelector("#entry-form"); const frontText = document.querySelector("#front-text"); const backText = document.querySelector("#back-text"); const deleteBtn = document.querySelector("#delete-btn"); const flashCard = document.querySelector("#flashcard"); const submitBtn = entryForm.querySelector("button[type='submit']"); frontText.value = "Test question"; backText.value = "Test answer"; submitBtn.click(); frontText.value = "Test question 2"; backText.value = "Test answer 2"; submitBtn.click(); deleteBtn.click(); assert.notInclude(flashCard.textContent, "Test question 2"); ``` When the `delete-btn` is clicked, the previous flashcard data should be displayed. ```js const entryForm = document.querySelector("#entry-form"); const frontText = document.querySelector("#front-text"); const backText = document.querySelector("#back-text"); const deleteBtn = document.querySelector("#delete-btn"); const flashCard = document.querySelector("#flashcard"); const submitBtn = entryForm.querySelector("button[type='submit']"); frontText.value = "Test Front 1"; backText.value = "Test Back 1"; submitBtn.click(); frontText.value = "Test Front 2"; backText.value = "Test Back 2"; submitBtn.click(); deleteBtn.click(); assert.include(flashCard.textContent, "Test Front 1"); assert.notInclude(flashCard.textContent, "Test Front 2"); ``` You should create an entry form with an id of `entry-form`. ```js const element = document.querySelector("#entry-form"); assert.exists(element); ``` You should have two `textarea` elements of ids `front-text` and `back-text` respectively inside the form. ```js const entryForm = document.querySelector("#entry-form"); const frontText = entryForm.querySelector("#front-text"); const backText = entryForm.querySelector("#back-text"); assert.exists(frontText); assert.exists(backText); assert.equal(frontText.tagName.toLowerCase(), "textarea"); assert.equal(backText.tagName.toLowerCase(), "textarea"); ``` An `InvalidUserInputError` should be thrown when either the question text or question answer is empty in the entry form. ```js const entryForm = document.querySelector("#entry-form"); const frontText = document.querySelector("#front-text"); const backText = document.querySelector("#back-text"); const submitBtn = entryForm.querySelector("button[type='submit']"); const lengthBefore = currentCards.length; frontText.value = ""; backText.value = "Some answer"; submitBtn.click(); assert.strictEqual(currentCards.length, lengthBefore); frontText.value = "Some question"; backText.value = ""; submitBtn.click(); assert.strictEqual(currentCards.length, lengthBefore); ``` # --seed-- ## --seed-contents-- ```html ``` ```css ``` ```ts ``` # --solutions-- ```html Flash Card Quiz App

Flash Card Quiz App

Manage Cards


All Cards


Add a New Card

``` ```css :root { --bg-color: #1a1d24; --surface-color: #2c313a; --primary-color: #00aaff; --primary-hover-color: #0088cc; --text-color: #e0e0e0; --text-secondary-color: #a0a0a0; --border-color: #444952; --error-color: #ff4d4d; --shadow-color: rgba(37, 33, 33, 0.2); } *, *::before, *::after { box-sizing: border-box; } html, body { margin: 0; padding: 0; height: 100%; } body { background-color: var(--bg-color); color: var(--text-color); font-family: 'Inter', system-ui, sans-serif; place-items: center; min-height: 100vh; padding: 1rem; } h1 { text-align: center; font-weight: 700; color: white; margin-bottom: 2rem; font-size: 2.5rem; } .app-container { display: flex; gap: 2rem; width: 100%; max-width: 1200px; } .flashcard-panel { flex: 2; } .controls-panel { flex: 1; background-color: var(--surface-color); border-radius: 20px; padding: 2rem; box-shadow: 0 10px 30px var(--shadow-color); border: 1px solid var(--border-color); } hr { border: none; border-top: 1px solid var(--border-color); margin: 2rem 0; } #flashcard-body { background-color: var(--surface-color); border-radius: 20px; padding: 2rem 2.5rem; height: 500px; box-shadow: 0 10px 30px var(--shadow-color); border: 1px solid var(--border-color); display: flex; flex-direction: column; } .flashcard-container { display: flex; flex-direction: column; margin-bottom: 1.5rem; perspective: 1000px; } .flashcard { height: 250px; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 12px; display: flex; align-items: center; justify-content: center; padding: 2rem; text-align: center; font-size: 2rem; transition: transform 0.2s ease-in-out; margin-bottom: 0; position: relative; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1); } .flashcard.flipped { transform: rotateY(180deg); } .card-inner { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; } .card-front, .card-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; display: flex; align-items: center; justify-content: center; padding: 2rem; } .card-front { transform: rotateY(0deg); } .card-back { transform: rotateY(180deg); } .flashcard:hover { transform: translateY(-5px); } .button-group { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.5rem; } .card-actions { display: flex; gap: 1rem; } #cards-list { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-top: 1rem; } .card { color: var(--text-color); } .selected { background: var(--primary-hover-color); } button { background-color: var(--primary-color); color: white; border: none; border-radius: 8px; padding: 12px 24px; font-family: 'Inter', sans-serif; font-weight: 500; font-size: 1rem; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; width: 100%; } button:hover { background-color: var(--primary-hover-color); } button:active { transform: scale(0.98); } button.delete-bin { background-color: #4a4e57; } button.delete-btn:hover { background-color: #60656f; } .entry-form { display: flex; flex-direction: column; gap: 1rem; text-align: left; margin-bottom: 1.5rem; } .entry-form label { font-size: 0.9rem; color: var(--text-secondary-color); font-weight: 500; } .entry-form textarea { width: 100%; padding: 12px; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-color); font-family: 'Inter', sans-serif; font-size: 1rem; resize: vertical; box-sizing: border-box; min-height: 80px; } .entry-form textarea:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(0, 170, 255, 0.3); } .error { color: var(--error-color); text-align: center; min-height: 1.2em; } @media (max-width: 992px) { .app-container { flex-direction: column; align-items: center; gap: 1.5rem; } .flashcard-panel, .controls-panel { width: 100%; max-width: 700px; } #flashcard-body { height: auto; min-height: 400px; } } @media (max-width: 480px) { body { padding: 0.5rem; } .controls-panel, #flashcard-body { padding: 1.5rem; } .flashcard { height: 200px; font-size: 1.5rem; } button { padding: 14px 20px; } } ``` ```ts const cardDisplay = document.querySelector('#current-card')!; const cardButtonsContainer = document.querySelector('#cards-list')!; const frontInput = document.querySelector('#front-text')!; const backInput = document.querySelector('#back-text')!; const errorElement = document.querySelector('#entry-error')!; let currentCardIndex = -1; let currentCards: FlashCard[] = []; interface FlashCard { questionText: string; questionAnswer: string; } class InvalidUserInputError extends Error { constructor(message: string) { super(message); this.name = 'InvalidUserInputError'; } } const isButtonElement = (element: unknown): element is HTMLButtonElement => { return element instanceof HTMLButtonElement; }; function refresh(): void { if (currentCards.length === 0 || currentCardIndex < 0) { cardDisplay.querySelector('.card-front')!.textContent = ''; cardDisplay.querySelector('.card-back')!.textContent = ''; return; } const card = currentCards[currentCardIndex]; cardDisplay.querySelector('.card-front')!.textContent = card.questionText; cardDisplay.querySelector('.card-back')!.textContent = card.questionAnswer; Array.from(cardButtonsContainer.children).forEach((child, i) => { if (i === currentCardIndex) { child.classList.add('selected'); } else { child.classList.remove('selected'); } }); } function deleteCard(): void { if (currentCardIndex < 0 || currentCards.length === 0) return; currentCards.splice(currentCardIndex, 1); const btnToRemove = cardButtonsContainer.children[currentCardIndex]; if (btnToRemove) { cardButtonsContainer.removeChild(btnToRemove); } if (currentCards.length === 0) { currentCardIndex = -1; refresh(); return; } currentCardIndex = Math.max(0, currentCardIndex - 1); Array.from(cardButtonsContainer.children).forEach((child, i) => { if (!isButtonElement(child)) { console.warn(`Element {${child}} is not a button.`); return; } (child as HTMLButtonElement).onclick = () => { currentCardIndex = i; refresh(); }; }); refresh(); } function createCardButton( questionText: string, index: number ): HTMLButtonElement { const btn = document.createElement('button'); btn.innerText = questionText.length > 20 ? questionText.slice(0, 20) + '...' : questionText; (btn as HTMLButtonElement).onclick = () => { currentCardIndex = index; refresh(); }; return btn; } function uploadNewCard(): void { try { const questionText = frontInput.value.trim(); const questionAnswer = backInput.value.trim(); if (!questionText) throw new InvalidUserInputError('Front text cannot be empty.'); if (!questionAnswer) throw new InvalidUserInputError('Back text cannot be empty.'); const newCard: FlashCard = { questionText, questionAnswer }; currentCards.push(newCard); const newIndex = currentCards.length - 1; const cardBtn = createCardButton(questionText, newIndex); cardButtonsContainer.appendChild(cardBtn); currentCardIndex = newIndex; refresh(); frontInput.value = ''; backInput.value = ''; } catch (ex) { if (ex instanceof InvalidUserInputError) { errorElement.innerHTML = '\u26A0 ' + ex.message; } else { console.error('An unexpected error occurred:', ex); } } } class FlashCardController { private elements: { flashcard: HTMLElement; entryForm: HTMLFormElement; deleteBtn: HTMLButtonElement; } = {} as { flashcard: HTMLElement; entryForm: HTMLFormElement; deleteBtn: HTMLButtonElement; }; constructor() { this.elements = { flashcard: document.querySelector('.flashcard')!, entryForm: document.querySelector('.entry-form')!, deleteBtn: document.querySelector('#delete-btn')! }; this.initializeEventListeners(); } private initializeEventListeners(): void { this.elements.flashcard.addEventListener('click', () => this.flipCard()); this.elements.entryForm.addEventListener('submit', (ev: SubmitEvent) => { ev.preventDefault(); uploadNewCard(); }); this.elements.deleteBtn.addEventListener('click', () => deleteCard()); } private flipCard(): void { this.elements.flashcard.classList.toggle('flipped'); } } document.addEventListener('DOMContentLoaded', (event: Event) => { new FlashCardController(); frontInput.value = 'What is the capital of France?'; backInput.value = 'Paris'; uploadNewCard(); }); ```