Building LearnLeap — A Timeline Story and Complete Tutorial

LearnLeap is a quiz platform for revising non-fiction books and learning concepts fast. It lives at quiz.hasit.in, the code is at github.com/hasitpbhatt/gitquiz, and this post is both the story of how it evolved and a tutorial on each component.

If you follow along commit-by-commit, you’ll see the same progression I took: content first, then engine, then polish, then AI, then infrastructure.


Chapter 1: The Philosophy

Every project has a set of beliefs that guide its decisions, even if they aren’t written down. Here are the ones that drove LearnLeap.

— On Learning —

“Retrieval practice — recalling facts or concepts from memory — is a more effective learning strategy than review by rereading.”
— Make It Stick: The Science of Successful Learning, Harvard University Press

This is the scientific foundation of the entire platform. Reading a book gives you exposure. Answering a quiz forces your brain to reconstruct the knowledge. That act of reconstruction strengthens the neural pathways. It hurts more than rereading, but it works better.

“Learning is not the product of teaching. Learning is the product of the activity of learners.”
— John Holt

The AI doesn’t lecture you. The quiz doesn’t explain first and ask later. You engage, you commit to an answer, you get feedback. The learning happens in the gap between choosing and discovering.

“Tell me and I forget. Teach me and I remember. Involve me and I learn.”
— often attributed to Benjamin Franklin / Xunzi

A quiz is involvement. You can’t passively scroll through it. Every click is a stake in the ground.

— On Simplicity —

“Simplicity is the ultimate sophistication.”
— Leonardo da Vinci

The quiz engine is ~300 lines of vanilla JavaScript. No React, no Vue, no build step, no state management library, no backend. This wasn’t laziness — it was intentional. Every dependency you add is a surface area for bugs, a point of friction for contributors, and a reason to upgrade things later.

The design follows the same principle. No onboarding tour, no gamification popups, no newsletter signup forms. Question, options, feedback. That’s the entire loop. Everything else is noise.

No accounts, no login, no user tracking. The score resets on page reload. This sounds anti-modern-web, but for a tool focused on self-study it’s liberating. You’re not building a profile, you’re testing your understanding. There’s no leaderboard to climb, no streak to maintain, no data to export. Just you and the content.

— On Building in Public —

“The best time to start was yesterday. The next best time is now.”

The first commit to the repo was a single JSON file for Atomic Habits Chapter 1. There was no website, no plan, no roadmap. Just a question file. Then another. Eventually a course list. Then a bare HTML page to serve them. Then a wrapper for GitHub Pages. Then a name (“Quiz Portal Pro”, later rebranded to “LearnLeap”).

The project grew organically — not from a grand vision but from a simple need: I wanted to review books I’d read, and a flashcard app felt too sterile. I wanted scenarios, context, explanations. So I built exactly that, one file at a time.

The lesson: don’t wait until you have a full product to start. Start with the smallest possible artifact that provides value. For me, that was a JSON file with five questions about habits. Everything else was iteration.

— On Cost —

“The best technology is the one you can afford to run forever.”

Every infrastructure decision was made with long-term sustainability in mind:

  • Static hosting (free)
  • Raw GitHub for data storage (free)
  • Cloudflare Workers for proxying (free tier)
  • Mistral Small for AI (~$0.0002 per explanation)
  • No database, no auth, no server

Static hosting, DNS, and data storage don’t scale with users. The AI add-on is pay-per-call, but at ~$0.0002 per explanation it stays negligible until serious scale.

— The Guiding Question —

Before every feature, I asked: “Does this make the user think harder about the content, or does it distract them?”

The progress bar stays. The leaderboard never happened. The AI is a click away, not automatic. The share card exists but doesn’t interrupt. The catalog is a simple dropdown with a search filter.

Every feature that made it into LearnLeap passed that test. Every feature that didn’t, failed it.


Chapter 2: The Content-First Approach (Commits 1-100+)

The repo started not with code, but with JSON files.

Every quiz module is a single JSON file. Here’s the complete schema with every field explained:

