跪拜 Guibai
← Back to the summary

A Dad Built a Flashcard App to Replace the Chaos of After-School English Review

Background

Before my daughter started kindergarten, I told my wife very firmly: no academic pressure during kindergarten, just happy education. It turned out that saying it was easy. When she actually started school and we saw other classmates signing up, I couldn't resist and enrolled her in an English class with a foreign teacher.

The class itself wasn't a problem. What really gave me a headache was the after-class review. After each class, the teacher would send the day's learning content in the group chat, including vocabulary, sentence patterns, letter exercises, class videos, and homework. For example:


📆【Today's Learning Content】 LG1
————————————
👩‍🏫Teaching Objectives
I. Review Student book—《Classroom》
1. Key Vocabulary + Sentence Patterns
window 🪟
door 🚪
desk 桌子
chair 🪑
book 📖

II. Learn 《Super ABC》P12 Letter Cc
❤️Learn Letter Cc exercise
❤️Learn words starting with letter c
1️⃣cat 猫🐱
2️⃣cap 帽子🧢
3️⃣cake 蛋糕🍰
4️⃣car 汽车🚗
❤️Related sentence pattern learning

III. Pre-class Oral Dialogue

IV. Writing Practice
———————————— 
🌞【Homework】:
1️⃣Oral homework💬
Read aloud the words starting with letter c in 《Super ABC》P12
————————————
[Sun]【Today's Class Summary】
The kids were very active in class today 👏 Please listen to the audio more at home to practice! ❤️Please make sure all kids complete their homework 💕💕   

Or:


📆【Today's Learning Content】 
————————————
👩‍🏫Teaching Objectives
1. Free Talk: Pre-class Q&A

2. Review content learned for Letter A

3. ❤️Learn content related to Letter B
— Learn Letter B exercise
— Initial recognition of the shape of Letter B
— Learn key words starting with letter b
1️⃣bee 蜜蜂🐝
2️⃣ball 球
3️⃣balloon 气球🎈
4️⃣bear 熊🐻

Extended vocabulary: box 📦  bike 🚴
— Sentence pattern learning: B is for __
— Sentence pattern comprehension input:
1. I see a bee.
2. I have a ball.
3. The balloon is big.
4. The bear is brown.

4. Writing Practice
———————————— 
🌞【Homework】:
Oral homework 💬
Listen to the audio and read aloud 《Super ABC book》P8
————————————
[Sun]【Today's Class Summary】
The kids were very active in class today 👏 Please listen to the audio more at home to practice! ❤️Please make sure all kids complete their homework 💕💕    

The teacher's notes were actually quite detailed, but they were more like a "class record" rather than a product suitable for a 4-year-old to practice with repeatedly.

My initial review method was very basic: find the video the teacher sent, open it, drag it back to the part that needed reviewing, play it, pause, repeat after it, and continue.

It was fine once or twice, but after doing it many times, I found it very inefficient.

A child's attention span is short to begin with. While the parent is still scrolling through the group chat looking for the video and finding the progress bar, the child has already run off to play something else. Sometimes, just wanting to review the four words cat, cap, cake, car meant replaying the entire class video.

Later, I tried using apps like Oulu Dictionary or Baicizhan to input new words, hoping they would auto-play them in a loop.

Baicizhan's custom word bank had too much unfocused content during learning.

Oulu Dictionary either only had text, or if it had videos, the content was too complex.

These apps are all great on their own, but in my scenario, they always fell a bit short:

Ultimately, what I needed wasn't a massive English curriculum.

I just wanted to organize the few words and sentences the teacher taught that day into a set of review cards that could be seen, heard, and auto-played.

This need was so small that in the past, I probably wouldn't have written an app specifically for it.

But the AI era brings a very interesting change: previously, this kind of need that "only serves one family" had too low a return on investment; now I can use AI to quickly turn an idea into a prototype, and then use my frontend experience to slowly refine it into a truly handy tool.

So, after previously making an English learning app for myself, I made a second version.

This time, it wasn't written for myself, but for my daughter.

Requirements

I didn't start by wanting to build check-ins, points, leaderboards, or a children's version of Baicizhan.

I first minimized the requirements.

Although the content the teacher sent each time was long, the core that truly needed repeated review was only two types:

Words + Sentences

