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
- Go to app.23blocks.com
- Click "Create App"
- Name it:
my-elearning-platform - Environment:
Development - Click "Create"
1.2 Add Required Blocks
Navigate to your app's Blocks tab and add these blocks:
| Block | Purpose |
|---|---|
| Auth | User accounts, login, SSO |
| Onboarding | Welcome flows, activation emails |
| Files | Video storage, course materials |
| University | Courses, lessons, progress |
| Rewards | Badges, points, gamification |
| Sales | Subscriptions, 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:
| Setting | Value |
|---|---|
| Password minimum length | 8 |
| Require uppercase | Yes |
| Require number | Yes |
| Session duration | 7 days |
| Allow registration | Yes |
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:
| Setting | Value |
|---|---|
| Progress tracking | Enabled |
| Completion certificates | Enabled |
| Quiz passing score | 70% |
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:
| Badge | Trigger | Points |
|---|---|---|
| First Steps | Complete first lesson | 50 |
| Quick Learner | Complete 5 lessons in a day | 100 |
| Course Master | Complete a course | 200 |
| Quiz Wizard | Score 100% on a quiz | 150 |
| Streak Champion | 7-day learning streak | 300 |
Point Actions:
| Action | Points |
|---|---|
| lesson_complete | 10 |
| quiz_pass | 25 |
| course_complete | 100 |
| daily_login | 5 |
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
- Go to App → Blocks → Sales → Settings
- Add your Stripe API keys:
- Publishable Key:
pk_test_xxxxx - Secret Key:
sk_test_xxxxx
- Publishable Key:
5.2 Create Subscription Plans
In the Sales block dashboard, create:
| Plan | Price | Features |
|---|---|---|
| Free | $0/mo | 3 free courses, basic quizzes |
| Pro | $19/mo | All courses, certificates, priority support |
| Team | $49/mo | Pro 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
-
Register a new user
- Visit
/register - Create an account
- Check email for verification
- Visit
-
Browse courses
- Visit
/courses - View course details
- Visit
-
Enroll and learn
- Subscribe to a plan
- Enroll in a course
- Complete lessons
- Take quizzes
-
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
| Feature | Blocks to Add |
|---|---|
| Discussion forums | Content + Real Time |
| Live classes | Real Time |
| Instructor dashboard | Companies (roles) |
| Mobile app | Same blocks, React Native |
| Advanced search | Search |
| AI recommendations | AI/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.)