Essential React Components & Services
Frontend Components
1. Authentication Component (src/components/auth/Login.js)
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AuthService } from '../../services/authService';
import './Login.css';
const Login = ({ onLogin }) => {
const [formData, setFormData] = useState({
username: '',
password: '',
isRegistering: false,
full_name: '',
grade_level: '',
role: 'student'
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const { t } = useTranslation();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
let result;
if (formData.isRegistering) {
result = await AuthService.register(formData);
} else {
result = await AuthService.login(formData.username, formData.password);
}
localStorage.setItem('token', result.token);
onLogin(result.user);
} catch (error) {
setError(error.message || t('login_error'));
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<div className="login-container">
<div className="login-card">
<h2>{formData.isRegistering ? t('register') : t('login')}</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
placeholder={t('username')}
value={formData.username}
onChange={handleInputChange}
required
/>
<input
type="password"
name="password"
placeholder={t('password')}
value={formData.password}
onChange={handleInputChange}
required
/>
{formData.isRegistering && (
<>
<input
type="text"
name="full_name"
placeholder={t('full_name')}
value={formData.full_name}
onChange={handleInputChange}
required
/>
<select
name="role"
value={formData.role}
onChange={handleInputChange}
>
<option value="student">{t('student')}</option>
<option value="teacher">{t('teacher')}</option>
</select>
{formData.role === 'student' && (
<select
name="grade_level"
value={formData.grade_level}
onChange={handleInputChange}
required
>
<option value="">{t('select_grade')}</option>
{[1,2,3,4,5,6,7,8,9,10].map(grade => (
<option key={grade} value={grade.toString()}>{t('grade')} {grade}</op
))}
</select>
)}
</>
)}
<button type="submit" disabled={loading} className="submit-btn">
{loading ? t('loading') : (formData.isRegistering ? t('register') : t('login'
</button>
</form>
<button
type="button"
onClick={() => setFormData({...formData, isRegistering: !formData.isRegistering
className="toggle-btn"
>
{formData.isRegistering ? t('already_have_account') : t('create_account')}
</button>
{/* Demo Credentials */}
<div className="demo-credentials">
<h4>{t('demo_accounts')}:</h4>
<p><strong>{t('student')}:</strong> student / student123</p>
<p><strong>{t('teacher')}:</strong> teacher / teacher123</p>
</div>
</div>
</div>
);
};
export default Login;
2. Student Dashboard (src/components/student/StudentDashboard.js)
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ContentService } from '../../services/contentService';
import { OfflineStorage } from '../../services/offlineStorage';
import VoiceAssistant from './VoiceAssistant';
import ProgressTracker from './ProgressTracker';
import './StudentDashboard.css';
const StudentDashboard = ({ user }) => {
const [lessons, setLessons] = useState([]);
const [progress, setProgress] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedSubject, setSelectedSubject] = useState('all');
const { t, i18n } = useTranslation();
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
setLoading(true);
try {
// Try to load from cache first (offline support)
let lessonsData = await OfflineStorage.getLessons();
let progressData = await OfflineStorage.getProgress(user.id);
if (!lessonsData.length) {
// If no cached data, fetch from API
lessonsData = await ContentService.getLessons({ grade_level: user.grade_level });
progressData = await ContentService.getProgress();
// Cache the data
await OfflineStorage.saveLessons(lessonsData);
await OfflineStorage.saveProgress(progressData);
}
setLessons(lessonsData);
setProgress(progressData);
} catch (error) {
console.error('Error loading dashboard:', error);
// Load from offline storage if API fails
const cachedLessons = await OfflineStorage.getLessons();
const cachedProgress = await OfflineStorage.getProgress(user.id);
setLessons(cachedLessons);
setProgress(cachedProgress);
} finally {
setLoading(false);
}
};
const getLocalizedContent = (lesson, field) => {
const lang = i18n.language;
if (lang === 'punjabi' && lesson[`${field}_punjabi`]) {
return lesson[`${field}_punjabi`];
}
if (lang === 'hindi' && lesson[`${field}_hindi`]) {
return lesson[`${field}_hindi`];
}
return lesson[field];
};
const getLessonProgress = (lessonId) => {
const lessonProgress = progress.find(p => p.lesson_id === lessonId);
return lessonProgress ? lessonProgress.completion_percentage : 0;
};
const filteredLessons = selectedSubject === 'all'
? lessons
: lessons.filter(lesson => lesson.subject === selectedSubject);
const subjects = [...new Set(lessons.map(lesson => lesson.subject))];
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>{t('loading_dashboard')}</p>
</div>
);
}
return (
<div className="student-dashboard">
<div className="dashboard-header">
<h2>{t('welcome_message', { name: user.full_name })}</h2>
<div className="dashboard-stats">
<div className="stat-card">
<h3>{lessons.length}</h3>
<p>{t('total_lessons')}</p>
</div>
<div className="stat-card">
<h3>{progress.length}</h3>
<p>{t('lessons_started')}</p>
</div>
<div className="stat-card">
<h3>{progress.filter(p => p.completion_percentage >= 100).length}</h3>
<p>{t('lessons_completed')}</p>
</div>
</div>
</div>
<div className="dashboard-filters">
<select
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
className="subject-filter"
>
<option value="all">{t('all_subjects')}</option>
{subjects.map(subject => (
<option key={subject} value={subject}>{t(subject)}</option>
))}
</select>
</div>
<div className="lessons-grid">
{filteredLessons.map(lesson => (
<div key={lesson.id} className="lesson-card">
<div className="lesson-header">
<h3>{getLocalizedContent(lesson, 'title')}</h3>
<span className="subject-badge">{t(lesson.subject)}</span>
</div>
<p className="lesson-description">
{getLocalizedContent(lesson, 'content')}
</p>
<div className="lesson-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${getLessonProgress(lesson.id)}%` }}
></div>
</div>
<span>{Math.round(getLessonProgress(lesson.id))}% {t('complete')}</span>
</div>
<Link to={`/lesson/${lesson.id}`} className="start-lesson-btn">
{getLessonProgress(lesson.id) > 0 ? t('continue_lesson') : t('start_lesson'
</Link>
</div>
))}
</div>
{filteredLessons.length === 0 && (
<div className="no-lessons">
<p>{t('no_lessons_available')}</p>
</div>
)}
<ProgressTracker progress={progress} />
<VoiceAssistant />
</div>
);
};
export default StudentDashboard;
3. Lesson Viewer (src/components/student/LessonViewer.js)
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ContentService } from '../../services/contentService';
import { OfflineStorage } from '../../services/offlineStorage';
import QuizComponent from './QuizComponent';
import './LessonViewer.css';
const LessonViewer = ({ user }) => {
const { id } = useParams();
const navigate = useNavigate();
const [lesson, setLesson] = useState(null);
const [loading, setLoading] = useState(true);
const [currentSection, setCurrentSection] = useState('content');
const [startTime] = useState(Date.now());
const [progress, setProgress] = useState(0);
const { t, i18n } = useTranslation();
useEffect(() => {
loadLesson();
}, [id]);
const loadLesson = async () => {
setLoading(true);
try {
// Try offline first
let lessonData = await OfflineStorage.getLesson(id);
if (!lessonData) {
// Fetch from API if not cached
lessonData = await ContentService.getLesson(id);
await OfflineStorage.saveLesson(lessonData);
}
setLesson(lessonData);
} catch (error) {
console.error('Error loading lesson:', error);
} finally {
setLoading(false);
}
};
const getLocalizedContent = (field) => {
if (!lesson) return '';
const lang = i18n.language;
if (lang === 'punjabi' && lesson[`${field}_punjabi`]) {
return lesson[`${field}_punjabi`];
}
if (lang === 'hindi' && lesson[`${field}_hindi`]) {
return lesson[`${field}_hindi`];
}
return lesson[field];
};
const handleProgressUpdate = async (newProgress, quizScores = null) => {
const timeSpent = Math.floor((Date.now() - startTime) / 1000);
const progressData = {
lesson_id: lesson.id,
completion_percentage: newProgress,
quiz_scores: quizScores,
time_spent: timeSpent
};
try {
// Save online if possible
await ContentService.saveProgress(progressData);
} catch (error) {
// Save offline if online fails
await OfflineStorage.saveProgressOffline(user.id, progressData);
}
setProgress(newProgress);
};
const handleQuizComplete = (scores) => {
const quizCompletion = scores.correct / scores.total * 100;
const totalProgress = Math.max(progress, 50 + (quizCompletion / 2));
handleProgressUpdate(totalProgress, scores);
if (totalProgress >= 100) {
setTimeout(() => {
navigate('/');
}, 2000);
}
};
const markContentComplete = () => {
handleProgressUpdate(Math.max(progress, 50));
setCurrentSection('quiz');
};
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>{t('loading_lesson')}</p>
</div>
);
}
if (!lesson) {
return (
<div className="error-container">
<p>{t('lesson_not_found')}</p>
<button onClick={() => navigate('/')}>{t('go_back')}</button>
</div>
);
}
return (
<div className="lesson-viewer">
<div className="lesson-header">
<button onClick={() => navigate('/')} className="back-btn">
← {t('back_to_dashboard')}
</button>
<h1>{getLocalizedContent('title')}</h1>
<div className="lesson-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
></div>
</div>
<span>{Math.round(progress)}% {t('complete')}</span>
</div>
</div>
<div className="lesson-navigation">
<button
className={`nav-btn ${currentSection === 'content' ? 'active' : ''}`}
onClick={() => setCurrentSection('content')}
>
{t('lesson_content')}
</button>
<button
className={`nav-btn ${currentSection === 'quiz' ? 'active' : ''}`}
onClick={() => setCurrentSection('quiz')}
disabled={progress < 50}
>
{t('quiz')}
</button>
</div>
<div className="lesson-content">
{currentSection === 'content' && (
<div className="content-section">
<div className="lesson-text">
<p>{getLocalizedContent('content')}</p>
</div>
{/* Add multimedia content here */}
{lesson.media_files && (
<div className="media-content">
{/* This would include images, videos, etc. */}
</div>
)}
<div className="content-actions">
<button
onClick={markContentComplete}
className="complete-content-btn"
disabled={progress >= 50}
>
{progress >= 50 ? t('content_completed') : t('mark_complete')}
</button>
</div>
</div>
)}
{currentSection === 'quiz' && lesson.quiz && (
<QuizComponent
quiz={lesson.quiz}
language={i18n.language}
onComplete={handleQuizComplete}
/>
)}
{currentSection === 'quiz' && !lesson.quiz && (
<div className="no-quiz">
<p>{t('no_quiz_available')}</p>
<button onClick={() => handleProgressUpdate(100)}>
{t('complete_lesson')}
</button>
</div>
)}
</div>
</div>
);
};
export default LessonViewer;
4. Services
Authentication Service (src/services/authService.js)
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
export class AuthService {
static async login(username, password) {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return response.json();
}
static async register(userData) {
const response = await fetch(`${API_BASE_URL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return response.json();
}
static async verifyToken(token) {
// Decode JWT token (simple version)
try {
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.exp * 1000 < Date.now()) {
throw new Error('Token expired');
}
return payload;
} catch (error) {
throw new Error('Invalid token');
}
}
static logout() {
localStorage.removeItem('token');
}
static getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
}
}
Offline Storage Service (src/services/offlineStorage.js)
class OfflineStorageService {
constructor() {
this.dbName = 'RuralLearningDB';
this.version = 1;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create lessons store
if (!db.objectStoreNames.contains('lessons')) {
const lessonsStore = db.createObjectStore('lessons', { keyPath: 'id' });
lessonsStore.createIndex('subject', 'subject', { unique: false });
lessonsStore.createIndex('grade_level', 'grade_level', { unique: false });
}
// Create progress store
if (!db.objectStoreNames.contains('progress')) {
const progressStore = db.createObjectStore('progress', { keyPath: 'id', autoInc
progressStore.createIndex('user_id', 'user_id', { unique: false });
progressStore.createIndex('lesson_id', 'lesson_id', { unique: false });
}
// Create offline progress store
if (!db.objectStoreNames.contains('offline_progress')) {
const offlineProgressStore = db.createObjectStore('offline_progress', { keyPath
offlineProgressStore.createIndex('user_id', 'user_id', { unique: false });
offlineProgressStore.createIndex('synced', 'synced', { unique: false });
}
};
});
}
async saveLessons(lessons) {
const transaction = this.db.transaction(['lessons'], 'readwrite');
const store = transaction.objectStore('lessons');
for (const lesson of lessons) {
await store.put(lesson);
}
}
async getLessons(filters = {}) {
const transaction = this.db.transaction(['lessons'], 'readonly');
const store = transaction.objectStore('lessons');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => {
let lessons = request.result;
// Apply filters
if (filters.grade_level) {
lessons = lessons.filter(lesson => lesson.grade_level === filters.grade_level);
}
if (filters.subject) {
lessons = lessons.filter(lesson => lesson.subject === filters.subject);
}
resolve(lessons);
};
request.onerror = () => reject(request.error);
});
}
async getLesson(id) {
const transaction = this.db.transaction(['lessons'], 'readonly');
const store = transaction.objectStore('lessons');
return new Promise((resolve, reject) => {
const request = store.get(parseInt(id));
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async saveLesson(lesson) {
const transaction = this.db.transaction(['lessons'], 'readwrite');
const store = transaction.objectStore('lessons');
return store.put(lesson);
}
async saveProgress(progressData) {
const transaction = this.db.transaction(['progress'], 'readwrite');
const store = transaction.objectStore('progress');
for (const progress of progressData) {
await store.put(progress);
}
}
async getProgress(userId) {
const transaction = this.db.transaction(['progress'], 'readonly');
const store = transaction.objectStore('progress');
const index = store.index('user_id');
return new Promise((resolve, reject) => {
const request = index.getAll(userId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async saveProgressOffline(userId, progressData) {
const transaction = this.db.transaction(['offline_progress'], 'readwrite');
const store = transaction.objectStore('offline_progress');
const offlineProgress = {
...progressData,
user_id: userId,
synced: false,
timestamp: Date.now()
};
return store.add(offlineProgress);
}
async getOfflineProgress() {
const transaction = this.db.transaction(['offline_progress'], 'readonly');
const store = transaction.objectStore('offline_progress');
const index = store.index('synced');
return new Promise((resolve, reject) => {
const request = index.getAll(false);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async markProgressSynced(progressIds) {
const transaction = this.db.transaction(['offline_progress'], 'readwrite');
const store = transaction.objectStore('offline_progress');
for (const id of progressIds) {
const progress = await store.get(id);
if (progress) {
progress.synced = true;
await store.put(progress);
}
}
}
}
export const OfflineStorage = new OfflineStorageService();
5. Language Configuration (src/i18n/i18n.js)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Translation resources
const resources = {
english: {
translation: {
app_title: "Rural Learning Platform",
login: "Login",
register: "Register",
username: "Username",
password: "Password",
full_name: "Full Name",
student: "Student",
teacher: "Teacher",
grade: "Grade",
select_grade: "Select Grade",
welcome_message: "Welcome, {{name}}!",
total_lessons: "Total Lessons",
lessons_started: "Lessons Started",
lessons_completed: "Lessons Completed",
all_subjects: "All Subjects",
mathematics: "Mathematics",
language: "Language",
science: "Science",
start_lesson: "Start Lesson",
continue_lesson: "Continue Lesson",
complete: "Complete",
loading: "Loading...",
offline: "Offline",
online: "Online",
sync_pending: "Sync Pending",
logout: "Logout"
}
},
punjabi: {
translation: {
app_title: "ਪੇਂਡੂ ਸਿੱਖਿਆ ਪਲੇਟਫਾਰਮ",
login: "ਲਾਗਇਨ",
register: "ਰਜਿਸਟਰ",
username: "ਯੂਜ਼ਰ ਨਾਮ",
password: "ਪਾਸਵਰਡ",
full_name: "ਪੂਰਾ ਨਾਮ",
student: "ਵਿਦਿਆਰਥੀ",
teacher: "ਅਧਿਆਪਕ",
grade: "ਜਮਾਤ",
select_grade: "ਜਮਾਤ ਚੁਣੋ",
welcome_message: "ਜੀ ਆਇਆਂ ਨੂੰ, {{name}}!",
total_lessons: "ਕੁੱਲ ਪਾਠ",
lessons_started: "ਸ਼ੁਰੂ ਕੀਤੇ ਪਾਠ",
lessons_completed: "ਪੂਰੇ ਕੀਤੇ ਪਾਠ",
all_subjects: "ਸਾਰੇ ਵਿਸ਼ੇ",
mathematics: "ਗਣਿਤ",
language: "ਭਾਸ਼ਾ",
science: "ਵਿਗਿਆਨ",
start_lesson: "ਪਾਠ ਸ਼ੁਰੂ ਕਰੋ",
continue_lesson: "ਪਾਠ ਜਾਰੀ ਰੱਖੋ",
complete: "ਪੂਰਾ",
loading: "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
offline: "ਔਫਲਾਈਨ",
online: "ਔਨਲਾਈਨ",
sync_pending: "ਸਿੰਕ ਬਾਕੀ ਹੈ",
logout: "ਲਾਗਆਊਟ"
}
},
hindi: {
translation: {
app_title: "ग्रामीण शिक्षा प्लेटफॉर्म",
login: "लॉगिन",
register: "रजिस्टर",
username: "उपयोगकर्ता नाम",
password: "पासवर्ड",
full_name: "पूरा नाम",
student: "छात्र",
teacher: "अध्यापक",
grade: "कक्षा",
select_grade: "कक्षा चुनें",
welcome_message: "स्वागत है, {{name}}!",
total_lessons: "कु ल पाठ",
lessons_started: "शुरू किए गए पाठ",
lessons_completed: "पूरे किए गए पाठ",
all_subjects: "सभी विषय",
mathematics: "गणित",
language: "भाषा",
science: "विज्ञान",
start_lesson: "पाठ शुरू करें",
continue_lesson: "पाठ जारी रखें",
complete: "पूर्ण",
loading: "लोड हो रहा है...",
offline: "ऑफलाइन",
online: "ऑनलाइन",
sync_pending: "सिंक बकाया है",
logout: "लॉगआउट"
}
}
};
i18n
.use(initReactI18next)
.init({
resources,
lng: 'punjabi', // default language
fallbackLng: 'english',
interpolation: {
escapeValue: false
}
});
export default i18n;
6. Service Worker (public/sw.js)
const CACHE_NAME = 'rural-learning-v1';
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
'/manifest.json'
];
// Install service worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
// Fetch event
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
// Background sync for offline progress
self.addEventListener('sync', (event) => {
if (event.tag === 'progress-sync') {
event.waitUntil(syncOfflineProgress());
}
});
async function syncOfflineProgress() {
// This would sync offline progress data when connection is restored
console.log('Syncing offline progress...');
}