Skip to main content

Build an eLearning Platform with Gamification

Build a complete online learning platform with courses, progress tracking, gamification, and payments. This tutorial covers everything from setup to a production-ready application.

Time: 30 minutes Difficulty: Intermediate Blocks Used: Auth, Onboarding, Files, University, Rewards, Sales


What You'll Build

A fully-featured eLearning platform with:

  • User Authentication - Registration, login, SSO, password reset
  • Course Catalog - Browse and search available courses
  • Video Lessons - Stream video content with progress tracking
  • Quizzes - Test knowledge with interactive assessments
  • Gamification - Earn badges, points, and climb leaderboards
  • Payments - Purchase courses or subscribe for access
  • Progress Dashboard - Track completion and achievements
┌─────────────────────────────────────────────────────────────────┐
│ eLearning Platform │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Auth │ │University│ │ Rewards │ │ Sales │ │
│ │ │ │ │ │ │ │ │ │
│ │ Users │ │ Courses │ │ Badges │ │ Plans │ │
│ │ SSO │ │ Lessons │ │ Points │ │ Stripe │ │
│ │ MFA │ │ Quizzes │ │ Boards │ │ Invoices│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │Onboarding│ │ Files │ │
│ │ │ │ │ │
│ │ Welcome │ │ Videos │ │
│ │ Emails │ │ PDFs │ │
│ │ Flows │ │ Images │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Prerequisites

Before starting, make sure you have:

  • ✅ A 23blocks account (Sign up)
  • ✅ Basic knowledge of JavaScript/TypeScript
  • ✅ Node.js 18+ installed
  • ✅ A Stripe account (for payments)

Part 1: Project Setup

1.1 Create Your App

  1. Go to app.23blocks.com
  2. Click "Create App"
  3. Name it: my-elearning-platform
  4. Environment: Development
  5. Click "Create"

1.2 Add Required Blocks

Navigate to your app's Blocks tab and add these blocks:

BlockPurpose
AuthUser accounts, login, SSO
OnboardingWelcome flows, activation emails
FilesVideo storage, course materials
UniversityCourses, lessons, progress
RewardsBadges, points, gamification
SalesSubscriptions, payments

1.3 Get Your Credentials

From your app's API Keys tab, copy:

APP_ID=app_xxxxxxxxxxxx
API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
BASE_URL=https://api.23blocks.com/v1

1.4 Initialize Your Project

# Create a new project (React example)
npx create-react-app my-elearning-app --template typescript
cd my-elearning-app

# Install the SDK
npm install @23blocks/sdk

# Create environment file
cat > .env << EOF
REACT_APP_BLOCKS_URL=https://api.23blocks.com/v1
REACT_APP_BLOCKS_APP_ID=app_xxxxxxxxxxxx
REACT_APP_BLOCKS_API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
EOF

1.5 Create the Client

Create src/lib/blocks.ts:

import { BlocksClient } from '@23blocks/sdk';

export const blocks = new BlocksClient({
baseUrl: process.env.REACT_APP_BLOCKS_URL!,
appId: process.env.REACT_APP_BLOCKS_APP_ID!,
apiKey: process.env.REACT_APP_BLOCKS_API_KEY!,
});

// For authenticated requests (after login)
export const createAuthenticatedClient = (sessionToken: string) => {
return new BlocksClient({
baseUrl: process.env.REACT_APP_BLOCKS_URL!,
appId: process.env.REACT_APP_BLOCKS_APP_ID!,
sessionToken,
});
};

Part 2: User Authentication

2.1 Configure the Auth Block

In your dashboard, go to App → Blocks → Auth → Settings:

SettingValue
Password minimum length8
Require uppercaseYes
Require numberYes
Session duration7 days
Allow registrationYes

2.2 Registration Component

Create src/components/auth/Register.tsx:

import { useState } from 'react';
import { blocks } from '../../lib/blocks';

interface RegisterForm {
email: string;
password: string;
firstName: string;
lastName: string;
}

