What if you opened a web app every morning and it had a fresh, personalized English lesson waiting for you? And you couldn’t unlock tomorrow’s lesson until you completed today’s?
That’s what we’re building in this post. A Daily Lesson Generator that:
- ✅ Generates a new lesson every day using AI
- ✅ Requires completion before unlocking the next day
- ✅ Tracks your progress with an assessment dashboard
- ✅ Adapts difficulty based on your performance
- ✅ Covers real tech lead scenarios from Part 9
App Overview
Core Flow
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Open App │───▶│ Today's │───▶│ Complete │
│ (Login) │ │ Lesson │ │ Exercises │
└──────────────┘ └──────────────┘ └──────────────┘
│
┌──────────────┐ ┌──────┴───────┐
│ Unlock │◀───│ Get Score │
│ Next Day │ │ + Feedback │
└──────────────┘ └──────────────┘
Features
| Feature | Description |
|---|---|
| Daily Lesson | One new lesson per day, generated by AI |
| Completion Gate | Must complete today’s lesson to unlock tomorrow’s |
| Streak Tracker | Track consecutive days of practice |
| Assessment Score | Score each exercise (vocabulary, grammar, fluency) |
| Progress Dashboard | Visual charts showing improvement over time |
| Lesson History | Review and redo past lessons |
| Difficulty Adaptation | AI adjusts based on your performance |
Tech Stack
Frontend: Next.js 14 (App Router) + TypeScript
Styling: Tailwind CSS
Database: Supabase (PostgreSQL)
AI: Claude API (lesson generation + feedback)
Auth: Supabase Auth
Deploy: Vercel (free tier)
Database Schema
Supabase Tables
-- Users (handled by Supabase Auth, extended with profile)
CREATE TABLE user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id),
display_name TEXT,
current_level TEXT DEFAULT 'beginner', -- beginner, intermediate, advanced
streak_count INTEGER DEFAULT 0,
longest_streak INTEGER DEFAULT 0,
total_lessons_completed INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Daily Lessons
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
day_number INTEGER NOT NULL,
date DATE NOT NULL,
title TEXT NOT NULL,
category TEXT NOT NULL, -- standup, meeting, technical, presentation, email
difficulty TEXT NOT NULL, -- beginner, intermediate, advanced
content JSONB NOT NULL, -- full lesson structure
is_completed BOOLEAN DEFAULT FALSE,
is_unlocked BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, day_number)
);
-- Exercise Results
CREATE TABLE exercise_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lesson_id UUID REFERENCES lessons(id),
user_id UUID REFERENCES auth.users(id),
exercise_type TEXT NOT NULL, -- vocabulary, speaking, writing, scenario
user_answer TEXT,
ai_feedback TEXT,
score INTEGER, -- 0-100
completed_at TIMESTAMPTZ DEFAULT NOW()
);
-- Assessment Progress
CREATE TABLE assessments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
assessment_date DATE NOT NULL,
vocabulary_score INTEGER, -- 0-100
grammar_score INTEGER, -- 0-100
fluency_score INTEGER, -- 0-100
confidence_score INTEGER, -- 0-100
overall_score INTEGER, -- 0-100
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
AI Lesson Generation
The Lesson Generation Prompt
Here’s the core prompt that generates daily lessons:
// lib/generate-lesson.ts
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
interface LessonRequest {
dayNumber: number;
userLevel: string;
previousScores: { category: string; avgScore: number }[];
completedCategories: string[];
}
export async function generateLesson(request: LessonRequest) {
const weakestCategory = request.previousScores
.sort((a, b) => a.avgScore - b.avgScore)[0]?.category || 'standup';
const prompt = `You are an English teacher for Vietnamese tech leads.
Generate a daily lesson for Day ${request.dayNumber}.
Student level: ${request.userLevel}
Weakest area: ${weakestCategory}
Previously completed categories: ${request.completedCategories.join(', ')}
Generate a lesson in this EXACT JSON structure:
{
"title": "Day ${request.dayNumber}: [Descriptive Title]",
"category": "[standup|meeting|technical|presentation|email|negotiation]",
"estimatedMinutes": 15,
"warmup": {
"type": "vocabulary",
"instruction": "[What to do]",
"items": [
{
"word": "[English word/phrase]",
"pronunciation": "[IPA]",
"meaning": "[Simple definition]",
"exampleSentence": "[Usage in tech context]",
"vietnameseHint": "[Vietnamese translation]"
}
]
},
"mainExercise": {
"type": "[scenario|writing|speaking|reading]",
"scenario": "[Realistic tech lead situation]",
"instruction": "[Clear task description]",
"templateToComplete": "[Template with blanks ___ for user to fill]",
"idealAnswer": "[Model answer for reference]",
"scoringCriteria": ["Clarity", "Professional tone", "Structure", "Vocabulary"]
},
"speakingDrill": {
"instruction": "[What to practice saying out loud]",
"phrases": ["[Phrase 1]", "[Phrase 2]", "[Phrase 3]"],
"contextNote": "[When to use these phrases]"
},
"reflectionPrompt": "[Question for the user to reflect on]"
}
Focus the lesson on ${weakestCategory} scenarios.
Make it practical — every exercise should relate to real work situations.
Vocabulary should be tech-industry specific.
Difficulty: ${request.userLevel}.`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 2000,
messages: [{ role: 'user', content: prompt }],
});
return JSON.parse(response.content[0].text);
}
Scoring User Answers
// lib/score-answer.ts
export async function scoreAnswer(
exercise: any,
userAnswer: string
) {
const prompt = `You are grading an English exercise for a Vietnamese tech lead.
Exercise: ${exercise.instruction}
Scenario: ${exercise.scenario}
Ideal answer: ${exercise.idealAnswer}
User's answer: ${userAnswer}
Score the answer on these criteria (each 0-25, total 0-100):
1. Clarity: Is the message clear and easy to understand?
2. Professional tone: Is it appropriate for a business context?
3. Structure: Is it well-organized (introduction, body, conclusion)?
4. Vocabulary: Does it use appropriate technical/business vocabulary?
Respond in this JSON format:
{
"clarity": [0-25],
"professionalTone": [0-25],
"structure": [0-25],
"vocabulary": [0-25],
"totalScore": [0-100],
"feedback": "[2-3 sentences of constructive feedback]",
"improvedVersion": "[The user's answer rewritten with improvements]",
"keyTakeaway": "[One specific thing to remember]"
}`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1000,
messages: [{ role: 'user', content: prompt }],
});
return JSON.parse(response.content[0].text);
}
Key Pages
1. Today’s Lesson Page
// app/lesson/today/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase-client';
export default function TodayLesson() {
const [lesson, setLesson] = useState(null);
const [currentSection, setCurrentSection] = useState('warmup');
const [userAnswer, setUserAnswer] = useState('');
const [feedback, setFeedback] = useState(null);
const [isCompleted, setIsCompleted] = useState(false);
useEffect(() => {
loadTodayLesson();
}, []);
async function loadTodayLesson() {
const res = await fetch('/api/lesson/today');
const data = await res.json();
setLesson(data);
}
async function submitExercise() {
const res = await fetch('/api/lesson/submit', {
method: 'POST',
body: JSON.stringify({
lessonId: lesson.id,
exerciseType: currentSection,
userAnswer,
}),
});
const result = await res.json();
setFeedback(result);
}
async function completeLesson() {
await fetch('/api/lesson/complete', {
method: 'POST',
body: JSON.stringify({ lessonId: lesson.id }),
});
setIsCompleted(true);
}
if (!lesson) return <LoadingSkeleton />;
return (
<div className="max-w-2xl mx-auto p-6">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<span className="text-sm text-gray-500">
Day {lesson.day_number}
</span>
<h1 className="text-2xl font-bold">{lesson.title}</h1>
<span className="inline-block mt-1 px-3 py-1 rounded-full
text-xs bg-blue-100 text-blue-800">
{lesson.category}
</span>
</div>
<div className="text-right">
<span className="text-3xl">🔥</span>
<p className="text-sm font-medium">
{lesson.streak} day streak
</p>
</div>
</div>
{/* Progress Bar */}
<div className="w-full bg-gray-200 rounded-full h-2 mb-8">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{
width: currentSection === 'warmup' ? '25%'
: currentSection === 'main' ? '50%'
: currentSection === 'speaking' ? '75%'
: '100%',
}}
/>
</div>
{/* Section Content */}
{currentSection === 'warmup' && (
<WarmupSection
data={lesson.content.warmup}
onComplete={() => setCurrentSection('main')}
/>
)}
{currentSection === 'main' && (
<MainExercise
data={lesson.content.mainExercise}
userAnswer={userAnswer}
onChange={setUserAnswer}
onSubmit={submitExercise}
feedback={feedback}
onNext={() => setCurrentSection('speaking')}
/>
)}
{currentSection === 'speaking' && (
<SpeakingDrill
data={lesson.content.speakingDrill}
onComplete={() => setCurrentSection('reflection')}
/>
)}
{currentSection === 'reflection' && (
<ReflectionSection
prompt={lesson.content.reflectionPrompt}
onComplete={completeLesson}
/>
)}
{/* Completion */}
{isCompleted && (
<div className="mt-8 p-6 bg-green-50 rounded-xl text-center">
<span className="text-5xl">🎉</span>
<h2 className="text-xl font-bold mt-4">
Day {lesson.day_number} Complete!
</h2>
<p className="text-gray-600 mt-2">
Tomorrow's lesson unlocks in{' '}
<CountdownTimer targetDate={tomorrow()} />
</p>
<div className="mt-4 grid grid-cols-4 gap-4">
<ScoreCard label="Clarity" score={feedback?.clarity} />
<ScoreCard label="Tone" score={feedback?.professionalTone} />
<ScoreCard label="Structure" score={feedback?.structure} />
<ScoreCard label="Vocabulary" score={feedback?.vocabulary} />
</div>
</div>
)}
</div>
);
}
2. Progress Dashboard
// app/progress/page.tsx
export default function ProgressDashboard() {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Your Progress</h1>
{/* Stats Row */}
<div className="grid grid-cols-4 gap-4 mb-8">
<StatCard icon="🔥" label="Current Streak" value="12 days" />
<StatCard icon="📚" label="Lessons Done" value="42" />
<StatCard icon="📈" label="Avg Score" value="78/100" />
<StatCard icon="🏆" label="Level" value="Intermediate" />
</div>
{/* Score History Chart */}
<div className="bg-white rounded-xl p-6 shadow mb-8">
<h2 className="text-xl font-bold mb-4">Score Trend</h2>
<ScoreChart data={assessments} />
{/* Line chart showing daily scores over time */}
</div>
{/* Category Breakdown */}
<div className="bg-white rounded-xl p-6 shadow mb-8">
<h2 className="text-xl font-bold mb-4">Skills Breakdown</h2>
<div className="grid grid-cols-2 gap-4">
<SkillBar label="Vocabulary" score={82} color="blue" />
<SkillBar label="Grammar" score={68} color="green" />
<SkillBar label="Fluency" score={75} color="purple" />
<SkillBar label="Confidence" score={71} color="orange" />
</div>
</div>
{/* Category Performance */}
<div className="bg-white rounded-xl p-6 shadow mb-8">
<h2 className="text-xl font-bold mb-4">By Scenario Type</h2>
<RadarChart
data={{
'Standups': 85,
'Meetings': 72,
'Technical': 78,
'Presentations': 65,
'Emails': 80,
'Negotiations': 60,
}}
/>
</div>
{/* Lesson History */}
<div className="bg-white rounded-xl p-6 shadow">
<h2 className="text-xl font-bold mb-4">Lesson History</h2>
<div className="space-y-3">
{lessons.map((lesson) => (
<LessonRow
key={lesson.id}
day={lesson.day_number}
title={lesson.title}
score={lesson.score}
date={lesson.date}
category={lesson.category}
/>
))}
</div>
</div>
</div>
);
}
3. API Routes
// app/api/lesson/today/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerClient } from '@/lib/supabase-server';
import { generateLesson } from '@/lib/generate-lesson';
export async function GET(req: NextRequest) {
const supabase = createServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get user profile
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', user.id)
.single();
// Check if today's lesson already exists
const today = new Date().toISOString().split('T')[0];
const { data: existingLesson } = await supabase
.from('lessons')
.select('*')
.eq('user_id', user.id)
.eq('date', today)
.single();
if (existingLesson) {
return NextResponse.json({
...existingLesson,
content: existingLesson.content,
streak: profile.streak_count,
});
}
// Check if previous day's lesson is completed
const { data: previousLesson } = await supabase
.from('lessons')
.select('is_completed')
.eq('user_id', user.id)
.eq('day_number', profile.total_lessons_completed)
.single();
// Day 1 is always unlocked, others need previous completion
const dayNumber = profile.total_lessons_completed + 1;
if (dayNumber > 1 && !previousLesson?.is_completed) {
return NextResponse.json({
locked: true,
message: 'Complete the previous lesson first!',
previousDayNumber: dayNumber - 1,
});
}
// Get previous scores for adaptive difficulty
const { data: previousResults } = await supabase
.from('exercise_results')
.select('exercise_type, score')
.eq('user_id', user.id)
.order('completed_at', { ascending: false })
.limit(30);
const categoryScores = calculateCategoryAverages(previousResults);
const completedCategories = await getCompletedCategories(
supabase, user.id
);
// Generate new lesson with AI
const lessonContent = await generateLesson({
dayNumber,
userLevel: profile.current_level,
previousScores: categoryScores,
completedCategories,
});
// Save to database
const { data: newLesson } = await supabase
.from('lessons')
.insert({
user_id: user.id,
day_number: dayNumber,
date: today,
title: lessonContent.title,
category: lessonContent.category,
difficulty: profile.current_level,
content: lessonContent,
is_unlocked: true,
})
.select()
.single();
return NextResponse.json({
...newLesson,
streak: profile.streak_count,
});
}
// app/api/lesson/complete/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerClient } from '@/lib/supabase-server';
export async function POST(req: NextRequest) {
const supabase = createServerClient();
const { data: { user } } = await supabase.auth.getUser();
const { lessonId } = await req.json();
// Mark lesson as completed
await supabase
.from('lessons')
.update({
is_completed: true,
completed_at: new Date().toISOString(),
})
.eq('id', lessonId);
// Update streak
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', user.id)
.single();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const { data: yesterdayLesson } = await supabase
.from('lessons')
.select('is_completed')
.eq('user_id', user.id)
.eq('date', yesterdayStr)
.single();
const newStreak = yesterdayLesson?.is_completed
? profile.streak_count + 1
: 1;
await supabase
.from('user_profiles')
.update({
streak_count: newStreak,
longest_streak: Math.max(newStreak, profile.longest_streak),
total_lessons_completed: profile.total_lessons_completed + 1,
})
.eq('id', user.id);
// Calculate if user should level up
const avgScore = await calculateRecentAverage(supabase, user.id);
let newLevel = profile.current_level;
if (avgScore > 85 && profile.current_level === 'beginner') {
newLevel = 'intermediate';
} else if (avgScore > 85 && profile.current_level === 'intermediate') {
newLevel = 'advanced';
}
if (newLevel !== profile.current_level) {
await supabase
.from('user_profiles')
.update({ current_level: newLevel })
.eq('id', user.id);
}
return NextResponse.json({
success: true,
streak: newStreak,
levelUp: newLevel !== profile.current_level ? newLevel : null,
});
}
Weekly Assessment
Every 7 days, the app generates a comprehensive assessment:
// lib/generate-assessment.ts
export async function generateWeeklyAssessment(userId: string) {
const weekResults = await getWeekResults(userId);
const prompt = `Based on these exercise results from the past 7 days,
generate a weekly assessment:
Results: ${JSON.stringify(weekResults)}
Provide scores (0-100) and specific recommendations:
{
"vocabulary_score": [0-100],
"grammar_score": [0-100],
"fluency_score": [0-100],
"confidence_score": [0-100],
"overall_score": [0-100],
"strengths": ["[strength 1]", "[strength 2]"],
"areasToImprove": ["[area 1]", "[area 2]"],
"weeklyGoal": "[One specific goal for next week]",
"recommendation": "[Personalized advice]"
}`;
// ... call AI and store assessment
}
Quick Start Guide
1. Create the project
npx -y create-next-app@latest english-daily --typescript --tailwind --app --src-dir --use-npm
cd english-daily
2. Install dependencies
npm install @supabase/supabase-js @anthropic-ai/sdk recharts
3. Set up environment variables
# .env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
ANTHROPIC_API_KEY=your_claude_api_key
4. Set up Supabase
- Create a project at supabase.com
- Run the SQL schema from above in the SQL Editor
- Enable Row Level Security (RLS)
- Set up email/password authentication
5. Run locally
npm run dev
User Experience Flow
First Visit
- Sign up / Log in
- Take a quick level assessment (5 questions)
- App assigns you: Beginner / Intermediate / Advanced
- Day 1 lesson is immediately unlocked
Daily Experience
- Open the app → see today’s lesson
- Warmup (2 min): Learn 3-5 new vocabulary words
- Main Exercise (8 min): Complete a scenario-based exercise
- Speaking Drill (3 min): Practice saying phrases out loud
- Reflection (2 min): Answer a reflection question
- Get your score + AI feedback
- Tomorrow’s lesson unlocks
If You Miss a Day
- Streak resets (motivates consistency)
- You can still access your current lesson
- Previous lessons remain available for review
- No penalty — just encouragement to continue
Estimated Costs
| Service | Free Tier | Estimated Monthly |
|---|---|---|
| Vercel | 100GB bandwidth | $0 |
| Supabase | 50K API calls, 500MB DB | $0 |
| Claude API | ~30 lessons × $0.01 | ~$0.30 |
| Total | ~$0.30/month |
For personal use, this app is essentially free.
What You’ll Learn Building This
Beyond English practice, building this app teaches you:
- Next.js App Router with server components
- Supabase authentication and database
- AI integration with Claude API
- Streak/gamification mechanics
- Adaptive learning algorithms
It’s an English learning app that also teaches you modern web development. Win-win.
Next up: Part 11 — English for Client Interviews & Project Bidding — frameworks and scripts for winning new projects, first client meetings, and bid presentations.