// =============================================================================
// 1. DATA LOADER
// =============================================================================
acidBaseData = {
// Define the path to your new JSON file.
// IMPORTANT: You will need to create this file and adjust the path.
const jsonFilePath = "../files/json/acids-bases.json";
const response = await fetch(jsonFilePath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} - Could not load ${jsonFilePath}`);
}
return await response.json();
}
Acids and Bases
// =============================================================================
// 2. HELPER FUNCTION
// =============================================================================
function shuffle(array) {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
// =============================================================================
// 3. CSS STYLING
// =============================================================================
html`
<style>
.acid-base-widget {
max-width: 600px; margin: 2rem auto; padding: 1.5rem 2rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fdfdfd; border-radius: 12px;
border: 1px solid #e2e8f0;
}
.ab-card-front {
background: white; border-radius: 8px; border: 1px solid #e2e8f0;
padding: 2rem; text-align: center; min-height: 120px;
display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 1rem;
}
.ab-substance { font-size: 2.5rem; font-weight: 600; color: #1e293b; }
.ab-question { font-size: 1.25rem; color: #475569; font-weight: 500; }
.ab-choices {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 1.5rem;
}
.ab-choices button {
font-size: 1rem; font-weight: 600; padding: 0.75rem 1rem;
border-radius: 8px; border: 2px solid; cursor: pointer; transition: all 0.2s ease;
}
.ab-btn-strong-acid { background-color: #fef2f2; border-color: #f87171; color: #b91c1c; }
.ab-btn-strong-acid:hover { background-color: #fee2e2; }
.ab-btn-weak-acid { background-color: #fff7ed; border-color: #fb923c; color: #9a3412; }
.ab-btn-weak-acid:hover { background-color: #fed7aa; }
.ab-btn-strong-base { background-color: #eff6ff; border-color: #60a5fa; color: #1d4ed8; }
.ab-btn-strong-base:hover { background-color: #dbeafe; }
.ab-btn-weak-base { background-color: #f0f9ff; border-color: #7dd3fc; color: #0369a1; }
.ab-btn-weak-base:hover { background-color: #e0f2fe; }
.ab-card-back {
background-color: #f7f9fc; border: 1px solid #dbe3f0; border-radius: 8px;
opacity: 0; max-height: 0; overflow: hidden; transition: all 0.5s ease-in-out;
margin-top: 1.5rem;
}
.ab-card-back.visible { opacity: 1; max-height: 500px; padding: 1.5rem; }
.feedback-header { display: flex; align-items: center; gap: 0.75rem; font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; }
.feedback-correct { color: #047857; }
.feedback-incorrect { color: #b91c1c; }
.feedback-icon { font-size: 1.8rem; }
.feedback-answer-text { font-size: 1.1rem; margin-bottom: 1rem; line-height: 1.6; }
.feedback-note-label { font-weight: 600; color: #475569; margin-bottom: 0.5rem; }
.feedback-note-text { background: white; padding: 1rem; border-radius: 6px; border: 1px solid #e2e8f0; }
.ab-controls { display: flex; justify-content: center; margin-top: 1.5rem; border-top: 1px solid #e2e8f0; padding-top: 1.5rem; }
.ab-btn-next {
font-size: 1rem; font-weight: 600; padding: 0.75rem 1.5rem;
border-radius: 8px; border: 2px solid #c7d2fe; cursor: pointer; transition: all 0.2s ease;
background: #eef2ff; color: #4338ca;
}
.ab-btn-next:hover { background-color: #e0e7ff; }
</style>
`
// =============================================================================
// 4. REACTIVE STATE
// =============================================================================
mutable appState = ({
deck: [], // Start with an empty deck
index: 0,
quizMode: 'formula', // 'formula' or 'name'
userAnswer: null, // null, 'correct', or 'incorrect'
showAnswer: false
});
// =============================================================================
// 5. DATA INITIALIZER (Corrected to Filter Comments)
// =============================================================================
{
// This line makes this cell reactively dependent on `acidBaseData`.
// It will only run AFTER the fetch is complete.
acidBaseData;
// This logic now runs only when the data is ready and the deck is still empty.
if (appState.deck.length === 0 && acidBaseData && acidBaseData.length > 0) {
// We filter the array to include only objects that have a `formulaHTML` property.
// This removes all the `{ "comment": "..." }` objects.
const filteredDeck = acidBaseData.filter(card => card.formulaHTML);
// Now, we shuffle the CLEAN, filtered deck.
mutable appState = { ...appState, deck: shuffle(filteredDeck) };
}
// This cell produces no visible output.
return html``;
}
// =============================================================================
// 6. MAIN APPLICATION RENDERER
// =============================================================================
{
if (appState.deck.length === 0) {
return html`<div class="acid-base-widget"><div class="ab-card-front">Loading cards...</div></div>`;
}
const currentCard = appState.deck[appState.index];
// --- Event Handlers ---
const handleAnswer = (type, strength) => {
const isCorrect = (type === currentCard.type && strength === currentCard.strength);
mutable appState = {
...appState,
userAnswer: isCorrect ? 'correct' : 'incorrect',
showAnswer: true
};
};
const handleNextCard = () => {
const nextIndex = (appState.index + 1) % appState.deck.length;
mutable appState = {
...appState,
index: nextIndex,
userAnswer: null,
showAnswer: false
};
};
// --- Create the main HTML node ---
const widgetNode = html`
<div class="acid-base-widget">
<div class="ab-card-front">
<div class="ab-substance">${appState.quizMode === 'formula' ? currentCard.formulaHTML : currentCard.name}</div>
<div class="ab-question">How would you classify this substance?</div>
</div>
<div class="ab-choices" style="display: ${appState.showAnswer ? 'none' : 'grid'};">
<button class="ab-btn-strong-acid" data-type="acid" data-strength="strong">Strong Acid</button>
<button class="ab-btn-strong-base" data-type="base" data-strength="strong">Strong Base</button>
<button class="ab-btn-weak-acid" data-type="acid" data-strength="weak">Weak Acid</button>
<button class="ab-btn-weak-base" data-type="base" data-strength="weak">Weak Base</button>
</div>
<div class="ab-card-back ${appState.showAnswer ? 'visible' : ''}">
${appState.userAnswer === 'correct' ? html`
<div class="feedback-header feedback-correct"><span class="feedback-icon">✓</span> Correct!</div>` : ''}
${appState.userAnswer === 'incorrect' ? html`
<div class="feedback-header feedback-incorrect"><span class="feedback-icon">✗</span> Not quite.</div>` : ''}
<div class="feedback-answer-text">
<strong>${currentCard.name} (${currentCard.formulaHTML})</strong> is a <strong>${currentCard.strength} ${currentCard.type}</strong>.
</div>
<div class="feedback-note-label">Note:</div>
<div class="feedback-note-text">${currentCard.notes}</div>
</div>
<div class="ab-controls" style="display: ${appState.showAnswer ? 'flex' : 'none'};">
<button class="ab-btn-next" data-action="next">Next Card</button>
</div>
</div>
`;
// --- Attach all event listeners ---
widgetNode.querySelectorAll('.ab-choices button').forEach(btn => {
btn.addEventListener('click', () => handleAnswer(btn.dataset.type, btn.dataset.strength));
});
const nextBtn = widgetNode.querySelector('[data-action="next"]');
if (nextBtn) {
nextBtn.addEventListener('click', handleNextCard);
}
return widgetNode;
}