{
“id”: “001”,
“course”: “book-atomic-habits”,
“questions”: [
{
“id”: 1,
“question”: “What is the 1st Law of Behavior Change?”,
“options”: [
“Make it Obvious”,
“Make it Attractive”,
“Make it Easy”,
“Make it Satisfying”
],
“answer”: “Make it Obvious”,
“difficulty”: “easy”,
“content”: “The 1st Law: Make it Obvious. Cue -> Craving -> Response -> Reward.”,
“description”: “You’re trying to build a habit of reading before bed…”,
“explanation”: “The Four Laws are the framework for building good habits…”
}
]
}

Field-by-field:

  • id: Module number within the course (001, 002…)
  • course: Folder name matching the course directory
  • question: The question text presented to the user
  • options: Array of 4 choices, always one correct
  • answer: Exact string matching one of the options
  • difficulty: easy, medium, or hard (controls pace and confidence calibration)
  • content: Source passage or context the question is based on (shown in a highlighted box)
  • description: A real-world scenario framing the question (the “why should I care”)
  • explanation: Shown after answering, gives the reasoning behind the correct answer

Each course is a folder (book-atomic-habits/) with numbered JSON files (001.json, 002.json…). A courses_list.txt at the root lists every course, one folder name per line.

“No database? Seriously?” — Yes. GitHub’s raw file URLs serve as a free, globally distributed CDN for all quiz data. The catalog is a plain text file. The trade-off: no real-time updates, no writes from users, no authentication. But for a read-heavy self-study tool, this is ideal. There’s zero database maintenance, zero connection pooling, zero migration scripts. If GitHub is down, the quizzes are down — but that’s happened about 3 hours total in 2 years.

For the first several months, the only “commits” were adding courses — Atomic Habits, Deep Work, Influence, The Adaptive Edge, Bhagavad Gita, The Psychology of Money, The Changing World Order, Super Thinking, Algorithms to Live By, and more.

The content dictated the architecture, not the other way around. This was a deliberate choice: build a library first, then figure out how to serve it.

If you’re building your own quiz platform, start here. Don’t write a single line of UI code until you have at least 5 courses. It forces you to think about the data model correctly.


Chapter 3: The Quiz Engine (Commit 3061129)

The first real code commit was index.html — a single-file quiz portal using Tailwind CSS (CDN), vanilla JavaScript, and a small styles.css for custom theming.

The architecture is brutally simple:

  • Static HTML served from GitHub Pages (or any static host)
  • Quiz data loaded via fetch() from raw GitHub URLs
  • State tracked in global JS variables
  • No build step, no framework, no backend

The engine has four screens:

  1. Setup Screen — Catalog dropdown or custom URL input
  2. Quiz Flow — Progress bar, score/streak/timer, question, options, explanation
  3. AI Section — Hidden until an answer is chosen
  4. Completion Screen — Final score, certificate download

— Scoring —

Scoring is dead simple: +100 per correct answer, +0 for wrong. The streak counter increments on consecutive correct answers and resets on a wrong one. There’s no partial credit, no weighted difficulty, no time bonus. Why? Because the primary goal is retrieval practice, not game mechanics. The score is a loose confidence signal — if you finish with 800 out of 700 possible, you probably knew the material.

— Next Module Flow —

When you finish the last question, the completion screen shows a “Start Next Module” button if the next module exists. It derives the next URL by incrementing the module number:

const nextUrl = currentUrl.replace(/(\d+)(?=.json)/, (m) =>
String(parseInt(m, 10) + 1).padStart(3, ‘0’));

If the next file exists (200 OK), the button appears. If not (404), the button is hidden and you return to the catalog. This means new modules added to a course are automatically discoverable — no config changes needed.

— Mobile —

Responsive from the start. Tailwind’s breakpoint classes handle the layout: single-column on mobile, wider on tablet/desktop. The only intentional mobile concession is the fixed share button in the top-right corner — on very small screens it overlaps the header slightly, but it’s a trade-off for accessibility.

— Loading the Catalog —

const CATALOG_URL = “https://raw.githubusercontent.com/hasitpbhatt/gitquiz/main/courses/courses_list.txt”;

