A gamified language learning platform inspired by Duolingo, featuring a hearts system, XP points, leaderboards, quests, and a shop — all built around an interactive lesson engine with AI-generated audio, sound effects, and keyboard shortcuts. Includes a Pro tier with unlimited hearts via Stripe subscriptions and a full admin dashboard using React Admin for content management. Built with Next.js 14 Server Actions, Drizzle ORM, MySQL, and Clerk authentication.
How the system is structured from frontend to external services.
Server Components for data-fetching pages (learn, shop, leaderboard, quests). Client Components for the interactive quiz engine with audio playback, keyboard shortcuts, and confetti animations. Zustand manages modal state (exit, hearts, practice). Custom button component with 13 variants for gamified UI.
Server Actions handle all mutations — challenge progress, hearts management, course switching, and Stripe checkout. React cache() wraps all database queries for request-level deduplication. Clerk provides authentication with modal-based sign-in/sign-up. Admin check compares Clerk userId against ADMIN_ID env variable.
Eight tables: courses, units, lessons, challenges, challenge_options, challenge_progress, user_progress, and user_subscription. Ordered content via position integers with cascade deletes. Relational queries with nested eager loading for lessons with challenges, options, and user-specific progress.
Clerk handles authentication with public/protected route middleware. Stripe processes Pro subscriptions ($20/month) with webhook-driven subscription lifecycle. ElevenLabs generates AI voice audio for challenge options. React Admin provides a full CRUD dashboard backed by REST API routes for content management.
Core entities and how they relate to each other.
A language course (e.g., Spanish, French) — each course has a title and flag image displayed on the course selection page
id (int, auto-increment, PK)titleimageSrc (flag image)A thematic section within a course (e.g., 'Nouns', 'Verbs') — ordered by position with a title and description shown on the unit banner
id (int, PK)titledescriptioncourseIdorder (int, position)An individual lesson within a unit — displayed as a button on the zigzag path with lock/unlock, progress, and completion states
id (int, PK)titleunitIdorder (int, position)A single question within a lesson — supports SELECT (image+audio grid) and ASSIST (text question bubble) types, ordered within the lesson
id (int, PK)lessonIdtype (enum: SELECT | ASSIST)question (text)order (int)An answer option for a challenge — can include an image (for SELECT type), audio clip (ElevenLabs), and a correct/incorrect flag
id (int, PK)challengeIdtextcorrect (boolean)imageSrc (optional)audioSrc (optional)Tracks whether a specific user has completed a specific challenge — used to calculate lesson completion percentage and practice mode detection
id (int, PK)userId (varchar)challengeIdcompleted (boolean, default: false)Central gamification state per user — tracks active course, hearts (max 5), XP points, and profile info synced from Clerk
userId (varchar, PK)userNameuserImageSrcactiveCourseIdhearts (int, default: 5)points (int, default: 0)Stripe Pro subscription record — tracks Stripe customer, subscription, and price IDs with period end for active status calculation
id (int, PK)userId (unique)stripeCustomerId (unique)stripeSubscriptionId (unique)stripePriceIdstripeCurrentPeriodEndStep-by-step walkthrough of the main user and system flows.
The hearts, XP, and practice mode systems interact in complex ways — wrong answers in practice shouldn't cost hearts, Pro users should never lose hearts, and practice should award hearts. Getting these edge cases right across multiple server actions is error-prone.
Each server action (upsertChallengeProgress, reduceHearts, refillHearts) checks the full context: existing challenge progress (practice detection), active subscription (Pro status), and current hearts count before mutating state. Practice mode is detected by checking if a ChallengeProgress record already exists for the challenge.
The quiz needs to handle multiple input methods (click and keyboard), play audio clips per option, manage complex visual states (default, selected, correct, wrong), and support two different challenge layouts — all while keeping the UI responsive.
Built with react-use hooks: useAudio for per-option audio playback, useKey for keyboard shortcuts (1-4 mapped to options), and useAudio for correct/wrong/finish sound effects. Card component manages its own audio state and visual variant based on the quiz's global status. Separate layouts for SELECT (grid) and ASSIST (single column with question bubble).
Lessons must be locked/unlocked based on completion of previous lessons, and completion requires ALL challenges in a lesson to have progress records. The zigzag path needs to show correct state for every lesson button.
The getUnits query uses Drizzle's relational loading with nested lessons → challenges → challengeProgress (filtered by userId). Each lesson is normalized with a completed boolean (true only if every challenge has a completed progress record). Lesson buttons are locked if any previous lesson in the unit is incomplete.
Subscription state must be accurate — new purchases, renewals, and expired subscriptions all need different handling. The webhook must be idempotent and the subscription check needs a grace period for failed renewals.
Webhook handles two events: checkout.session.completed (new subscription insert) and invoice.payment_succeeded (renewal update). The isActive calculation adds a 1-day buffer to stripeCurrentPeriodEnd to handle timezone edge cases and give a grace period. The createStripeUrl action intelligently routes existing customers to the billing portal instead of creating a new checkout.
LingoMaster demonstrates gamification design with a hearts/XP economy, interactive quiz engine with audio and keyboard support, Stripe subscription integration with webhook lifecycle management, and a full admin CMS via React Admin. The project showcases handling complex state interactions between practice mode, Pro subscriptions, and the hearts system — all built with Next.js 14 Server Actions, Drizzle ORM, and Clerk authentication.