// =============================================================================
// 1. DATA LOADER
// =============================================================================
flashcardData = {
// Define the path to your JSON file.
const jsonFilePath = "/files/json/nomenclature.json";
// Fetch the data and handle potential errors.
const response = await fetch(jsonFilePath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} - Could not load ${jsonFilePath}`);
}
return await response.json();
}
Nomenclature
// =============================================================================
// 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;
}
// =============================================================================
// 2. CSS STYLING (With Improved Box Shadow)
// =============================================================================
html`
<style>
.flashcard-widget {
max-width: 800px;
margin: 2rem auto;
padding: 1.5rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
border-radius: 16px;
border: 1px solid #e2e8f0;
}
/* ... the rest of the CSS remains exactly the same ... */
.setup-screen {
display: flex;
flex-direction: column;
gap: 2rem;
}
.setup-section {
background: white;
padding: 1.5rem;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.setup-section h3 {
margin: 0 0 1.5rem 0;
color: #1e293b;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.study-mode-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.study-mode-option {
background: #ffffff;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
position: relative;
}
.study-mode-option:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.15);
}
.study-mode-option.selected {
border-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.2);
}
.study-mode-title {
font-weight: 600;
font-size: 1.1rem;
color: #1e293b;
margin-bottom: 0.5rem;
}
.study-mode-option.selected .study-mode-title {
color: #1d4ed8;
}
.study-mode-desc {
font-size: 0.9rem;
color: #64748b;
line-height: 1.4;
}
.study-mode-option.selected .study-mode-desc {
color: #3730a3;
}
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.category-option {
background: #ffffff;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
position: relative;
}
.category-option:hover {
border-color: #10b981;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.15);
}
.category-option.selected {
border-color: #10b981;
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.2);
}
.category-title {
font-weight: 600;
font-size: 1.1rem;
color: #1e293b;
margin-bottom: 0.5rem;
}
.category-option.selected .category-title {
color: #047857;
}
.category-desc {
font-size: 0.9rem;
color: #64748b;
line-height: 1.4;
}
.category-option.selected .category-desc {
color: #065f46;
}
.flashcard-screen {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.progress-bar {
background: #f1f5f9;
border-radius: 20px;
padding: 0.75rem 1.5rem;
text-align: center;
color: #475569;
font-weight: 500;
border: 1px solid #e2e8f0;
}
.flashcard {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.flashcard-question {
font-size: 2.5rem;
font-weight: 600;
color: #1e293b;
line-height: 1.3;
}
.flashcard-answer {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 2px solid #0ea5e9;
border-radius: 12px;
padding: 0;
overflow: hidden;
opacity: 0;
max-height: 0;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.flashcard-answer.visible {
opacity: 1;
max-height: 1200px;
padding: 2rem;
}
.answer-section {
margin-bottom: 1.5rem;
}
.answer-section:last-child {
margin-bottom: 0;
}
.answer-label {
font-weight: 600;
color: #0c4a6e;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.answer-content {
color: #1e293b;
line-height: 1.6;
font-size: 1rem;
}
.answer-primary {
font-size: 1.75rem;
font-weight: 600;
color: #0369a1;
text-align: center;
}
.answer-details-separator {
border: 0;
height: 1px;
background: #bae6fd;
margin: 1.5rem 0;
}
.button-group {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}
.btn {
padding: 0.875rem 1.75rem;
font-size: 1rem;
font-weight: 600;
border: 2px solid transparent;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
color: #4338ca;
border-color: #c7d2fe;
}
.btn:hover {
background: linear-gradient(135deg, #eef2ff 0%, #f8fafc 100%);
border-color: #a5b4fc;
transform: translateY(-1px);
}
.btn.primary {
background: linear-gradient(135deg, #4338ca 0%, #3730a3 100%);
color: white;
border-color: #4338ca;
box-shadow: 0 4px 12px rgba(67, 56, 202, 0.3);
}
.btn.primary:hover {
background: linear-gradient(135deg, #3730a3 0%, #312e81 100%);
border-color: #3730a3;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(67, 56, 202, 0.4);
}
.error-message {
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
color: #dc2626;
border: 2px solid #f87171;
border-radius: 10px;
text-align: center;
font-weight: 500;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.flashcard-widget { margin: 1rem; padding: 1rem; }
.flashcard-widget .flashcard-question { font-size: 1.8rem; }
.flashcard-widget .study-mode-grid, .flashcard-widget .category-grid { grid-template-columns: 1fr; }
.flashcard-widget .button-group { flex-direction: column; align-items: center; }
.flashcard-widget .btn { width: 100%; max-width: 300px; }
}
@media (max-width: 480px) {
.flashcard-widget .flashcard { padding: 2rem 1rem; }
.flashcard-widget .flashcard-question { font-size: 1.5rem; }
}
</style>
`
// =============================================================================
// 3. REACTIVE STATE
// =============================================================================
mutable appState = ({
screen: 'setup', // 'setup' or 'study'
studyMode: 'formula_to_name', // 'formula_to_name', 'name_to_formula', 'mixed'
selectedCategories: ['Ions and Ionic Compounds', 'Covalent Compounds'],
deck: [],
currentIndex: 0,
showAnswer: false,
cardsStudied: 0,
error: null
})
// =============================================================================
// 4. MAIN APPLICATION RENDERER
// =============================================================================
{
// --- Define Event Handler Functions ---
const handleModeSelect = (mode) => {
mutable appState = {...appState, studyMode: mode};
};
const handleCategoryToggle = (category) => {
const cats = appState.selectedCategories;
const newCats = cats.includes(category)
? cats.filter(c => c !== category)
: [...cats, category];
mutable appState = {...appState, selectedCategories: newCats, error: null};
};
const handleStartSession = () => {
if (appState.selectedCategories.length === 0) {
mutable appState = {...appState, error: 'Please select at least one category to study.'};
return;
}
const filteredData = flashcardData.filter(card =>
appState.selectedCategories.includes(card.category)
);
if (filteredData.length === 0) {
mutable appState = {...appState, error: 'No cards available for selected categories.'};
return;
}
const newDeck = shuffle(filteredData).map(card => {
const cardName = card.common_name || card.IUPAC_name;
const cardFormula = html`${card.formula}`;
let question, answer;
const mode = appState.studyMode;
if (mode === 'formula_to_name') {
question = cardFormula;
answer = cardName;
} else if (mode === 'name_to_formula') {
question = cardName;
answer = cardFormula;
} else {
if (Math.random() < 0.5) {
question = cardFormula;
answer = cardName;
} else {
question = cardName;
answer = cardFormula;
}
}
return { question, answer, details: card };
});
mutable appState = {
...appState, screen: 'study', deck: newDeck,
currentIndex: 0, showAnswer: false, cardsStudied: 0, error: null
};
};
const handleShowAnswer = () => {
mutable appState = {...appState, showAnswer: true};
};
const handleNextCard = () => {
const totalCards = appState.deck.length;
const newCardsStudied = appState.cardsStudied + 1;
const nextIndex = (appState.currentIndex + 1) % totalCards;
mutable appState = {
...appState, currentIndex: nextIndex, showAnswer: false,
cardsStudied: (nextIndex === 0 && appState.currentIndex === totalCards - 1) ? totalCards : newCardsStudied
};
};
const handleEndSession = () => {
mutable appState = {
...appState, screen: 'setup', deck: [], currentIndex: 0,
showAnswer: false, cardsStudied: 0, error: null
};
};
// --- A. Render the Setup Screen ---
if (appState.screen === 'setup') {
const setupNode = html`
<div class="flashcard-widget">
<div class="setup-screen">
${appState.error ? html`<div class="error-message">${appState.error}</div>` : ''}
<div class="setup-section">
<h3>๐งช Study Mode</h3>
<div class="study-mode-grid">
<div class="study-mode-option ${appState.studyMode === 'formula_to_name' ? 'selected' : ''}" data-mode="formula_to_name">
<div class="study-mode-title">Formula โ Name</div>
<div class="study-mode-desc">See chemical formulas, provide the names</div>
</div>
<div class="study-mode-option ${appState.studyMode === 'name_to_formula' ? 'selected' : ''}" data-mode="name_to_formula">
<div class="study-mode-title">Name โ Formula</div>
<div class="study-mode-desc">See chemical names, provide the formulas</div>
</div>
<div class="study-mode-option ${appState.studyMode === 'mixed' ? 'selected' : ''}" data-mode="mixed">
<div class="study-mode-title">Mixed Mode</div>
<div class="study-mode-desc">Random combination of both directions</div>
</div>
</div>
</div>
<div class="setup-section">
<h3>๐ Categories to Study</h3>
<div class="category-grid">
<div class="category-option ${appState.selectedCategories.includes('Ions and Ionic Compounds') ? 'selected' : ''}" data-category="Ions and Ionic Compounds">
<div class="category-title">Ions and Ionic Compounds</div>
<div class="category-desc">Monatomic ions, polyatomic ions, and ionic compounds</div>
</div>
<div class="category-option ${appState.selectedCategories.includes('Covalent Compounds') ? 'selected' : ''}" data-category="Covalent Compounds">
<div class="category-title">Covalent Compounds</div>
<div class="category-desc">Binary compounds, acids, and simple organic compounds</div>
</div>
</div>
</div>
<div class="button-group">
<button class="btn primary" data-action="start">Start Studying</button>
</div>
</div>
</div>
`;
setupNode.querySelectorAll('[data-mode]').forEach(el => el.addEventListener('click', () => handleModeSelect(el.dataset.mode)));
setupNode.querySelectorAll('[data-category]').forEach(el => el.addEventListener('click', () => handleCategoryToggle(el.dataset.category)));
setupNode.querySelector('[data-action="start"]').addEventListener('click', handleStartSession);
return setupNode;
}
// --- B. Render the Study Screen ---
const currentFlashcard = appState.deck[appState.currentIndex];
const { question, answer, details: currentCard } = currentFlashcard;
const totalCards = appState.deck.length;
const cardName = currentCard.common_name || currentCard.IUPAC_name;
const studyNode = html`
<div class="flashcard-widget">
<div class="flashcard-screen">
<div class="progress-bar">Card ${appState.currentIndex + 1} of ${totalCards}</div>
<div class="flashcard">
<div class="flashcard-question">${question}</div>
</div>
<div class="flashcard-answer ${appState.showAnswer ? 'visible' : ''}">
<div class="answer-primary">${answer}</div>
<hr class="answer-details-separator">
${currentCard.common_name && currentCard.IUPAC_name && currentCard.common_name !== currentCard.IUPAC_name ? html`
<div class="answer-section">
<div class="answer-label">${cardName === currentCard.common_name ? "IUPAC Name:" : "Common Name:"}</div>
<div class="answer-content">${cardName === currentCard.common_name ? currentCard.IUPAC_name : currentCard.common_name}</div>
</div>
` : ''}
<div class="answer-section">
<div class="answer-label">Category:</div>
<div class="answer-content">${currentCard.subcategory}</div>
</div>
${currentCard.components ? html`<div class="answer-section"><div class="answer-label">Components:</div><div class="answer-content">${html`${currentCard.components.join(' + ')}`}</div></div>` : ''}
${currentCard.charge_balance ? html`<div class="answer-section"><div class="answer-label">Charge Balance:</div><div class="answer-content">${currentCard.charge_balance}</div></div>` : ''}
${currentCard.notes ? html`<div class="answer-section"><div class="answer-label">Notes:</div><div class="answer-content">${html`${currentCard.notes}`}</div></div>` : ''}
</div>
<div class="button-group">
${!appState.showAnswer ? `<button class="btn primary" data-action="show">Show Answer</button>` : ''}
${appState.showAnswer ? `<button class="btn primary" data-action="next">Next Card</button>` : ''}
<button class="btn" data-action="end">End Session</button>
</div>
</div>
</div>
`;
const showBtn = studyNode.querySelector('[data-action="show"]');
if (showBtn) showBtn.addEventListener('click', handleShowAnswer);
const nextBtn = studyNode.querySelector('[data-action="next"]');
if (nextBtn) nextBtn.addEventListener('click', handleNextCard);
studyNode.querySelector('[data-action="end"]').addEventListener('click', handleEndSession);
return studyNode;
}