async function loadCatalog() {
const response = await fetch(CATALOG_URL);
const text = await response.text();
fullCatalog = text.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
renderCatalogOptions(fullCatalog);
}

The catalog is a plain text file, one course per line. Each line is the folder name (e.g. “book-atomic-habits”). The dropdown is rendered from this list.

— Starting a Quiz —

When the user clicks “Begin Challenge” or lands with a ?course= URL param:

async function initializeQuiz(url) {
currentUrl = url;
const res = await fetch(url);
const module = await res.json();
quizData = module.questions;
currentIdx = 0;
renderQuestion();
}

The URL points directly to a raw GitHub JSON file. The module is loaded, questions are shuffled, and the first question renders.

— Rendering a Question —

function renderQuestion() {
const q = quizData[currentIdx];
// Update progress bar
// Update question counter “3 / 7”
// Set module label from URL folder name
// Show question text
// Show content/context box if available
// Show scenario/description
// Render options as buttons with A/B/C/D badges
// Hide AI section and explanation
}

Each option button gets an onclick handler that records the answer, shows the explanation, and reveals the AI section.

This is the entire engine. ~200 lines of JavaScript. It scales to any number of courses because the data is external.


Chapter 4: The UX Evolution (Commits 2443d90 through a5a02b1)

After the engine was solid, the polish passes began. Here is the exact evolution from the git log:

— Phase 1: Streamlining (2443d90) —

Removed the mandatory name input field. Added localStorage for the user’s name (only asked when generating a certificate). Implemented auto-start — if you land with ?course=atomic-habits, the quiz starts immediately, no button click needed. Changed the URL parameter from ?book= to ?course= for cleaner naming.

The key code:

const courseParam = new URLSearchParams(window.location.search).get(‘course’);
if (courseParam && fullCatalog.includes(courseParam)) {
const dropdown = document.getElementById(‘course-dropdown’);
dropdown.value = courseParam;
handleStart();
}

This runs in an IIFE immediately after the catalog loads. No window.onload dependency (a lesson learned when Google Ads blocked DOMContentLoaded).

— Phase 2: Navigation Fixes (81e8a8d) —

The “Return to Catalog” and “← Menu” buttons used to reload the page and re-trigger the auto-start, creating an infinite loop. The fix: strip the ?course= param on navigation.

← Menu

By using just the pathname (no query string), the auto-start condition is never met, and the user returns to the catalog cleanly.

— Phase 3: Share Cards (78bb40e through f62fc22) —

The share feature generates an image certificate using html2canvas. It renders a hidden div with the user’s score, course name, and a QR-like URL, then captures it as a PNG.

“Wait, the certificate is just a screenshot of a div? Not a real certificate?” — Exactly. It’s a styled div with the score, course name, and date, rendered off-screen and captured with html2canvas. It’s not a PDF, not server-generated, not verifiable. But it works instantly, requires no backend, and users share it on social media anyway. Practical over proper.

The share button uses the Web Share API when available, with a manual fallback for unsupported browsers.

function shareHandler() {
if (isCompletion) {
shareCertificate();
return;
}
if (isQuizActive && quizData.length > 0) {
shareQuestion();
return;
}
shareSetup();
}

Three modes: share the current question (with A/B/C/D options), share the completion certificate, or share the portal URL.

— Phase 4: Branding and Polish (257ac4d through a5a02b1) —

This was the biggest UX push, all in one weekend:

  • Rebranded from “Quiz Portal Pro” to “LearnLeap” with the tagline “Read. Retain. Rise.”
  • Added full SEO metadata: canonical URL, Open Graph tags, Twitter Cards, JSON-LD schema
  • Added a quote from Make It Stick (Harvard University Press) as social proof
  • Added a “← Menu” button during active quizzes to return to the catalog
  • Added a module label showing the course name and module number extracted from the URL
  • Added a question counter (“3 / 7”) next to the progress bar
  • Added letter badges (A/B/C/D) on option buttons
  • Added checkmark/cross icons on correct/wrong feedback
  • Added a score pop animation on correct answers
  • Added type badges (Book, Podcast, Course) to catalog items
  • Added transition-colors for smooth dark mode switching

