mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-01 15:02:09 +08:00
fix(curriculum): merge fortune teller final steps (#69582)
This commit is contained in:
+628
-5
@@ -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();
|
||||
```
|
||||
|
||||
-608
@@ -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();
|
||||
```
|
||||
-1197
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user