Sitemap

My Story Building an Unacademy Clone with React JS

6 min readMar 29, 2024

--

Press enter or click to view image in full size
Unacademy Clone

I’ve always been inspired by edtech companies like Unacademy that are making education more accessible. When the pandemic hit and online learning became the norm, I saw an opportunity to build my own learning platform.

In this blog, I will take you through my journey of replicating key Unacademy features to build an MVP learning app. You can also checkout Zipprr Page https://zipprr.com/unacademy-clone/

Choosing Technologies

As a front-end developer, React was my obvious choice for building the user interfaces.

For the back-end, I settled on Node, Express and MongoDB.

Get Zipprr’s stories in your inbox

Join Medium for free to get updates from this writer.

I took some time to research various tech stack possibilities:

  • Frontend: React for its maturity and popularity. Vue and Svelte were other options.
  • Backend: Node/Express as I was most familiar with JavaScript backend. Also considered Python Django.
  • Database: MongoDB as document-oriented database suited the data. Also explored PostgreSQL, MySQL.
  • Hosting: Heroku for initial hosting. Planned AWS for production.
  • Payment: Stripe for simplicity of checking out and subscriptions.
  • Video: YouTube APIs for video playback and hosting content.

To evaluate the options programmatically:

// frameworks.js

const frameworks = [
{
name: 'React',
pros: ['Large community', 'Latest features'],
cons: ['Library not framework']
},
{
name: 'Vue',
pros: ['Smaller size', 'Rising in popularity'],
cons: ['More limited resources']
}
]

function scoreFramework(framework) {
let score = 0;

framework.pros.forEach(pro => {
score++
})

framework.cons.forEach(con => {
score--
})

return score
}

const bestFramework = frameworks.sort((a, b) =>
scoreFramework(b) - scoreFramework(a)
)[0]

This analysis approach helped select technologies well-suited for the requirements and my skillset.

Designing the Homepage

To design the homepage, I first created the basic page layout and UI components in React:

// Home.jsx

function Home() {
return (
<div>
<Hero Banner />
<CourseCategories />
<FeaturedInstructors />
<PopularCourses />
<FreeCourseCTA />
</div>
)
}

I then built out each component with functionality:

// HeroBanner.jsx

function HeroBanner() {

return (
<div className="hero">
<Image src="/hero-img.png" />
<Button>Get Started</Button>
</div>
)

}

Styling was added using Styled Components:

const Hero = styled.div`
display: flex;
align-items: center;
background: url(https://medium.com/$%7B(props) => props.background});

After linking to mock data, the basic homepage structure was complete. I then added responsive design using media queries.

Building the Course Catalog

To build the course catalog, I first created the Course model:

// models/Course.js

const courseSchema = new Schema({
title: String,
//...other fields
})

export default model('Course', courseSchema)

I built API routes to retrieve courses:

// courses.controller.js

export const getCourses = async (req, res) => {
try {
const courses = await Course.find();
res.json(courses);
} catch (err) {
res.status(500).json({ message: err.message });
}
}

Then displayed them in React:

// Courses.jsx

function Courses() {

const [courses, setCourses] = useState([]);

useEffect(() => {
const fetchCourses = async () => {
const res = await axios.get('/api/courses');
setCourses(res.data);
}
fetchCourses();
},[])

return (
<div>
{courses.map(course => (
<CourseItem key={course._id} course={course}/>
))}
</div>
)
}

This populated real courses on the catalog page.

Adding Search and Filters

I enabled search using MongoDB text search:

// courses.controller.js

export const searchCourses = async (req, res) => {

const { keyword } = req.query;

try {
const courses = await Course.find({
$text: { $search: keyword }
});

res.json(courses);

} catch (err) {
res.status(500).json({ message: err.message });
}

}

Filters were added with React Hook Form:

// CourseFilter.jsx

function CourseFilter() {

const { register, handleSubmit } = useForm();

const onSubmit = async (data) => {
// call api with filter data
}

return (
<form onSubmit={handleSubmit(onSubmit)}>

<Controller
name="category"
control={control}
render={({ field }) => (
<Select {...field}>
// categories
</Select>
)}
/>

<Button type="submit">
Filter
</Button>

</form>
)
}

This allowed powerful discovery of relevant courses.

Designing the Instructor Experience

I created API routes for authentication:

// auth.controller.js

export const register = async (req, res) => {

try {

// Create user from request
const user = await User.create(req.body);

// Generate JWT
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET);

res.status(200).json({ token });

} catch (err) {
res.status(400).json({ message: err.message });
}

}

And profile forms in React:

// ProfileForm.jsx

function ProfileForm() {

const { register, handleSubmit } = useForm();

const onSubmit = (data) => {
// call API to update profile
}

return (
<form onSubmit={handleSubmit(onSubmit)}>

<Controller
name="name"
control={control}
render={({ field }) => (
<Input {...field} />
)}
/>

<Button type="submit">
Update
</Button>

</form>
)

}

This allowed instructors to conveniently onboard.

Building the Instructor Dashboard

The dashboard had a common layout:

// Dashboard.jsx

function Dashboard() {
return (
<Layout>
<Sidebar />
<DashboardContent />
</Layout>
)
}

Individual pages used API data:

// MyCourses.jsx

function MyCourses() {

const [courses, setCourses] = useState([])

useEffect(() => {
const fetchCourses = async () => {
const res = await api.getCourses();
setCourses(res.data);
}

fetchCourses();

},[])

return (
// display list of courses
)

}

Navigation handled routing pages. This centralized instructor tools.

Uploading Course Content

The uploader used Amplify:

// S3.js

import Amplify, { Storage } from 'aws-amplify';

Amplify.configure({
// AWS config
});

export const uploadFile = async (file) => {

try {

const filename = file.name;

const stored = await Storage.put(filename, file);

return stored;

} catch (err) {
console.log('Error uploading file: ', err);
}

}

A React component integrated it:

// FileUploader.jsx

const FileUploader = () => {

const handleUpload = async (e) => {

const file = e.target.files[0];

await uploadFile(file);

}

return (
<div>
<input type="file" onChange={handleUpload}/>
</div>
)

}

This empowered instructors to efficiently upload content.

Managing Lessons and Materials

The lessons were stored in MongoDB:

// models/Course.js

const courseSchema = new Schema({
lessons: [{
title: String,
content: String,
//...
}]
})

A lesson editor allowed CRUD:

// LessonEditor.jsx

const LessonEditor = ({ lesson }) => {

const { register, handleSubmit } = useForm();

const onSubmit = (data) => {
// API call to update lesson
}

return (
<form onSubmit={handleSubmit(onSubmit)}>

<Controller
name="title"
control={control}
//...
/>

<Button type="submit">
Update
</Button>

</form>
)

}

Reorder used array methods:

const reorder = (list, startIndex, endIndex) => {

const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);

return result;

}

Implementing Live Classes

The Jitsi API was initialized:

// jitsi.service.js

import JitsiMeetSDK from "../../node_modules/jitsi-meet";

const domain = 'domain';
const options = {
//...
}

export const jitsi = new JitsiMeetSDK(options);

Classes were scheduled:

// ScheduleClass.js

const schedule = async () => {

// Create class doc
const classDoc = await Class.create({
//...
});

// Generate token
const token = jitsi.generateToken(meeting);

// Return class & token
res.send({class, token});

}

Student Enrollment Experience

Enrollment used React Context:

// CartProvider.js

export const CartContext = createContext();

function CartProvider({children}) {

const [cart, setCart] = useState([]);

const addToCart = (course) => {
setCart([...cart, course]);
}

return (
<CartContext.Provider value={{cart, addToCart}}>
{children}
</CartContext.Provider>
)

}

Checkout integrated Stripe payments:

// Checkout.js

const { cart, removeFromCart } = useContext(CartContext);

const handlePayment = async () => {

// Create payment intent
const paymentIntent = await createPaymentIntent();

// Confirm card payment
const { error } = await stripe.confirmCardPayment(
paymentIntent.client_secret
);

if(!error) {
// TODO: process payment
}

}

Architecture Discussion

Key services were created:

// courses.service.js

const courses = async (req, res) => {

try {

const { id } = req.params;

if(id) {
// get single course
} else {
// get all courses
}

res.send()

} catch (err) {
// error handling
}

}

module.exports = {
courses
}

A gateway handled routing:

// gateway.js

import { coursesService } from './courses.service';

app.get('/api/courses/:id?', (req, res) => {
coursesService.courses(req, res);
});

This established a scalable platform architecture.

Testing and Launching the MVP

Before launch, extensive testing was crucial to ensure quality. Some of the key testing strategies implemented were:

Unit Testing

I used Jest to unit test smaller pieces of code in isolation:

// course.service.test.js

test('GET returns courses', async () => {

// Mock course database
courses.find = jest.fn().mockResolvedValue([
// dummy course data
]);

await coursesService.getCourses();

expect(courses.find).toHaveBeenCalled();
expect(response).toEqual([/* course data */]);

});

Integration Testing

Integration tests used Supertest to test API route handler functions:

// courses.controller.test.js

test('GET /api/courses', async () => {

const response = await request(app)
.get('/api/courses')

expect(response.status).toBe(200);
expect(response.body).toEqual([/* course data */]);

});

UI Testing

Cypress ran frontend tests in a real browser:

// courses.cy.js

describe('Courses Page', () => {

it('fetches courses on mount', () => {

cy.intercept('GET','/api/courses', {
// fixture data
}).as('getCourses');

cy.visit('/courses');

cy.wait('@getCourses');

cy.get('.courses').should('have.length', 3);

});

});

Load Testing

I used Loader.io to test API response under heavy load.

Security Auditing

Snyk automated security checks found any vulnerabilities in dependencies.

Manual testing cross-checked all features across browsers/devices. Feedback from closed beta helped correct critical bugs before public launch. Monitoring with DataDog ensured stability post-launch.

Gradual rollout helped manage traffic while minimizing impact of any remaining issues for users. Overall, thorough testing helped deliver a robust and performant MVP.

--

--

Zipprr
Zipprr

Written by Zipprr

Zipprr is a leading provider of ready-made websites, mobile apps and Uber for X scripts, revolutionizing how entrepreneurs launch their online businesses.