The module label extraction is a neat trick:

const parts = currentUrl.split(‘/’);
const courseFolderName = parts[parts.length – 2];
const moduleFileName = parts[parts.length – 1].replace(‘.json’, ”);
// courseFolderName: “book-atomic-habits”
// moduleFileName: “001”
// Display: “Atomic Habits • 001”

The folder name follows a convention: {type}-{slugified-title}. By stripping the type prefix and replacing hyphens with spaces, you get a readable course name.

“That’s clever but brittle” — It is. If someone names a folder mycrazy-course_v2-final, the label comes out as “Mycrazy Course V2 Final”. The convention only works because I enforce it in my content pipeline. For a public platform this would need a proper metadata field. For a solo project, the folder name is the source of truth and that’s fine.


Chapter 5: The Course Quality Pipeline (Commits e3f581e through eff7de4)

As the course library grew past 1500 questions, quality became the bottleneck. I built a book-to-quiz skill — essentially a set of scripts and conventions for generating high-quality quiz content.

The key learnings:

  1. Difficulty distribution should be ~40% easy, ~40% medium, ~20% hard. Here is how each level is defined:
  • Easy: Recall of a directly stated fact. The answer is explicitly in the content. If you read the chapter, you know it.
  • Medium: Application of a concept to a new scenario. You need to understand the idea, not just remember the words.
  • Hard: Synthesis across multiple concepts or edge cases. Requires connecting ideas from different parts of the material. These are the difference between “I’ve read this” and “I understand this.”
  1. Review-only chapters (chapters asking “what did you learn” rather than “how would you apply this”) should be removed or merged
  2. Answer options should never reference each other (“Both B and C”, “All of the above”) — it breaks randomization
  3. Scenarios should be diversified across professional, personal, historical, and hypothetical contexts
  4. Each question needs a unique scenario to avoid cross-chapter repetition

The audit commit (324eae2) touched all 17 courses, fixing difficulty distributions and removing review-only chapters. The difficulty refactor (a951880) added difficulty fields to all 1917 questions across 19 courses.

If you’re building a quiz platform, invest in your content pipeline before your UI. A beautiful quiz with bad questions is worse than an ugly quiz with great questions.


Chapter 6: The AI Explain Feature (Commit b6ba4c0)

This is the feature most people ask about. Here is the complete tutorial.

— The Problem —

Users answered a question, saw the correct answer and a short explanation, but wanted more. They wanted to understand the “why” behind the answer, with real-world examples and intuitive explanations.

I needed this to be:

  • On-demand (not automatic — I didn’t want the AI to interrupt the quiz flow)
  • Cost-controlled (no unpredictable bills)
  • Zero-infrastructure (no backend to maintain)

— The Architecture —

Browser (JS) -> Cloudflare Worker (proxy) -> Mistral AI API

Three layers, each with a specific job.

— Layer 1: The HTML — Explain More with AI

Both the button and the response div start hidden. The entire section is feature-flagged — if MISTRAL_PROXY_URL is empty, the section never appears, and the user sees no trace of the AI feature.

— Layer 2: The JavaScript —

Before I show the code, here is the flow:

  1. User clicks an answer option
  2. The onclick handler records the answer and shows the explanation
  3. If MISTRAL_PROXY_URL is set, the AI section becomes visible
  4. User clicks “Explain More with AI”
  5. The askAI() function constructs a prompt, sends it to the proxy, and displays the response

The state tracking:

let MISTRAL_PROXY_URL = ‘https://quiz-ai-proxy.hasit-p-bhatt.workers.dev/’;
let lastSelectedAnswer = ”;
let lastAnswerCorrect = false;

The proxy URL is the only configuration needed. It points to a Cloudflare Worker that forwards requests to Mistral.

The prompt construction is the most important part:

