mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: add flashcard quiz app lab (#62166)
Co-authored-by: Kolade <chrisjay967@gmail.com>
This commit is contained in:
@@ -6248,6 +6248,12 @@
|
||||
"In this workshop, you will continue to practice working with TypeScript by building a fortune telling app."
|
||||
]
|
||||
},
|
||||
"lab-flashcard-quiz-app": {
|
||||
"title": "Build a Flashcard Quiz App",
|
||||
"intro": [
|
||||
"In this lab, you will practice using TypeScript by building a flashcard quiz app."
|
||||
]
|
||||
},
|
||||
"review-typescript": {
|
||||
"title": "Typescript Review",
|
||||
"intro": [
|
||||
|
||||
+701
@@ -0,0 +1,701 @@
|
||||
---
|
||||
id: 69b868127999e97f1903f8e1
|
||||
title: Build a Flashcard Quiz App
|
||||
challengeType: 25
|
||||
dashedName: lab-flashcard-quiz-app
|
||||
---
|
||||
|
||||
# --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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flash Card Quiz App</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-container">
|
||||
<div class="flashcard-panel">
|
||||
<div id="flashcard-body">
|
||||
<h1>Flash Card Quiz App</h1>
|
||||
<div class="flashcard-container">
|
||||
<div id="flashcard" class="flashcard">
|
||||
<div class="card-inner" id="current-card">
|
||||
<div class="card-front"></div>
|
||||
<div class="card-back"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
<h2>Manage Cards</h2>
|
||||
<div class="card-actions">
|
||||
<button id="delete-btn" class="delete-btn">Delete</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>All Cards</h3>
|
||||
<div id="cards-list"></div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>Add a New Card</h3>
|
||||
<form id="entry-form" class="entry-form">
|
||||
<label for="front-text">Front:</label>
|
||||
<textarea id="front-text"></textarea>
|
||||
|
||||
<label for="back-text">Back:</label>
|
||||
<textarea id="back-text"></textarea>
|
||||
|
||||
<p id="entry-error" class="error"></p>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit">Save new flash card</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```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<HTMLElement>('#current-card')!;
|
||||
const cardButtonsContainer =
|
||||
document.querySelector<HTMLElement>('#cards-list')!;
|
||||
const frontInput = document.querySelector<HTMLTextAreaElement>('#front-text')!;
|
||||
const backInput = document.querySelector<HTMLTextAreaElement>('#back-text')!;
|
||||
const errorElement =
|
||||
document.querySelector<HTMLParagraphElement>('#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<HTMLElement>('.flashcard')!,
|
||||
entryForm: document.querySelector<HTMLFormElement>('.entry-form')!,
|
||||
deleteBtn: document.querySelector<HTMLButtonElement>('#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();
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "lab-flashcard-quiz-app",
|
||||
"helpCategory": "JavaScript",
|
||||
"blockLayout": "link",
|
||||
"challengeOrder": [
|
||||
{ "id": "69b868127999e97f1903f8e1", "title": "Build a Flashcard Quiz App" }
|
||||
],
|
||||
"blockLabel": "lab",
|
||||
"usesMultifileEditor": true
|
||||
}
|
||||
@@ -101,6 +101,7 @@
|
||||
"lab-product-showcase",
|
||||
"lecture-working-with-typescript-configuration-files",
|
||||
"workshop-fortune-teller-app",
|
||||
"lab-flashcard-quiz-app",
|
||||
"review-typescript",
|
||||
"quiz-typescript"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user