Content like "Teaching Objectives," "Class Summary," "Writing Practice," and "Please make sure all kids complete their homework" is useful for parents but shouldn't appear on a child's review cards.

Therefore, the core workflow of this app was organized by me as:

The teacher sends the class content
        ↓
Extract English words and sentences
        ↓
Supplement Chinese, Emoji, or images
        ↓
Play and read aloud card by card
        ↓
Record the day's review progress

On top of this core workflow, I added a few requirements that were unavoidable in actual use.

1. Content must be organized by class date

The child learns different content in each class. Today is Letter B, the next might be Letter C, and the one after that might switch to fruits.

So the content can't be mixed into an ever-growing word list; it needs to create "review days" by date. After switching dates, continue from the last review progress.

2. One card does one thing

Each card only displays one letter, word, or sentence, keeping the English, Chinese, picture, and pronunciation.

The child doesn't need to understand complex operations; they just look at the current content, listen to the pronunciation, and tap "next."

3. It should play as soon as it opens, and ideally auto-play

During review, parents shouldn't have to click play for every single word.

The page automatically reads aloud when switching to a new card, and can also enable learning mode: read the current content, pause for a few seconds, then automatically jump to the next card.

4. Images should be simple, intuitive, and replaceable

For a young child, bear corresponding to a picture of a bear is much more effective than a long Chinese explanation.

The system's built-in Emoji is the most effortless solution: no downloading, no storage needed, and fast loading. cat uses 🐱, bee uses 🐝, chair uses 🪑; most common words can be found directly.

But Emoji can't cover everything. Some words have no corresponding icon, and some icons aren't accurate enough, so it also needs to support online image search and local uploads.

5. Different devices at home should see the same content

If the data is only stored in the browser's localStorage, it's gone when switching to another phone. Content recorded by dad can't be seen on mom's phone.

Therefore, it ultimately couldn't just be a purely static page; it also needed a very lightweight cloud storage.

Implementation

The tech stack wasn't deliberately novel; it's still the set I'm most familiar with:

Vue 3
Vite
TypeScript
Element Plus
Supabase
GitHub Pages

For this kind of personal tool, the most important thing in tech selection isn't "advanced," but whether I can modify it quickly and dare to keep using it after modifications.

1. Separating the teacher's original text from the generated content

When adding a review day, I kept two copies of the content:

This step is very important.

If the entire group message was directly used for rendering, things like LG1, Super ABC, teaching objectives, and homework would all be stuffed into the question bank. Rather than pursuing a "100% understanding of all teacher copy" universal parser, it's better to let AI first organize the core content, and then give parents an input box for manual confirmation.

For example, for the Letter B class, the content finally entering the app could be simplified to:

Words
bee 蜜蜂🐝
ball 球
balloon 气球🎈
bear 熊🐻
box 盒子📦
bike 自行车🚲

Sentences
B is for __
I see a bee.
I have a ball.
The balloon is big.
The bear is brown.

The app parses line by line, automatically determines if it's a word or a sentence, and removes duplicates:

export function parseReviewText(rawText: string) {
  let currentCategory: ReviewItemCategory | undefined;

  const parsed = rawText.split(/\r?\n/).flatMap((line) => {
    const heading = cleanLine(line).replace(/[::]$/g, "").trim();

    if (/^(words?|单词)$/i.test(heading)) {
      currentCategory = "word";
      return [];
    }

    if (/^(sentences?|句子)$/i.test(heading)) {
      currentCategory = "sentence";
      return [];
    }

    return parseReviewLine(line).map((item) => ({
      ...item,
      category: currentCategory || item.category
    }));
  });

  return assignContextualEmojis(dedupeReviewItems(parsed));
}

If you just want to get words from the teacher's original text, you can also directly extract all English words, deduplicate, and then edit:

function extractEnglishWords(sourceText: string) {
  const matches = sourceText.match(/[A-Za-z]+(?:['’-][A-Za-z]+)*/g) || [];
  const seenWords = new Set<string>();

  return matches.reduce<string[]>((words, match) => {
    const normalizedWord = match.toLocaleLowerCase();
    if (!normalizedWord || seenWords.has(normalizedWord)) return words;

    seenWords.add(normalizedWord);
    words.push(normalizedWord);
    return words;
  }, []);
}

Here, AI wasn't stuffed into every runtime flow. It's more of an assistant during the development and organization phase: helping me converge the teacher's long text into structured content; when actually playing, the app still uses simple, stable, manually editable data.

2. Auto-completing Chinese translation, Emoji, and sentences

Parents can just input English.

The app will first try the browser's built-in translation capability, and if not supported, call an online translation service, caching the result locally to avoid repeated requests for the same word.

The Emoji strategy is also kept as simple as possible:

  1. If the teacher's original text already has an Emoji, keep it first;
  2. If the local mapping table has a corresponding word, match it directly;
  3. If a sentence contains a word that has already appeared, reuse that word's Emoji;
  4. If it really can't be found, display a generic placeholder icon.

For example:

function buildReviewItem(english: string, chinese = "") {
  const explicitEmoji = extractReviewEmoji(chinese);

  return {
    id: createReviewId("item"),
    english,
    chinese,
    category: inferCategory(english),
    emoji: explicitEmoji || pickEmoji(english, chinese)
  };
}

Why use system Emoji first, instead of searching for images online for every word from the start?

Because "reliable display" is more important than "particularly beautiful images."

Emoji is unaffected by image hotlinking protection, API rate limits, and network fluctuations. For early childhood vocabulary like apple, cat, car, it's already clear enough.

3. When Emoji isn't enough, search for images online

When encountering content where Emoji isn't accurate enough, an image picker can be opened directly on the card.

The app integrates Openverse and Wikimedia Commons; entering an English keyword returns several candidate images. If the Openverse request fails, it automatically tries Wikimedia.

async function fetchReferenceImages(item, cursor, source) {
  if (source === "openverse") {
    try {
      return await fetchOpenverseReferenceImages(item.english, cursor);
    } catch {
      return await fetchCommonsReferenceImages(item.english, cursor);
    }
  }

  return fetchCommonsReferenceImages(item.english, cursor);
}

If the open image libraries still can't find a suitable image, you can also jump to external sources like Baidu Images or Sogou Images to search, and copy the image URL back.

If that still doesn't work, upload locally.

Local images aren't uploaded as-is. The page first uses Canvas to compress the longest side to 720px, then converts to WebP, controlling the size before uploading to Supabase Storage:

const maxDimension = 720;
const scale = Math.min(
  1,
  maxDimension / Math.max(image.naturalWidth, image.naturalHeight)
);

context.drawImage(image, 0, 0, width, height);
const result = canvas.toDataURL("image/webp", 0.72);

This image strategy is essentially a three-tier fallback:

System Emoji
    ↓ Not suitable
Openverse / Wikimedia online image search
    ↓ Still not suitable
Local upload to Supabase Storage

It doesn't pursue perfection in one go, but ensures parents always have a way to swap in an image the child can understand.

4. Pronunciation doesn't require a separate paid service; use browser capabilities first

English reading uses the browser's built-in speechSynthesis.

const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "en-US";
utterance.pitch = 1.08;
utterance.rate = 0.78;

window.speechSynthesis.speak(utterance);

I set the speech rate slightly slower than the default, making it more suitable for children to repeat after.

To prevent some browsers from not properly triggering the end event, a fallback timer was also added. This way, the auto-learning mode won't get stuck on a single card forever.

This solution's voice might not be as natural as professional foreign teacher audio, but it's free, simple to call, and sufficient for "repeatedly listening to a few words of the day."

5. Local first, then sync to cloud

Review content and learning progress are first written to localStorage.

This is because the biggest fear for a home learning tool is the entire page becoming unusable if the network stutters. Local caching at least ensures that already entered content can still be reviewed.

Cloud sync then handles multi-device sharing.

I didn't put Supabase's service role key on the frontend, but instead routed all requests through Supabase Edge Functions:

Vue Page
   ↓
Supabase Edge Function
   ├── PostgreSQL: Save review content and version numbers
   └── Storage: Save locally uploaded reference images

Reading can be done publicly, but writing requires entering a family edit code.

At the same time, each piece of cloud data has a revision. When saving, the frontend sends the version number it read; if the cloud has already been modified by another device, the API returns 409, prompting to sync the latest content first, preventing a later save from directly overwriting a previous modification.

For a small tool used by only a few people at home, this permission setup isn't complex, but it's just enough.

Deployment Pitfalls

Initially, I wanted to put both the frontend and the data interface on Cloudflare.

Frontend on Cloudflare Pages, data in D1, and providing read/save interfaces through Pages Functions. The plan looked very complete, and all had free tiers.

I had even already connected the table structure, D1 binding, and REVIEW_EDIT_CODE.

But during actual debugging, the problem wasn't that the code couldn't run, but that the services were often inaccessible. The deployed pages, interfaces, and console were all unstable in my network environment. Debugging a small issue required repeatedly confirming whether it was a code error or the network wasn't connected.

What drains people the most in personal projects is often not difficult problems, but this kind of uncertainty.

In the end, I didn't continue to force it and directly migrated the data layer to Supabase:

The frontend itself is still a Vite static build, so there's no need to bind it with the data service. I handed the pages to GitHub Pages, using GitHub Actions to automatically build on every push to main:


- name: Build

run: pnpm build:github

env:

VITE_REVIEW_SYNC_URL: ${{ vars.VITE_REVIEW_SYNC_URL }}

- name: Deploy to GitHub Pages

uses: actions/deploy-pages@v4

There's another easily missed point here: GitHub Pages deploys under a repository sub-path, so Vite's base needs to correspond to the repository name.


const base =

process.env.VITE_DEPLOY_TARGET === "github-pages"

? "/vancy-learn-english/"

: "/";

The final architecture became:


GitHub Pages: Deploy Vue static pages

Supabase PostgreSQL: Save review content

Supabase Storage: Save uploaded images

Supabase Edge Function: Unified read/write entry point

After separating the pages and data, it actually became clearer.

It should be noted that services like GitHub, Supabase, Openverse, and Wikimedia may all have access fluctuations in domestic network environments. When initializing the project, deploying functions, and troubleshooting interfaces, I needed to prepare a stable network environment.

This is also why the app always retains localStorage: when cloud sync fails, the local cache can still be used. A day's review shouldn't be delayed just because a free service is temporarily unreachable.

Results

The final product turned out simpler than I initially thought.

Overall Effect

Settings

Converting Content

Setting Custom Images

Learning Mode

After opening the page, the child sees a large card for the current review day. In the middle is an Emoji or reference image, with English and Chinese below. The card auto-reads aloud when switching, and can also enter learning mode to auto-play at set intervals.

Parent-side operations are concentrated in "Review Settings":

Before, reviewing Letter B meant first scrolling through the group chat, finding the video, dragging the progress bar, and pausing one by one.

Now, just open the day's review day:


bee → ball → balloon → bear

After the words are played, continue listening to:


I see a bee.

I have a ball.

The balloon is big.

The bear is brown.

It has no complex learning system, nor an addictive points design.

But it turned a review session that was originally a bit taxing for parents into a process the child is willing to tap through a few times on their own.

That's enough for me.

Summary

After finishing this app, I increasingly feel that good software isn't necessarily the software with the most features.

For commercial products, we care about user scale, growth, and universality; but for personal products, value can be very specific: it just needs to save the family ten minutes of repetitive, inefficient, and easily frustrating operations every day, and it already has meaning for existing.

What really solved the problem this time wasn't any single technical point among Vue, Supabase, or AI.

It was first clearly articulating the pain point:


I don't need another English curriculum.

I just need to turn the words and sentences the teacher taught today into cards my child is willing to listen to repeatedly.

Once the requirements converge, the subsequent implementation naturally becomes simple.

AI lowered the cost of "making software that only serves one's own family," and frontend experience allowed me to continue refining this prototype into something handy, stable, and usable long-term.

Project URL: github.com/mjsong07/vancy-learn-english

Live URL: mjsong07.github.io/vancy-learn-english

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away." — Antoine de Saint-Exupéry

Perfection is not when there is nothing more to add, but when there is nothing left to take away.

Comments

Top 4 from juejin.cn, machine-translated. The original thread is authoritative.

柒号华仔

King of the grind

安欣

Uncle, I want one too. I'm preparing for CET-4 and CET-6 and I'm missing one.

check_null

Way too intense

杭州胖敬亭

Isn't this app a bit lacking in fun for kindergarten kids? Wouldn't watching English cartoons be more effective?