// =============================================================================
// 1. DATA LOADER
// =============================================================================
solubilityData = {
const jsonFilePath = "/files/json/solubility-rules.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();
}
Solubility Rules
// =============================================================================
// 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>
.solubility-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;
}
.solubility-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;
}
.solubility-formula { font-size: 2.5rem; font-weight: 600; color: #1e2sxb; }
.solubility-question { font-size: 1.25rem; color: #475569; font-weight: 500; }
.solubility-choices { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
.solubility-choices button {
font-size: 1.1rem; font-weight: 600; padding: 0.75rem 2rem;
border-radius: 8px; border: 2px solid transparent; cursor: pointer; transition: all 0.2s ease;
}
.sol-btn-soluble { background-color: #e0f2fe; border-color: #38bdf8; color: #0369a1; }
.sol-btn-soluble:hover { background-color: #bae6fd; }
.sol-btn-insoluble { background-color: #fff7ed; border-color: #fb923c; color: #9a3412; }
.sol-btn-insoluble:hover { background-color: #fed7aa; }
.solubility-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;
}
.solubility-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; }
.feedback-rule-label { font-weight: 600; color: #475569; margin-bottom: 0.5rem; }
.feedback-rule-text { background: white; padding: 1rem; border-radius: 6px; border: 1px solid #e2e8f0; }
.solubility-controls { display: flex; justify-content: center; margin-top: 1.5rem; border-top: 1px solid #e2e8f0; padding-top: 1.5rem; }
.sol-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;
}
.sol-btn-next:hover {
background-color: #e0e7ff;
}
</style>
`
// =============================================================================
// 4. REACTIVE STATE
// =============================================================================
mutable appState = ({
deck: [], // Start with an empty deck
index: 0,
userAnswer: null, // null, 'correct', or 'incorrect'
showAnswer: false
});
// =============================================================================
// 5. DATA INITIALIZER
// This cell waits for the data to load, filters out non-card objects,
// and then populates the deck ONE TIME.
// =============================================================================
{
// This line makes this cell reactively dependent on `solubilityData`.
// It will only run AFTER the fetch is complete.
solubilityData;
// This logic now runs only when the data is ready and the deck is still empty.
if (appState.deck.length === 0 && solubilityData && solubilityData.length > 0) {
// THIS IS THE FIX:
// We filter the array to include only objects that have a `formula` property.
// This removes all the `{ "comment": "..." }` objects.
const filteredDeck = solubilityData.filter(card => card.formula);
// 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 (Updated)
// =============================================================================
{
// Guard clause to handle the initial loading state.
if (appState.deck.length === 0) {
return html`<div class="solubility-widget"><div class="solubility-card-front">Loading cards...</div></div>`;
}
const currentCard = appState.deck[appState.index];
// --- Event Handlers ---
const handleAnswer = (userChoice) => {
const isCorrect = userChoice === currentCard.soluble;
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="solubility-widget">
<div class="solubility-card-front">
<div class="solubility-formula">${currentCard.formula}</div>
<div class="solubility-question">Is this compound soluble in water?</div>
</div>
<div class="solubility-choices" style="display: ${appState.showAnswer ? 'none' : 'flex'};">
<button class="sol-btn-soluble" data-choice="true">Soluble</button>
<button class="sol-btn-insoluble" data-choice="false">Insoluble</button>
</div>
<div class="solubility-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.formula})</strong> is <strong>${currentCard.soluble ? 'soluble' : 'insoluble'}</strong>.
</div>
<!-- THIS IS THE MODIFIED PART -->
<div class="feedback-rule-text">
<strong>Rule:</strong> ${currentCard.primary_rule}<br>
${currentCard.exception ? `<strong>Exception:</strong> ${currentCard.exception}`: ''}
</div>
</div>
<div class="solubility-controls" style="display: ${appState.showAnswer ? 'flex' : 'none'};">
<button class="sol-btn-next" data-action="next">Next Card</button>
</div>
</div>
`;
// --- Attach all event listeners ---
const choiceButtons = widgetNode.querySelectorAll('.solubility-choices button');
if (choiceButtons.length > 0) {
widgetNode.querySelector('[data-choice="true"]').addEventListener('click', () => handleAnswer(true));
widgetNode.querySelector('[data-choice="false"]').addEventListener('click', () => handleAnswer(false));
}
const nextBtn = widgetNode.querySelector('[data-action="next"]');
if (nextBtn) {
nextBtn.addEventListener('click', handleNextCard);
}
return widgetNode;
}