export function Register() {
const [form, setForm] = useState<RegisterForm>({
email: '',
password: '',
firstName: '',
lastName: '',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
const user = await blocks.auth.register({
email: form.email,
password: form.password,
profile: {
firstName: form.firstName,
lastName: form.lastName,
},
});

// Redirect to onboarding
window.location.href = '/onboarding';
} catch (err: any) {
setError(err.message || 'Registration failed');
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-2xl font-bold">Create Your Account</h2>

{error && (
<div className="bg-red-100 text-red-700 p-3 rounded">
{error}
</div>
)}

<div className="grid grid-cols-2 gap-4">
<input
type="text"
placeholder="First Name"
value={form.firstName}
onChange={(e) => setForm({ ...form, firstName: e.target.value })}
className="border p-2 rounded"
required
/>
<input
type="text"
placeholder="Last Name"
value={form.lastName}
onChange={(e) => setForm({ ...form, lastName: e.target.value })}
className="border p-2 rounded"
required
/>
</div>

<input
type="email"
placeholder="Email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="border p-2 rounded w-full"
required
/>

<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="border p-2 rounded w-full"
required
/>

<button
type="submit"
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded w-full"
>
{loading ? 'Creating Account...' : 'Sign Up'}
</button>
</form>
);
}

2.3 Login Component

Create src/components/auth/Login.tsx:

import { useState } from 'react';
import { blocks } from '../../lib/blocks';

export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
const session = await blocks.auth.login({ email, password });

// Store the session token
localStorage.setItem('session_token', session.token);
localStorage.setItem('user', JSON.stringify(session.user));

// Redirect to dashboard
window.location.href = '/dashboard';
} catch (err: any) {
setError(err.message || 'Login failed');
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-2xl font-bold">Welcome Back</h2>

{error && (
<div className="bg-red-100 text-red-700 p-3 rounded">
{error}
</div>
)}

<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 rounded w-full"
required
/>

<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border p-2 rounded w-full"
required
/>

<button
type="submit"
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded w-full"
>
{loading ? 'Signing In...' : 'Sign In'}
</button>

<a href="/forgot-password" className="text-blue-600 text-sm">
Forgot password?
</a>
</form>
);
}

2.4 Auth Context

Create src/contexts/AuthContext.tsx:

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createAuthenticatedClient } from '../lib/blocks';

interface User {
id: string;
email: string;
profile: {
firstName: string;
lastName: string;
};
}

interface AuthContextType {
user: User | null;
client: ReturnType<typeof createAuthenticatedClient> | null;
loading: boolean;
logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [client, setClient] = useState<ReturnType<typeof createAuthenticatedClient> | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const token = localStorage.getItem('session_token');
const userData = localStorage.getItem('user');

if (token && userData) {
setUser(JSON.parse(userData));
setClient(createAuthenticatedClient(token));
}
setLoading(false);
}, []);

const logout = () => {
localStorage.removeItem('session_token');
localStorage.removeItem('user');
setUser(null);
setClient(null);
window.location.href = '/login';
};

return (
<AuthContext.Provider value={{ user, client, loading, logout }}>
{children}
</AuthContext.Provider>
);
}

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};

Part 3: Course Catalog (University Block)

3.1 Configure the University Block

In your dashboard, go to App → Blocks → University → Settings:

SettingValue
Progress trackingEnabled
Completion certificatesEnabled
Quiz passing score70%

3.2 Create Sample Courses (Dashboard)

In the University block dashboard, create:

Course 1: JavaScript Fundamentals

  • Lesson 1: Variables and Data Types
  • Lesson 2: Functions and Scope
  • Lesson 3: Arrays and Objects
  • Quiz: JavaScript Basics

Course 2: React Essentials

  • Lesson 1: Components and JSX
  • Lesson 2: State and Props
  • Lesson 3: Hooks
  • Quiz: React Fundamentals

3.3 Course Catalog Component

Create src/components/courses/CourseCatalog.tsx:

import { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { blocks } from '../../lib/blocks';

interface Course {
id: string;
title: string;
description: string;
thumbnail: string;
duration: string;
lessonsCount: number;
price: number;
instructor: {
name: string;
avatar: string;
};
}

export function CourseCatalog() {
const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(true);
const { client } = useAuth();

useEffect(() => {
loadCourses();
}, []);

const loadCourses = async () => {
try {
const response = await blocks.university.courses.list({
status: 'published',
limit: 20,
});
setCourses(response.data);
} catch (error) {
console.error('Failed to load courses:', error);
} finally {
setLoading(false);
}
};

if (loading) {
return <div className="text-center py-10">Loading courses...</div>;
}

return (
<div className="max-w-6xl mx-auto px-4">
<h1 className="text-3xl font-bold mb-8">Explore Courses</h1>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{courses.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</div>
</div>
);
}

function CourseCard({ course }: { course: Course }) {
return (
<a
href={`/courses/${course.id}`}
className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<img
src={course.thumbnail}
alt={course.title}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="font-bold text-lg mb-2">{course.title}</h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{course.description}
</p>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">
{course.lessonsCount} lessons • {course.duration}
</span>
<span className="font-bold text-green-600">
${course.price}
</span>
</div>
</div>
</a>
);
}

3.4 Course Player Component

Create src/components/courses/CoursePlayer.tsx:

import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';

interface Lesson {
id: string;
title: string;
type: 'video' | 'text' | 'quiz';
duration: string;
completed: boolean;
videoUrl?: string;
content?: string;
}

interface CourseDetails {
id: string;
title: string;
lessons: Lesson[];
progress: number;
}

export function CoursePlayer() {
const { courseId } = useParams<{ courseId: string }>();
const { client } = useAuth();
const [course, setCourse] = useState<CourseDetails | null>(null);
const [currentLesson, setCurrentLesson] = useState<Lesson | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadCourse();
}, [courseId]);

const loadCourse = async () => {
if (!client || !courseId) return;

try {
const response = await client.university.courses.get(courseId, {
include: ['lessons', 'progress'],
});
setCourse(response.data);
setCurrentLesson(response.data.lessons[0]);
} catch (error) {
console.error('Failed to load course:', error);
} finally {
setLoading(false);
}
};

const markLessonComplete = async (lessonId: string) => {
if (!client || !courseId) return;

try {
await client.university.progress.complete(courseId, lessonId);

// Update local state
setCourse((prev) => {
if (!prev) return prev;
return {
...prev,
lessons: prev.lessons.map((l) =>
l.id === lessonId ? { ...l, completed: true } : l
),
progress: prev.progress + (100 / prev.lessons.length),
};
});

// Award points for completion
await client.rewards.points.award({
action: 'lesson_complete',
points: 10,
metadata: { lessonId, courseId },
});
} catch (error) {
console.error('Failed to mark lesson complete:', error);
}
};

if (loading || !course) {
return <div className="text-center py-10">Loading course...</div>;
}

return (
<div className="flex h-screen">
{/* Sidebar - Lesson List */}
<aside className="w-80 bg-gray-100 overflow-y-auto">
<div className="p-4">
<h2 className="font-bold text-lg mb-4">{course.title}</h2>

{/* Progress Bar */}
<div className="mb-4">
<div className="flex justify-between text-sm mb-1">
<span>Progress</span>
<span>{Math.round(course.progress)}%</span>
</div>
<div className="h-2 bg-gray-300 rounded-full">
<div
className="h-2 bg-green-500 rounded-full transition-all"
style={{ width: `${course.progress}%` }}
/>
</div>
</div>

{/* Lesson List */}
<ul className="space-y-2">
{course.lessons.map((lesson, index) => (
<li key={lesson.id}>
<button
onClick={() => setCurrentLesson(lesson)}
className={`w-full text-left p-3 rounded-lg flex items-center gap-3 ${
currentLesson?.id === lesson.id
? 'bg-blue-100 text-blue-700'
: 'hover:bg-gray-200'
}`}
>
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-sm ${
lesson.completed
? 'bg-green-500 text-white'
: 'bg-gray-300'
}`}>
{lesson.completed ? '✓' : index + 1}
</span>
<div>
<div className="font-medium">{lesson.title}</div>
<div className="text-xs text-gray-500">{lesson.duration}</div>
</div>
</button>
</li>
))}
</ul>
</div>
</aside>

{/* Main Content */}
<main className="flex-1 overflow-y-auto">
{currentLesson && (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">{currentLesson.title}</h1>

{currentLesson.type === 'video' && (
<div className="aspect-video bg-black rounded-lg mb-4">
<video
src={currentLesson.videoUrl}
controls
className="w-full h-full"
onEnded={() => markLessonComplete(currentLesson.id)}
/>
</div>
)}

{currentLesson.type === 'text' && (
<div
className="prose max-w-none mb-4"
dangerouslySetInnerHTML={{ __html: currentLesson.content || '' }}
/>
)}

{!currentLesson.completed && (
<button
onClick={() => markLessonComplete(currentLesson.id)}
className="bg-green-600 text-white px-6 py-2 rounded-lg"
>
Mark as Complete
</button>
)}
</div>
)}
</main>
</div>
);
}

Part 4: Gamification (Rewards Block)

4.1 Configure the Rewards Block

In your dashboard, create these reward items:

Badges:

BadgeTriggerPoints
First StepsComplete first lesson50
Quick LearnerComplete 5 lessons in a day100
Course MasterComplete a course200
Quiz WizardScore 100% on a quiz150
Streak Champion7-day learning streak300

Point Actions:

ActionPoints
lesson_complete10
quiz_pass25
course_complete100
daily_login5

4.2 Leaderboard Component

Create src/components/rewards/Leaderboard.tsx:

import { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';

interface LeaderboardEntry {
rank: number;
userId: string;
userName: string;
avatar: string;
points: number;
badges: number;
}

export function Leaderboard() {
const { client, user } = useAuth();
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
const [userRank, setUserRank] = useState<LeaderboardEntry | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadLeaderboard();
}, [client]);

const loadLeaderboard = async () => {
if (!client) return;

try {
const response = await client.rewards.leaderboard.get({
period: 'all_time',
limit: 10,
});
setEntries(response.data.entries);
setUserRank(response.data.currentUser);
} catch (error) {
console.error('Failed to load leaderboard:', error);
} finally {
setLoading(false);
}
};

if (loading) {
return <div className="text-center py-10">Loading leaderboard...</div>;
}

return (
<div className="max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Leaderboard</h2>

{/* Top 3 */}
<div className="flex justify-center gap-4 mb-8">
{entries.slice(0, 3).map((entry, index) => (
<div
key={entry.userId}
className={`text-center ${index === 0 ? 'order-2' : index === 1 ? 'order-1' : 'order-3'}`}
>
<div className={`relative ${index === 0 ? 'scale-110' : ''}`}>
<img
src={entry.avatar}
alt={entry.userName}
className="w-16 h-16 rounded-full mx-auto border-4"
style={{ borderColor: index === 0 ? 'gold' : index === 1 ? 'silver' : '#cd7f32' }}
/>
<span className={`absolute -bottom-2 left-1/2 -translate-x-1/2 w-6 h-6 rounded-full flex items-center justify-center text-white text-sm font-bold ${
index === 0 ? 'bg-yellow-500' : index === 1 ? 'bg-gray-400' : 'bg-orange-700'
}`}>
{entry.rank}
</span>
</div>
<div className="mt-4">
<div className="font-medium">{entry.userName}</div>
<div className="text-sm text-gray-500">{entry.points.toLocaleString()} pts</div>
</div>
</div>
))}
</div>

{/* Full List */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{entries.map((entry) => (
<div
key={entry.userId}
className={`flex items-center gap-4 p-4 border-b ${
entry.userId === user?.id ? 'bg-blue-50' : ''
}`}
>
<span className="w-8 text-center font-bold text-gray-500">
{entry.rank}
</span>
<img
src={entry.avatar}
alt={entry.userName}
className="w-10 h-10 rounded-full"
/>
<div className="flex-1">
<div className="font-medium">{entry.userName}</div>
<div className="text-xs text-gray-500">{entry.badges} badges</div>
</div>
<div className="font-bold text-blue-600">
{entry.points.toLocaleString()} pts
</div>
</div>
))}
</div>

{/* Current User (if not in top 10) */}
{userRank && userRank.rank > 10 && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<div className="text-sm text-gray-500 mb-2">Your Rank</div>
<div className="flex items-center gap-4">
<span className="w-8 text-center font-bold">#{userRank.rank}</span>
<div className="flex-1 font-medium">{userRank.userName}</div>
<div className="font-bold text-blue-600">
{userRank.points.toLocaleString()} pts
</div>
</div>
</div>
)}
</div>
);
}

4.3 Badges Display Component

Create src/components/rewards/BadgesDisplay.tsx:

import { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';

interface Badge {
id: string;
name: string;
description: string;
icon: string;
earnedAt: string | null;
progress?: {
current: number;
target: number;
};
}

export function BadgesDisplay() {
const { client } = useAuth();
const [badges, setBadges] = useState<Badge[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadBadges();
}, [client]);

const loadBadges = async () => {
if (!client) return;

try {
const response = await client.rewards.badges.list({
include: ['progress'],
});
setBadges(response.data);
} catch (error) {
console.error('Failed to load badges:', error);
} finally {
setLoading(false);
}
};

if (loading) {
return <div className="text-center py-10">Loading badges...</div>;
}

const earnedBadges = badges.filter((b) => b.earnedAt);
const lockedBadges = badges.filter((b) => !b.earnedAt);

return (
<div className="max-w-4xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Your Badges</h2>

{/* Earned Badges */}
<div className="mb-8">
<h3 className="text-lg font-semibold mb-4 text-green-600">
Earned ({earnedBadges.length})
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{earnedBadges.map((badge) => (
<div
key={badge.id}
className="bg-white rounded-lg p-4 text-center shadow-md border-2 border-green-200"
>
<div className="text-4xl mb-2">{badge.icon}</div>
<div className="font-bold">{badge.name}</div>
<div className="text-xs text-gray-500">{badge.description}</div>
<div className="text-xs text-green-600 mt-2">
Earned {new Date(badge.earnedAt!).toLocaleDateString()}
</div>
</div>
))}
</div>
</div>

{/* Locked Badges */}
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-500">
Locked ({lockedBadges.length})
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{lockedBadges.map((badge) => (
<div
key={badge.id}
className="bg-gray-100 rounded-lg p-4 text-center opacity-60"
>
<div className="text-4xl mb-2 grayscale">{badge.icon}</div>
<div className="font-bold">{badge.name}</div>
<div className="text-xs text-gray-500">{badge.description}</div>

{badge.progress && (
<div className="mt-2">
<div className="h-1 bg-gray-300 rounded-full">
<div
className="h-1 bg-blue-500 rounded-full"
style={{
width: `${(badge.progress.current / badge.progress.target) * 100}%`
}}
/>
</div>
<div className="text-xs mt-1">
{badge.progress.current}/{badge.progress.target}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
);
}

Part 5: Payments (Sales Block)

5.1 Configure Stripe Integration

  1. Go to App → Blocks → Sales → Settings
  2. Add your Stripe API keys:
    • Publishable Key: pk_test_xxxxx
    • Secret Key: sk_test_xxxxx

5.2 Create Subscription Plans

In the Sales block dashboard, create:

PlanPriceFeatures
Free$0/mo3 free courses, basic quizzes
Pro$19/moAll courses, certificates, priority support
Team$49/moPro features + 5 seats, team analytics

5.3 Pricing Component

Create src/components/sales/Pricing.tsx:

import { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { blocks } from '../../lib/blocks';

interface Plan {
id: string;
name: string;
price: number;
interval: 'month' | 'year';
features: string[];
highlighted?: boolean;
}

export function Pricing() {
const { client, user } = useAuth();
const [plans, setPlans] = useState<Plan[]>([]);
const [currentPlan, setCurrentPlan] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadPlans();
}, [client]);

const loadPlans = async () => {
try {
const response = await blocks.sales.plans.list();
setPlans(response.data);

if (client) {
const subscription = await client.sales.subscriptions.current();
setCurrentPlan(subscription?.planId || null);
}
} catch (error) {
console.error('Failed to load plans:', error);
} finally {
setLoading(false);
}
};

const handleSubscribe = async (planId: string) => {
if (!client) {
window.location.href = '/login?redirect=/pricing';
return;
}

try {
const checkout = await client.sales.checkout.create({
planId,
successUrl: `${window.location.origin}/subscription/success`,
cancelUrl: `${window.location.origin}/pricing`,
});

// Redirect to Stripe Checkout
window.location.href = checkout.url;
} catch (error) {
console.error('Failed to create checkout:', error);
}
};

if (loading) {
return <div className="text-center py-10">Loading plans...</div>;
}

return (
<div className="max-w-5xl mx-auto px-4 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-4">Choose Your Plan</h1>
<p className="text-gray-600">
Start learning today. Upgrade or cancel anytime.
</p>
</div>

<div className="grid md:grid-cols-3 gap-8">
{plans.map((plan) => (
<div
key={plan.id}
className={`bg-white rounded-2xl shadow-lg p-8 ${
plan.highlighted ? 'ring-2 ring-blue-500 scale-105' : ''
}`}
>
{plan.highlighted && (
<div className="bg-blue-500 text-white text-sm font-medium px-3 py-1 rounded-full inline-block mb-4">
Most Popular
</div>
)}

<h3 className="text-xl font-bold">{plan.name}</h3>

<div className="my-4">
<span className="text-4xl font-bold">${plan.price}</span>
<span className="text-gray-500">/{plan.interval}</span>
</div>

<ul className="space-y-3 mb-8">
{plan.features.map((feature, i) => (
<li key={i} className="flex items-center gap-2">
<span className="text-green-500"></span>
{feature}
</li>
))}
</ul>

<button
onClick={() => handleSubscribe(plan.id)}
disabled={currentPlan === plan.id}
className={`w-full py-3 rounded-lg font-medium ${
currentPlan === plan.id
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
: plan.highlighted
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-900 text-white hover:bg-gray-800'
}`}
>
{currentPlan === plan.id ? 'Current Plan' : 'Get Started'}
</button>
</div>
))}
</div>
</div>
);
}

Part 6: File Uploads (Files Block)

6.1 Video Upload Component

Create src/components/admin/VideoUpload.tsx:

import { useState, useCallback } from 'react';
import { useAuth } from '../../contexts/AuthContext';

export function VideoUpload({ onUploadComplete }: { onUploadComplete: (url: string) => void }) {
const { client } = useAuth();
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);

const handleUpload = useCallback(async (file: File) => {
if (!client) return;

setUploading(true);
setError(null);
setProgress(0);

try {
const response = await client.files.upload({
file,
folder: 'course-videos',
public: false,
onProgress: (percent) => setProgress(percent),
});

onUploadComplete(response.url);
} catch (err: any) {
setError(err.message || 'Upload failed');
} finally {
setUploading(false);
}
}, [client, onUploadComplete]);

const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('video/')) {
handleUpload(file);
} else {
setError('Please upload a video file');
}
}, [handleUpload]);

return (
<div
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
className={`border-2 border-dashed rounded-lg p-8 text-center ${
uploading ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
}`}
>
{uploading ? (
<div>
<div className="text-lg mb-4">Uploading... {progress}%</div>
<div className="h-2 bg-gray-200 rounded-full max-w-xs mx-auto">
<div
className="h-2 bg-blue-500 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
) : (
<div>
<div className="text-4xl mb-4">📹</div>
<div className="text-lg mb-2">Drop your video here</div>
<div className="text-gray-500 mb-4">or</div>
<label className="bg-blue-600 text-white px-4 py-2 rounded cursor-pointer">
Choose File
<input
type="file"
accept="video/*"
className="hidden"
onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
/>
</label>
</div>
)}

{error && (
<div className="mt-4 text-red-600">{error}</div>
)}
</div>
);
}

Part 7: Putting It All Together

7.1 App Router

Create src/App.tsx:

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';

// Auth
import { Login } from './components/auth/Login';
import { Register } from './components/auth/Register';

// Main
import { CourseCatalog } from './components/courses/CourseCatalog';
import { CoursePlayer } from './components/courses/CoursePlayer';
import { Dashboard } from './components/dashboard/Dashboard';

// Rewards
import { Leaderboard } from './components/rewards/Leaderboard';
import { BadgesDisplay } from './components/rewards/BadgesDisplay';

// Sales
import { Pricing } from './components/sales/Pricing';

function PrivateRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();

if (loading) {
return <div className="flex items-center justify-center h-screen">Loading...</div>;
}

return user ? <>{children}</> : <Navigate to="/login" />;
}

function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public Routes */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/courses" element={<CourseCatalog />} />
<Route path="/pricing" element={<Pricing />} />

{/* Private Routes */}
<Route path="/dashboard" element={
<PrivateRoute><Dashboard /></PrivateRoute>
} />
<Route path="/courses/:courseId" element={
<PrivateRoute><CoursePlayer /></PrivateRoute>
} />
<Route path="/leaderboard" element={
<PrivateRoute><Leaderboard /></PrivateRoute>
} />
<Route path="/badges" element={
<PrivateRoute><BadgesDisplay /></PrivateRoute>
} />

{/* Default */}
<Route path="/" element={<Navigate to="/courses" />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}

export default App;

7.2 User Dashboard

Create src/components/dashboard/Dashboard.tsx:

import { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';

interface DashboardData {
enrolledCourses: number;
completedCourses: number;
totalPoints: number;
badges: number;
streak: number;
recentActivity: Array<{
id: string;
type: string;
title: string;
date: string;
}>;
}

export function Dashboard() {
const { client, user } = useAuth();
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadDashboard();
}, [client]);

const loadDashboard = async () => {
if (!client) return;

try {
// Fetch data from multiple blocks in parallel
const [courses, rewards, activity] = await Promise.all([
client.university.enrollments.stats(),
client.rewards.user.stats(),
client.university.activity.recent({ limit: 5 }),
]);

setData({
enrolledCourses: courses.enrolled,
completedCourses: courses.completed,
totalPoints: rewards.points,
badges: rewards.badgesCount,
streak: rewards.streak,
recentActivity: activity.data,
});
} catch (error) {
console.error('Failed to load dashboard:', error);
} finally {
setLoading(false);
}
};

if (loading || !data) {
return <div className="text-center py-10">Loading dashboard...</div>;
}

return (
<div className="max-w-6xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">
Welcome back, {user?.profile.firstName}!
</h1>

{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<StatCard label="Enrolled" value={data.enrolledCourses} icon="📚" />
<StatCard label="Completed" value={data.completedCourses} icon="" />
<StatCard label="Points" value={data.totalPoints.toLocaleString()} icon="" />
<StatCard label="Badges" value={data.badges} icon="🏆" />
<StatCard label="Day Streak" value={data.streak} icon="🔥" />
</div>

{/* Continue Learning */}
<section className="mb-8">
<h2 className="text-xl font-bold mb-4">Continue Learning</h2>
{/* Add enrolled courses list here */}
</section>

{/* Recent Activity */}
<section>
<h2 className="text-xl font-bold mb-4">Recent Activity</h2>
<div className="bg-white rounded-lg shadow">
{data.recentActivity.map((item) => (
<div key={item.id} className="p-4 border-b last:border-b-0 flex items-center gap-4">
<span className="text-2xl">
{item.type === 'lesson_complete' ? '📖' :
item.type === 'badge_earned' ? '🏆' :
item.type === 'quiz_passed' ? '✅' : '📝'}
</span>
<div className="flex-1">
<div className="font-medium">{item.title}</div>
<div className="text-sm text-gray-500">
{new Date(item.date).toLocaleDateString()}
</div>
</div>
</div>
))}
</div>
</section>
</div>
);
}

function StatCard({ label, value, icon }: { label: string; value: string | number; icon: string }) {
return (
<div className="bg-white rounded-lg shadow p-4 text-center">
<div className="text-3xl mb-2">{icon}</div>
<div className="text-2xl font-bold">{value}</div>
<div className="text-gray-500 text-sm">{label}</div>
</div>
);
}

Part 8: Testing Your Application

8.1 Test User Flow

  1. Register a new user

    • Visit /register
    • Create an account
    • Check email for verification
  2. Browse courses

    • Visit /courses
    • View course details
  3. Enroll and learn

    • Subscribe to a plan
    • Enroll in a course
    • Complete lessons
    • Take quizzes
  4. Check gamification

    • View earned badges
    • Check leaderboard position
    • Track points

8.2 Test Checklist

  • User can register and login
  • Course catalog loads correctly
  • Video lessons play
  • Progress is tracked
  • Badges are awarded
  • Points accumulate
  • Leaderboard updates
  • Payment flow works
  • Subscription is active

Next Steps

Congratulations! You've built a complete eLearning platform. Here are ideas for extending it:

Feature Ideas

FeatureBlocks to Add
Discussion forumsContent + Real Time
Live classesReal Time
Instructor dashboardCompanies (roles)
Mobile appSame blocks, React Native
Advanced searchSearch
AI recommendationsAI/Jarvis

Production Checklist

  • Switch to production API keys
  • Configure custom domain
  • Set up monitoring
  • Enable backup policies
  • Configure webhooks for events
  • Add error tracking (Sentry, etc.)

Resources