Visual English Flashcards
Click the card to flip, see the word and hear its pronunciation. Use I Know This to fill stars. After 10 stars a word becomes
Card: 1/0
👆Click
const STORAGE_KEYS = { progress: 'fc_progress_v1', learned: 'fc_learned_v1' };
let progressCounts = JSON.parse(localStorage.getItem(STORAGE_KEYS.progress) || '{}'); // {cat: {Word: n}}
let learnedMeta = JSON.parse(localStorage.getItem(STORAGE_KEYS.learned) || '{}'); // {"cat::Word": {word,category}}
function saveState(){
localStorage.setItem(STORAGE_KEYS.progress, JSON.stringify(progressCounts));
localStorage.setItem(STORAGE_KEYS.learned, JSON.stringify(learnedMeta));
}
// ---------- State ----------
let manifest = null;
let category = 'animals';
let words = []; // [{word, image, category}]
let currentIndex = 0;
let repeatMode = false;
let repeatIndex = 0;
// ---------- DOM ----------
const flashcard = document.getElementById('flashcard');
const cardImage = document.getElementById('cardImage');
const cardBackImage = document.getElementById('cardBackImage');
const wordDisplay = document.getElementById('wordDisplay');
const currentElement = document.getElementById('current');
const totalElement = document.getElementById('total');
const progressBar = document.getElementById('progressBar');
const nextBtn = document.getElementById('nextBtn');
const knowBtn = document.getElementById('knowBtn');
const repeatBtn = document.getElementById('repeatBtn');
const learnedList = document.getElementById('learnedList');
const categorySelect = document.getElementById('categorySelect');
const learnedCountEl = document.getElementById('learnedCount');
const totalCountEl = document.getElementById('totalCount');
const starsEl = document.getElementById('stars');
// ---------- Utils ----------
function toTitle(str){
return str.replace(/[_-]+/g,' ').replace(/\s+/g,' ').trim().replace(/\b\w/g, c=>c.toUpperCase());
}
function learntKey(cat, word){ return `${cat}::${word}`; }
function getProgress(cat, word){ return (progressCounts[cat] && progressCounts[cat][word]) || 0; }
function setProgress(cat, word, value){ if(!progressCounts[cat]) progressCounts[cat] = {}; progressCounts[cat][word] = value; saveState(); }
async function imgExists(url){
return new Promise(resolve => { const i = new Image(); i.onload=()=>resolve(true); i.onerror=()=>resolve(false); i.src = url + '?_=' + Date.now(); });
}
async function resolveImagePath(cat, stem){
const base = `images/${cat}/`;
const s = stem.toLowerCase().replace(/\s+/g,'_');
const exts = ['jpg','jpeg','png','webp'];
for(const ext of exts){ const url = `${base}${s}.${ext}`; if(await imgExists(url)) return url; }
return `${base}${s}.jpg`; // best guess
}
async function loadManifest(){
try{
const res = await fetch(MANIFEST_URL, {cache:'no-store'});
if(!res.ok) return null;
return await res.json();
}catch(e){ return null; }
}
async function listCategoryDirectory(cat){
// Try to parse directory listing if server allows
try{
const res = await fetch(`images/${cat}/`, {cache:'no-store'});
if(!res.ok) return [];
const html = await res.text();
const files = [];
const rx = /href\s*=\s*"([^"?]+)"/gi; let m;
const allowed = ['.jpg','.jpeg','.png','.webp'];
while((m = rx.exec(html))){
const href = m[1];
const name = href.split('/').pop();
if(allowed.some(ext => name.toLowerCase().endsWith(ext))) files.push(name);
}
return files;
} catch(e){ return []; }
}
function populateCategorySelect(categories){
categorySelect.innerHTML = '';
categories.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat; opt.textContent = toTitle(cat);
categorySelect.appendChild(opt);
});
if(categories.includes('animals')) categorySelect.value = 'animals';
category = categorySelect.value;
}
// ---------- Stars UI ----------
function renderStars(word){
starsEl.innerHTML = '';
const count = getProgress(category, word) || 0;
for(let i=1;i<=10;i++){
const s = document.createElement('span');
s.className = 'star' + (i <= count ? ' filled' : '');
s.textContent = '★';
starsEl.appendChild(s);
}
}
// ---------- Learned UI ----------
function getLearnedWordsInCurrentCategory(){
return Object.keys(learnedMeta)
.filter(k => k.startsWith(category + '::'))
.map(k => learnedMeta[k].word);
}
function updateLearnedUI(popWord=null){
// Badge shows learned count for current category
const learnedInCurrent = getLearnedWordsInCurrentCategory();
learnedCountEl.textContent = learnedInCurrent.length;
totalCountEl.textContent = words.length;
// Learned chips (all categories)
learnedList.innerHTML = '';
Object.keys(learnedMeta).forEach(key => {
const {word, category:cat} = learnedMeta[key];
const li = document.createElement('li');
const chip = document.createElement('span');
chip.className = `chip ${['animals','fruits','household','food'].includes(cat) ? cat : 'default'}`;
chip.innerHTML = `