fix(curriculum): merge fortune teller final steps (#69582)

This commit is contained in:
Caio Bittencourt
2026-08-18 04:24:48 -03:00
committed by GitHub
parent 45acf8166e
commit cecb7be0f7
4 changed files with 629 additions and 1813 deletions
@@ -9,20 +9,68 @@ dashedName: step-53
At this point, you should be able to see the fortune each card shows once you click a card. The only thing left to do is to make the new reading button work.
Inside the `newReading` method, call `showElements()` and `hideElements()`.
Inside the `newReading` method, call `showElements` and pass `this.elements.singleCardBtn`, `this.elements.multipleCardsBtn`, and `this.elements.headerTitle` as arguments.
Then call `hideElements` and pass `this.elements.singleCard`, `this.elements.multipleCard`, `this.elements.fortuneContainer`, and `this.elements.fortuneDescription` as arguments.
With that, your Fortune Telling app is complete!
# --hints--
You should call the `showElements` function inside the `newReading` method.
You should pass `singleCardBtn` as the first argument to the `showElements` function.
```js
assert.match(__helpers.removeJSComments(code), /newReading\s*\(\s*\)\s*\{[\s\S]*?showElements\s*\(/);
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\(\s*this\.elements\.singleCardBtn/);
```
You should call the `hideElements` function inside the `newReading` method.
You should pass `multipleCardsBtn` as the second argument to the `showElements` function.
```js
assert.match(__helpers.removeJSComments(code), /newReading\s*\(\s*\)\s*\{[\s\S]*?hideElements\s*\(/);
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\(\s*this\.elements\.singleCardBtn\s*,\s*this\.elements\.multipleCardsBtn/);
```
You should pass `headerTitle` as an argument to the `showElements` function.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\([\s\S]*?this\.elements\.headerTitle/);
```
You should pass `singleCard` as the first argument to `hideElements`.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /hideElements\s*\(\s*this\.elements\.singleCard\b/);
```
You should pass `multipleCard` as an argument to `hideElements`.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /hideElements\s*\([\s\S]*?this\.elements\.multipleCard\b/);
```
You should pass `fortuneContainer` as an argument to `hideElements`.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /hideElements\s*\([\s\S]*?this\.elements\.fortuneContainer/);
```
You should pass `fortuneDescription` as an argument to `hideElements`.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /hideElements\s*\([\s\S]*?this\.elements\.fortuneDescription/);
```
# --seed--
@@ -592,3 +640,578 @@ class Game {
const game = new Game();
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fortune Telling App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main id="main_container">
<header>
<h1>Mystic Tarot</h1>
<h6>
Discover the wisdom of the cards and unveil the mysteries of your path
</h6>
</header>
<div id="btn_title_container">
<div class="title_wrapper">
<h2 class="header_title">Choose Your Reading</h2>
</div>
<div class="btn_wrapper">
<button class="btn btn_singleCard" id="btn-single-card">
Single Card Reading
</button>
<button class="btn btn_threeCard" id="btn-multiple-cards">
Three Card Spread
</button>
</div>
</div>
<section class="fortune_container hidden">
<div class="single_card"></div>
<h2 class="hidden text">Click the card and reveal the fortune</h2>
<div class="multiple_card hidden"></div>
<div class="fortune_description hidden">
<h2 class="title"></h2>
<h5 class="sub_title"></h5>
<div class="description_wrapper">
<span class="desc_title">Description:</span>
<p class="description"></p>
</div>
</div>
<button class="btn_reveal">New Reading</button>
</section>
</main>
<script src="index.ts" type="module"></script>
</body>
</html>
```
```css
:root {
/* Background Colors */
--bg-main: #0b0f1a;
--bg-card: #131622;
--bg-secondary: #232633;
/* Text Colors */
--text-primary: #f7e580;
--text-muted: #b8a880;
--text-dark: #0b0f1a;
/* Border Colors */
--border-main: #2a2d3a;
--border-input: #232633;
/* Accent Colors */
--accent-primary: #8080ff;
--accent-secondary: #b8a866;
--accent-mystic: #bf80ff;
--accent-light-cosmic: #b3b3ff;
--accent-light-gold: #f2d966;
/* Interactive Colors */
--focus-ring: #8080ff;
--destructive: #e53e3e;
/* Gradient Colors */
--gradient-cosmic-from: #8080ff;
--gradient-cosmic-to: #bf80ff;
--gradient-gold-from: #b8a866;
--gradient-gold-to: #f2d966;
--gradient-card-from: #131622;
--gradient-card-to: #1a1e2e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
color: var(--text-primary);
text-align: center;
min-height: 100vh;
background-size: 100% 100%; /* forces stretch */
background-repeat: no-repeat;
background-image: linear-gradient(to bottom, #1e1b4b, #3b0764, #0f172a);
font-family:
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
}
header {
background: rgb(0 0 0 / 0.2);
padding: 1rem;
}
h1 {
color: var(--accent-mystic);
font-weight: 700;
font-size: 3rem;
padding-bottom: 12px;
}
h2 {
color: var(--accent-light-gold);
font-weight: 700;
font-size: 1.75rem;
margin: 2rem 0;
}
h6 {
color: var(--accent-secondary);
font-size: 1.8rem;
font-weight: 1.75;
}
h5 {
color: var(--text-muted);
font-size: 1.1rem;
}
.fortune_description {
width: 99%;
border: 1px solid var(--accent-light-cosmic);
border-radius: 0.5rem;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
margin: 1rem auto;
padding: 1rem;
h2 {
margin: 0;
padding-bottom: 1rem;
}
background: linear-gradient(
to bottom,
rgba(15, 23, 42, 0.9),
rgba(30, 41, 59, 0.9)
);
}
.description_wrapper {
background-color: rgba(88, 28, 135, 0.2);
border-radius: 0.5rem;
border: 1px solid rgba(168, 85, 247, 0.2);
color: var(--accent-secondary);
padding: 2rem;
margin: 1rem 0;
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
justify-content: center;
}
.btn_wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
justify-content: center;
}
.btn {
color: var(--text-dark);
font-weight: 600;
font-size: 1.125rem;
padding: 1rem 3rem;
cursor: pointer;
white-space: nowrap;
border-radius: 0.3em;
outline: none;
border: none;
}
.btn_singleCard {
background-image: linear-gradient(
to right,
var(--gradient-cosmic-from),
var(--gradient-cosmic-to)
);
}
.btn_threeCard {
color: var(--accent-light-gold);
background-image: linear-gradient(
to right,
var(--gradient-cosmic-to),
var(--gradient-cosmic-from)
);
}
.btn:hover {
background-image: linear-gradient(to right, var(--gradient-cosmic-to));
}
.btn_reveal {
color: var(--text-dark);
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
cursor: pointer;
outline: none;
border: none;
background-color: var(--gradient-gold-to);
border-radius: 0.3rem;
margin: 1rem;
}
.btn_reveal:hover {
background-color: var(--gradient-gold-from);
}
.hidden {
display: none;
}
.card_container {
position: relative;
width: 10rem;
height: 15rem;
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
margin: 0 auto;
overflow: hidden;
border-radius: 0.5rem;
cursor: pointer;
border: 2px solid var(--accent-light-gold);
box-shadow: 0 0 7px var(--accent-light-gold);
img {
width: 100%;
}
}
.reversed-card {
transform: rotate(180deg);
transition: transform 0.4s ease;
}
.reversed-card:hover {
transform: rotate(0deg) translateY(-4%);
}
.card_front span:nth-child(2) {
font-size: 1.3rem;
}
.card_front span:nth-child(1) {
font-size: 1.8rem;
}
.card_back {
display: flex;
align-items: center;
width: 10rem;
height: 15rem;
border: 2px solid var(--gradient-cosmic-from);
border-radius: 0.5rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
}
.multiple_card {
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
flex-direction: column;
}
.card-shield span {
opacity: 10;
z-index: 3;
}
/* Small devices (phones in landscape, ≥ 600px) */
@media screen and (min-width: 600px) {
h1 {
font-size: 3rem;
}
.btn_wrapper {
flex-direction: row;
}
.fortune_description {
padding: 2rem;
width: 60%;
}
.multiple_card {
gap: 30px;
flex-direction: row;
}
}
/* Add to your CSS file (e.g., style.css) */
.img-loader {
width: 80px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.img-loader:after {
content: "";
display: block;
width: 40px;
height: 40px;
border: 4px solid #ccc;
border-top: 4px solid #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.card-img.hidden {
display: none;
}
```
```ts
interface Card {
name: string;
value: string | number;
name_short: string;
value_int: number;
suit: string;
type: string;
img: string;
meaning_up: string;
meaning_rev: string;
desc: string;
}
interface Deck {
cards: Card[];
}
const CDN_URL = "https://cdn.freecodecamp.org/curriculum/typescript/tarot-app";
const defaultImg = new URL("default.svg", CDN_URL + "/");
const LOCAL_DEFAULT_IMG = defaultImg;
const getElement = <T extends HTMLElement>(selector: string): T => {
const el = document.querySelector<T>(selector);
if (!el) throw new Error(`Element not found: ${selector}`);
return el;
};
const hideElements = (...elements: HTMLElement[]) =>
elements.forEach((el) => el.classList.add("hidden"));
const showElements = (...elements: HTMLElement[]) =>
elements.forEach((el) => el.classList.remove("hidden"));
const getRandomItem = <T,>(items: T[]): T => {
const index = Math.floor(Math.random() * items.length);
return items[index];
};
const renderCard = (drawingType: string, isReversed: boolean, shortName: string, img: string): string => `
<div>
<h2>${drawingType}</h2>
<figure class="card_container ${isReversed ? "reversed-card" : ""}" data-id="${shortName}">
<div class="img-loader"></div>
<img
src="${img ? `${CDN_URL}/${img}` : LOCAL_DEFAULT_IMG}"
class="card-img hidden"
onload="this.classList.remove('hidden');this.previousElementSibling.style.display='none';"
onerror="
if (!this.dataset.failed) {
this.dataset.failed = '1';
this.src='${LOCAL_DEFAULT_IMG}';
} else {
this.classList.remove('hidden');
this.previousElementSibling.style.display='none';
}
"
/>
</figure>
</div>
`;
enum DrawingType {
Past = "past",
Present = "present",
Future = "future",
}
class Game {
cards: Card[] = [];
private elements: {
singleCardBtn: HTMLElement;
singleCard: HTMLElement;
multipleCardsBtn: HTMLElement;
multipleCard: HTMLElement;
title: HTMLElement;
newReadingBtn: HTMLElement;
fortuneContainer: HTMLElement;
fortuneDescription: HTMLElement;
headerTitle: HTMLElement;
subTitle: HTMLElement;
cardTitle: HTMLElement;
description: HTMLElement;
text: HTMLElement;
}
constructor() {
this.elements = {
singleCardBtn: getElement("#btn-single-card"),
singleCard: getElement(".single_card"),
multipleCardsBtn: getElement("#btn-multiple-cards"),
multipleCard: getElement(".multiple_card"),
title: getElement(".title"),
newReadingBtn: getElement(".btn_reveal"),
fortuneContainer: getElement(".fortune_container"),
fortuneDescription: getElement(".fortune_description"),
headerTitle: getElement(".header_title"),
subTitle: getElement(".sub_title"),
cardTitle: getElement(".desc_title"),
description: getElement(".description"),
text: getElement(".text"),
}
this.fetchCardsData();
this.initializeEventListeners();
}
private async fetchCardsData() {
try {
const response = await fetch(`${CDN_URL}/card_data.json`);
const data: Deck = await response.json();
this.cards = data.cards;
} catch (error) {
console.error("Error fetching cards:", error);
}
}
private initializeEventListeners(): void {
this.elements.singleCardBtn.addEventListener("click", () =>
this.singleCardSelected(),
);
this.elements.multipleCardsBtn.addEventListener("click", () =>
this.multipleCardSelected(),
);
this.elements.newReadingBtn.addEventListener("click", () =>
this.newReading(),
);
document.addEventListener("click", (e: Event) => this.showFortune(e));
}
singleCardSelected() {
hideElements(
this.elements.singleCardBtn,
this.elements.multipleCardsBtn,
this.elements.multipleCard,
this.elements.text,
this.elements.headerTitle,
);
const isReversed = Math.random() < 0.5;
const chosenCard = getRandomItem(this.cards);
this.elements.singleCard.innerHTML = renderCard(
"Click the card and reveal the fortune",
isReversed,
chosenCard.name_short,
chosenCard.img,
);
this.elements.multipleCard.innerHTML = "";
showElements(this.elements.singleCard, this.elements.fortuneContainer);
}
multipleCardSelected() {
hideElements(
this.elements.singleCard,
this.elements.singleCardBtn,
this.elements.multipleCardsBtn,
this.elements.headerTitle,
);
showElements(
this.elements.multipleCard,
this.elements.fortuneContainer,
this.elements.text,
);
this.elements.multipleCard.innerHTML = Object.values(DrawingType)
.map((type) => {
const isReversed = Math.random() < 0.5;
const card = getRandomItem(this.cards);
return renderCard(type, isReversed, card.name_short, card.img);
})
.join("");
}
showFortune(e: Event) {
const target = e.target
if (!(target instanceof HTMLElement)) {
return;
}
const cardElement = target?.closest(".card_container")
if (!(cardElement instanceof HTMLElement)) {
return;
}
if (!cardElement) {
return;
}
const cardId = cardElement.getAttribute("data-id");
const foundCard = this.cards.find((card) => card.name_short === cardId);
if (foundCard) {
this.elements.cardTitle.textContent = foundCard.name;
this.elements.description.textContent = foundCard.desc;
this.elements.subTitle.textContent = foundCard.meaning_up;
this.elements.title.textContent = foundCard.name;
showElements(this.elements.fortuneDescription);
}
}
newReading() {
showElements(
this.elements.singleCardBtn,
this.elements.multipleCardsBtn,
this.elements.headerTitle,
);
hideElements(
this.elements.singleCard,
this.elements.multipleCard,
this.elements.fortuneContainer,
this.elements.fortuneDescription,
);
}
}
const game = new Game();
```
@@ -1,608 +0,0 @@
---
id: 68f1ebc05a602f99d49d701d
title: Step 54
challengeType: 0
dashedName: step-54
---
# --description--
Now inside the `showElements`, pass these as arguments: `singleCardBtn`, `multipleCardsBtn`, and `headerTitle`. Remember you get them from the `elements`, and access them with the `this` keyword.
# --hints--
You should pass `singleCardBtn` as the first argument to the `showElements` function.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\(\s*this\.elements\.singleCardBtn/);
```
You should pass `multipleCardsBtn` as the second argument to the `showElements` function.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\(\s*this\.elements\.singleCardBtn\s*,\s*this\.elements\.multipleCardsBtn/);
```
You should pass `headerTitle` as an argument to the `showElements` function.
```js
const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Game.methods.newReading.toString();
assert.match(method, /showElements\s*\([\s\S]*?this\.elements\.headerTitle/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fortune Telling App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main id="main_container">
<header>
<h1>Mystic Tarot</h1>
<h6>
Discover the wisdom of the cards and unveil the mysteries of your path
</h6>
</header>
<div id="btn_title_container">
<div class="title_wrapper">
<h2 class="header_title">Choose Your Reading</h2>
</div>
<div class="btn_wrapper">
<button class="btn btn_singleCard" id="btn-single-card">
Single Card Reading
</button>
<button class="btn btn_threeCard" id="btn-multiple-cards">
Three Card Spread
</button>
</div>
</div>
<section class="fortune_container hidden">
<div class="single_card"></div>
<h2 class="hidden text">Click the card and reveal the fortune</h2>
<div class="multiple_card hidden"></div>
<div class="fortune_description hidden">
<h2 class="title"></h2>
<h5 class="sub_title"></h5>
<div class="description_wrapper">
<span class="desc_title">Description:</span>
<p class="description"></p>
</div>
</div>
<button class="btn_reveal">New Reading</button>
</section>
</main>
<script src="index.ts" type="module"></script>
</body>
</html>
```
```css
:root {
/* Background Colors */
--bg-main: #0b0f1a;
--bg-card: #131622;
--bg-secondary: #232633;
/* Text Colors */
--text-primary: #f7e580;
--text-muted: #b8a880;
--text-dark: #0b0f1a;
/* Border Colors */
--border-main: #2a2d3a;
--border-input: #232633;
/* Accent Colors */
--accent-primary: #8080ff;
--accent-secondary: #b8a866;
--accent-mystic: #bf80ff;
--accent-light-cosmic: #b3b3ff;
--accent-light-gold: #f2d966;
/* Interactive Colors */
--focus-ring: #8080ff;
--destructive: #e53e3e;
/* Gradient Colors */
--gradient-cosmic-from: #8080ff;
--gradient-cosmic-to: #bf80ff;
--gradient-gold-from: #b8a866;
--gradient-gold-to: #f2d966;
--gradient-card-from: #131622;
--gradient-card-to: #1a1e2e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
color: var(--text-primary);
text-align: center;
min-height: 100vh;
background-size: 100% 100%; /* forces stretch */
background-repeat: no-repeat;
background-image: linear-gradient(to bottom, #1e1b4b, #3b0764, #0f172a);
font-family:
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
}
header {
background: rgb(0 0 0 / 0.2);
padding: 1rem;
}
h1 {
color: var(--accent-mystic);
font-weight: 700;
font-size: 3rem;
padding-bottom: 12px;
}
h2 {
color: var(--accent-light-gold);
font-weight: 700;
font-size: 1.75rem;
margin: 2rem 0;
}
h6 {
color: var(--accent-secondary);
font-size: 1.8rem;
font-weight: 1.75;
}
h5 {
color: var(--text-muted);
font-size: 1.1rem;
}
.fortune_description {
width: 99%;
border: 1px solid var(--accent-light-cosmic);
border-radius: 0.5rem;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
margin: 1rem auto;
padding: 1rem;
h2 {
margin: 0;
padding-bottom: 1rem;
}
background: linear-gradient(
to bottom,
rgba(15, 23, 42, 0.9),
rgba(30, 41, 59, 0.9)
);
}
.description_wrapper {
background-color: rgba(88, 28, 135, 0.2);
border-radius: 0.5rem;
border: 1px solid rgba(168, 85, 247, 0.2);
color: var(--accent-secondary);
padding: 2rem;
margin: 1rem 0;
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
justify-content: center;
}
.btn_wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
justify-content: center;
}
.btn {
color: var(--text-dark);
font-weight: 600;
font-size: 1.125rem;
padding: 1rem 3rem;
cursor: pointer;
white-space: nowrap;
border-radius: 0.3em;
outline: none;
border: none;
}
.btn_singleCard {
background-image: linear-gradient(
to right,
var(--gradient-cosmic-from),
var(--gradient-cosmic-to)
);
}
.btn_threeCard {
color: var(--accent-light-gold);
background-image: linear-gradient(
to right,
var(--gradient-cosmic-to),
var(--gradient-cosmic-from)
);
}
.btn:hover {
background-image: linear-gradient(to right, var(--gradient-cosmic-to));
}
.btn_reveal {
color: var(--text-dark);
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
cursor: pointer;
outline: none;
border: none;
background-color: var(--gradient-gold-to);
border-radius: 0.3rem;
margin: 1rem;
}
.btn_reveal:hover {
background-color: var(--gradient-gold-from);
}
.hidden {
display: none;
}
.card_container {
position: relative;
width: 10rem;
height: 15rem;
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
margin: 0 auto;
overflow: hidden;
border-radius: 0.5rem;
cursor: pointer;
border: 2px solid var(--accent-light-gold);
box-shadow: 0 0 7px var(--accent-light-gold);
img {
width: 100%;
}
}
.reversed-card {
transform: rotate(180deg);
transition: transform 0.4s ease;
}
.reversed-card:hover {
transform: rotate(0deg) translateY(-4%);
}
.card_front span:nth-child(2) {
font-size: 1.3rem;
}
.card_front span:nth-child(1) {
font-size: 1.8rem;
}
.card_back {
display: flex;
align-items: center;
width: 10rem;
height: 15rem;
border: 2px solid var(--gradient-cosmic-from);
border-radius: 0.5rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
}
.multiple_card {
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
flex-direction: column;
}
.card-shield span {
opacity: 10;
z-index: 3;
}
/* Small devices (phones in landscape, ≥ 600px) */
@media screen and (min-width: 600px) {
h1 {
font-size: 3rem;
}
.btn_wrapper {
flex-direction: row;
}
.fortune_description {
padding: 2rem;
width: 60%;
}
.multiple_card {
gap: 30px;
flex-direction: row;
}
}
/* Add to your CSS file (e.g., style.css) */
.img-loader {
width: 80px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.img-loader:after {
content: "";
display: block;
width: 40px;
height: 40px;
border: 4px solid #ccc;
border-top: 4px solid #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.card-img.hidden {
display: none;
}
```
```ts
interface Card {
name: string;
value: string | number;
name_short: string;
value_int: number;
suit: string;
type: string;
img: string;
meaning_up: string;
meaning_rev: string;
desc: string;
}
interface Deck {
cards: Card[];
}
const CDN_URL = "https://cdn.freecodecamp.org/curriculum/typescript/tarot-app";
const defaultImg = new URL("default.svg", CDN_URL + "/");
const LOCAL_DEFAULT_IMG = defaultImg;
const getElement = <T extends HTMLElement>(selector: string): T => {
const el = document.querySelector<T>(selector);
if (!el) throw new Error(`Element not found: ${selector}`);
return el;
};
const hideElements = (...elements: HTMLElement[]) =>
elements.forEach((el) => el.classList.add("hidden"));
const showElements = (...elements: HTMLElement[]) =>
elements.forEach((el) => el.classList.remove("hidden"));
const getRandomItem = <T,>(items: T[]): T => {
const index = Math.floor(Math.random() * items.length);
return items[index];
};
const renderCard = (drawingType: string, isReversed: boolean, shortName: string, img: string): string => `
<div>
<h2>${drawingType}</h2>
<figure class="card_container ${isReversed ? "reversed-card" : ""}" data-id="${shortName}">
<div class="img-loader"></div>
<img
src="${img ? `${CDN_URL}/${img}` : LOCAL_DEFAULT_IMG}"
class="card-img hidden"
onload="this.classList.remove('hidden');this.previousElementSibling.style.display='none';"
onerror="
if (!this.dataset.failed) {
this.dataset.failed = '1';
this.src='${LOCAL_DEFAULT_IMG}';
} else {
this.classList.remove('hidden');
this.previousElementSibling.style.display='none';
}
"
/>
</figure>
</div>
`;
enum DrawingType {
Past = "past",
Present = "present",
Future = "future",
}
class Game {
cards: Card[] = [];
private elements: {
singleCardBtn: HTMLElement;
singleCard: HTMLElement;
multipleCardsBtn: HTMLElement;
multipleCard: HTMLElement;
title: HTMLElement;
newReadingBtn: HTMLElement;
fortuneContainer: HTMLElement;
fortuneDescription: HTMLElement;
headerTitle: HTMLElement;
subTitle: HTMLElement;
cardTitle: HTMLElement;
description: HTMLElement;
text: HTMLElement;
}
constructor() {
this.elements = {
singleCardBtn: getElement("#btn-single-card"),
singleCard: getElement(".single_card"),
multipleCardsBtn: getElement("#btn-multiple-cards"),
multipleCard: getElement(".multiple_card"),
title: getElement(".title"),
newReadingBtn: getElement(".btn_reveal"),
fortuneContainer: getElement(".fortune_container"),
fortuneDescription: getElement(".fortune_description"),
headerTitle: getElement(".header_title"),
subTitle: getElement(".sub_title"),
cardTitle: getElement(".desc_title"),
description: getElement(".description"),
text: getElement(".text"),
}
this.fetchCardsData();
this.initializeEventListeners();
}
private async fetchCardsData() {
try {
const response = await fetch(`${CDN_URL}/card_data.json`);
const data: Deck = await response.json();
this.cards = data.cards;
} catch (error) {
console.error("Error fetching cards:", error);
}
}
private initializeEventListeners(): void {
this.elements.singleCardBtn.addEventListener("click", () =>
this.singleCardSelected(),
);
this.elements.multipleCardsBtn.addEventListener("click", () =>
this.multipleCardSelected(),
);
this.elements.newReadingBtn.addEventListener("click", () =>
this.newReading(),
);
document.addEventListener("click", (e: Event) => this.showFortune(e));
}
singleCardSelected() {
hideElements(
this.elements.singleCardBtn,
this.elements.multipleCardsBtn,
this.elements.multipleCard,
this.elements.text,
this.elements.headerTitle,
);
const isReversed = Math.random() < 0.5;
const chosenCard = getRandomItem(this.cards);
this.elements.singleCard.innerHTML = renderCard(
"Click the card and reveal the fortune",
isReversed,
chosenCard.name_short,
chosenCard.img,
);
this.elements.multipleCard.innerHTML = "";
showElements(this.elements.singleCard, this.elements.fortuneContainer);
}
multipleCardSelected() {
hideElements(
this.elements.singleCard,
this.elements.singleCardBtn,
this.elements.multipleCardsBtn,
this.elements.headerTitle,
);
showElements(
this.elements.multipleCard,
this.elements.fortuneContainer,
this.elements.text,
);
this.elements.multipleCard.innerHTML = Object.values(DrawingType)
.map((type) => {
const isReversed = Math.random() < 0.5;
const card = getRandomItem(this.cards);
return renderCard(type, isReversed, card.name_short, card.img);
})
.join("");
}
showFortune(e: Event) {
const target = e.target
if (!(target instanceof HTMLElement)) {
return;
}
const cardElement = target?.closest(".card_container")
if (!(cardElement instanceof HTMLElement)) {
return;
}
if (!cardElement) {
return;
}
const cardId = cardElement.getAttribute("data-id");
const foundCard = this.cards.find((card) => card.name_short === cardId);
if (foundCard) {
this.elements.cardTitle.textContent = foundCard.name;
this.elements.description.textContent = foundCard.desc;
this.elements.subTitle.textContent = foundCard.meaning_up;
this.elements.title.textContent = foundCard.name;
showElements(this.elements.fortuneDescription);
}
}
newReading() {
showElements(
--fcc-editable-region--
--fcc-editable-region--
);
hideElements();
}
}
const game = new Game();
```
@@ -57,9 +57,7 @@
{ "id": "68f1e955fdb3c88261c530b6", "title": "Step 50" },
{ "id": "68f1e9e9e745c787c19031f2", "title": "Step 51" },
{ "id": "68f1ea50270f838c35fe4cda", "title": "Step 52" },
{ "id": "68f1eb2bb413b893f8661ff2", "title": "Step 53" },
{ "id": "68f1ebc05a602f99d49d701d", "title": "Step 54" },
{ "id": "68f1ec48c0e7969eddbef24c", "title": "Step 55" }
{ "id": "68f1eb2bb413b893f8661ff2", "title": "Step 53" }
],
"blockLabel": "workshop",
"usesMultifileEditor": true