const prompt = ‘You are a tutor helping a student understand a concept. ‘ +
‘They just answered a quiz question.\n\n’ +
‘Question: ‘ + q.question + ‘\n’ +
‘Context: ‘ + (q.content || ‘N/A’) + ‘\n’ +
‘Scenario: ‘ + (q.description || ‘N/A’) + ‘\n’ +
‘Correct Answer: ‘ + q.answer + ‘\n\n’ +
‘The student selected: “‘ + lastSelectedAnswer + ‘” and was ‘ +
(lastAnswerCorrect ? ‘CORRECT’ : ‘INCORRECT’) + ‘.\n\n’ +
‘Provide a deeper, intuitive explanation of this concept with a fresh real-world example. ‘ +
‘Keep it concise (2-3 paragraphs).’;

The prompt feeds the AI five pieces of context:

  • The question itself
  • The source content (a quote or passage the question is based on)
  • The scenario (a real-world situation the question frames)
  • The correct answer
  • What the student chose and whether they were right

This lets Mistral tailor the explanation. If the student was correct, it reinforces and deepens their understanding. If they were wrong, it gently corrects the misconception.

The API call uses Mistral’s OpenAI-compatible format:

const res = await fetch(MISTRAL_PROXY_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
model: ‘mistral-small-latest’,
messages: [{ role: ‘user’, content: prompt }]
})
});

const data = await res.json();
const content = data.choices?.[0]?.message?.content || data.content || ”;

responseDiv.innerHTML = content;

The response is rendered as innerHTML, which means Mistral can return formatted text. The null-safe chaining (data.choices?.[0]?.message?.content) handles both Mistral’s and OpenAI’s response formats gracefully.

The loading state is handled with a spinner and disabled button:

btn.disabled = true;
btn.innerHTML = ‘⟳ Thinking…’;

And the cleanup in the finally block:

btn.disabled = false;
btn.innerHTML = ‘Explain More with AI’;

— Layer 3: The Cloudflare Worker —

The worker is 46 lines. Here is the full code with explanation:

export default {
async fetch(request, env) {
// Handle CORS preflight (browsers send this before the actual POST)
if (request.method === ‘OPTIONS’) {
return new Response(null, {
headers: {
‘Access-Control-Allow-Origin’: ‘*’,
‘Access-Control-Allow-Methods’: ‘POST, OPTIONS’,
‘Access-Control-Allow-Headers’: ‘Content-Type’,
},
});
}

// Only accept POST requests
if (request.method !== 'POST') {
  return new Response('Method not allowed', { status: 405 });
}

try {
  // Forward the request body as-is to Mistral
  const body = await request.json();
  const mistralResponse = await fetch('https://api.mistral.ai/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // API key is a Worker secret, never exposed to the client
      'Authorization': `Bearer ${env.MISTRAL_API_KEY}`,
    },
    body: JSON.stringify(body),
  });

  const data = await mistralResponse.json();

  // Return Mistral's response to the client
  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
  });
} catch (error) {
  return new Response(JSON.stringify({ error: error.message }), {
    status: 500,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

},
};

The key security property: the Mistral API key is stored as a Cloudflare Worker secret (env.MISTRAL_API_KEY). It is never sent to the browser. The client only knows the proxy URL.

“Does the AI remember our conversation across questions?” — No. Each “Explain More with AI” click is a stateless single-turn request. The prompt includes the current question’s context but no history. This is intentional: quizzes test discrete concepts, not conversations. If you want continuity, click the button again on the next question — the new prompt includes fresh context.

“Rate limiting?” — None on the client side. You could click the button 50 times on the same question and get 50 explanations (billed at ~$0.01 total). The Cloudflare Worker has daily free-tier limits (100k requests), and Mistral has rate limits on their API, but for individual use these never hit.

Deploy with a wrangler.toml:

name = “quiz-ai-proxy”
main = “worker.js”
compatibility_date = “2025-04-01”

And set the secret:

npx wrangler secret put MISTRAL_API_KEY

— Cost Analysis —

Mistral Small (mistral-small-latest) costs $0.20/1M input tokens and $0.60/1M output tokens. A typical call runs ~300-500 input tokens (question, content, scenario, answer, options, instructions) + ~200-300 output tokens for the explanation. Roughly $0.0002 per explanation depending on content length. At 1000 a month, that’s ~$0.20.

The Cloudflare Worker is free (100k requests/day on the free plan).

Total infrastructure cost for the AI feature: $0.


Chapter 7: The Subdomain Routing Trick

My static hosting provider (GitHub Pages) only allows one custom domain. I wanted quiz.hasit.in for the quiz portal, but the root hasit.in was already pointing to my personal site.

The solution: a Cloudflare Worker that silently proxies quiz.hasit.in -> hasit.in/quiz.

— The Worker —

export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);

// Rewrite the destination: change hostname and prepend path
url.hostname = 'hasit.in';
url.pathname = '/quiz' + url.pathname;

// Forward the request to the main domain
const modifiedRequest = new Request(url.toString(), {
  method: request.method,
  headers: request.headers,
  body: request.body,
  redirect: 'manual'
});

const response = await fetch(modifiedRequest);

// If the backend tries to redirect, rewrite the Location header
// so the visitor stays on quiz.hasit.in
if ([301, 302, 307, 308].includes(response.status)) {
  const location = response.headers.get('Location');
  if (location && location.includes('hasit.in/quiz')) {
    const newResponse = new Response(response.body, response);
    const newLocation = location.replace('hasit.in/quiz', 'quiz.hasit.in');
    newResponse.headers.set('Location', newLocation);
    return newResponse;
  }
}

return response;

},
};

The redirect handling is the subtle part. If hasit.in/quiz issues a 301 redirect to hasit.in/quiz/something, the worker rewrites it to quiz.hasit.in/something so the browser never shows the root domain.

— Setup Steps —

Step 1: Create the Worker
Cloudflare Dashboard -> Workers & Pages -> Create Worker. Paste the code, click Save and Deploy.

Step 2: Add a Route
Domain settings -> Workers Routes -> Add Route.
Route: quiz.hasit.in/*
Worker: Select the worker you just created.

Step 3: Proxy the DNS
DNS Records -> Find the quiz record -> Set Proxy status to Proxied (orange cloud).

— Why Assets Don’t Break —

I checked every reference:

  • styles.css, script.js — relative paths, the Worker rewrites the pathname
  • CDN scripts (Tailwind, html2canvas, Google Fonts) — absolute URLs, no domain dependence
  • Quiz JSON data — loaded from raw GitHub URLs, no domain dependence
  • Canonical and OG URLs — updated to point to quiz.hasit.in

The only subtle thing: the share URL uses window.location.origin + pathname. When accessed via quiz.hasit.in, shared links point to quiz.hasit.in. Both URLs work identically, so this is fine.


Chapter 8: By the Numbers

End of April 2026, here is the state of the project:

  • 19 courses
  • 1917 questions across all courses
  • 3 difficulty levels (easy/medium/hard with ~40/40/20 distribution)
  • ~150 commits on main
  • 1 Cloudflare Worker for AI (quiz-ai-proxy)
  • 1 Cloudflare Worker for subdomain routing (quiz-silent-proxy)
  • 1 Cloudflare secret (MISTRAL_API_KEY)
  • $0 infrastructure cost
  • ~90 lines of JavaScript for the AI feature
  • ~40 lines for the subdomain proxy
  • ~300 lines for the entire quiz engine

  1. Start the content pipeline earlier. The first 6 months was just adding courses. I should have built the book-to-quiz skill in month 1.
  2. Use a proper JSON schema validator from the start. Half of the early bugs were malformed JSON files (missing commas, duplicate keys).
  3. Add difficulty tagging from the beginning. Retroactively tagging 1917 questions was painful.
  4. The share card code is fragile (html2canvas + hidden div rendering). If I rebuilt this, I’d use a canvas-based approach or a dedicated share image API.
  5. The subdomain proxy worker works, but a Cloudflare Page Rule could handle simple redirects without any code. The Worker approach gives more control over redirect rewriting.

The code is open source at github.com/hasitpbhatt/gitquiz. Live at quiz.hasit.in.

Start with the content. Build the engine. Polish the UX. Add AI last. That order matters.

How is the AI not costing anything?

It does cost, but negligibly. The fixed infrastructure (GitHub Pages, Cloudflare, raw file serving) is free. The variable cost is the Mistral API: ~$0.0002 per explanation. At 1000 explanations a month, that’s $0.20. At 10,000, it’s $2.04. The bet is that users who click “Explain More with AI” on every question are a small fraction of users.

Can’t anyone see all the answers in the network tab?

Yes. Open DevTools, look at the fetch to the raw GitHub URL, and you have the full JSON with correct answers highlighted. This isn’t a vulnerability — it’s a design choice. LearnLeap is for self-study. If you cheat, you’re only cheating yourself. The same way you can flip to the back of a textbook for answer keys. The value isn’t in hiding answers, it’s in the act of attempting before revealing.

Can I retake a module? Is there spaced repetition?

No spaced repetition built in yet. You can retake any module by selecting it from the catalog again — the questions reshuffle, but the set is the same. True spaced repetition (SM-2, Leitner, etc.) would require user profiles and a database, which conflicts with the “no accounts” philosophy. I’m still deciding how to resolve this.

Vanilla JS? No React? No framework at all?

What would a framework add here? The quiz engine has exactly one interactive state: a question is being answered, or it isn’t. There are no routes, no forms, no real-time updates, no API calls that mutate data, no component tree deeper than 3 levels. React would add bundle size, build step, and cognitive overhead for zero benefit. Vanilla JS with global state is not only sufficient — it’s optimal for this scale of application. The entire engine is ~300 lines. A React version would be longer.

The module label from a folder name is fragile

It is. If someone names a folder “mycourse-v2”, the label renders as “Mycourse V2”. The convention only holds because I enforce it in my content pipeline. For a public platform this needs a metadata field. For a solo project, the folder name as source of truth keeps things simple.

The certificate is a screenshot?

html2canvas renders a styled div, captures it as PNG, and triggers a download. It’s not a verifiable credential, not a PDF, not server-signed. But users share it on Twitter and LinkedIn anyway. Practical over proper.

Why no accounts or login?

Because the product is “test your knowledge of this book,” not “build a profile on our platform.” No accounts means no passwords to leak, no GDPR paperwork, no forgot-password flow, no email verification, no session management, no database. The trade-off is no progress persistence across devices. For a review tool you use on your own machine, that’s acceptable.

Does the subdomain need a Worker? Couldn’t you just redirect?

A simple 301 redirect from quiz.hasit.in to hasit.in/quiz would work. But then the user sees hasit.in/quiz in the address bar. The Worker keeps quiz.hasit.in in the URL. It also rewrites redirect Location headers so that if hasit.in/quiz internally redirects, the browser stays on the subdomain. A Page Rule can’t do that rewrites.

What about the anti-cheat on options referencing each other?

Early courses had options like “Both B and C” or “All of the above.” These are position-dependent — after shuffling, “Both B and C” refers to the wrong options. The fix: every option must be standalone. If the correct answer is “Make it Obvious,” write that exactly. No relative references.

Is this legal?

Each course is derived from public domain material and transformed into an original quiz with new scenarios, questions, and explanations.

Can I use this portal for my own quizzes?

Yes. The “Custom URL” tab on the setup screen lets you paste any direct link to a JSON file following the same schema. If you host your quizzes on your own GitHub repository, you own the content and LearnLeap is just the rendering engine. The portal doesn’t store or claim ownership over anything you load through it.

What happens if GitHub is down?

Everything breaks. All quiz data, the catalog, course images — it’s all served from GitHub raw file URLs. In two years, GitHub has been down for maybe 3 hours total. For a self-study tool, that’s acceptable uptime.

Mobile experience?

Responsive from day one via Tailwind breakpoints. Single-column layout, touch-friendly button sizes, no hover-dependent interactions. The only concession is the fixed share button which slightly overlaps the header on very small screens.

How many people use this?

Just me and a handful of friends who share course links. It’s not a startup. It’s a tool I built for myself that other people found useful. That’s the entire scale.

Home » Building LearnLeap — A Timeline Story and Complete Tutorial

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.