Welcome to Zendora! This is a cross-platform React Native app designed to gamify and enrich students' learning experience through engaging quizzes, on-demand video lessons and competition amongst peers. I no longer have access to the main codebase but I have documented my sailent learning points onto this document for future reference.
Before you begin, ensure that you have the following software installed on your local machine:
- Node.js (v19.8.1)
- Yarn package manager (v1.22.19)
- React Native CLI (Follow the React Native CLI Quickstart instructions)
- Xcode (macOS only, for iOS development)
- Android Studio (for Android development)
This project was concocted by an engineering team of 10. I took charge of implementing several pivotal features that greatly enhanced the app's functionality, namely the virtual whiteboard, the rewards carousel and lastly the on-demand video lessons.
This was probably one of the most technically challenging task. The initial whiteboard implementation was easy enough - we stored a global state (i.e an array) consisting of our own in-house SVGPaths class wrappers (i.e IPath). Each IPath element contains path-specific contextual information, such as x/y coordinates, stroke width, stroke colour etc. Each time the user taps down and drags his finger across the screen, we re-render the state of the whiteboard in real-time. Though this was not very performance-efficient - since we were re-rendering all the IPath elements that were already on the screen.
To optimize this, we segment our whiteboard into 2 distinct x-y planes. The bottom x-y plane is knwon as the Display Layer, so it will simply display all the IPath elements that has been drawn before. The top x-y plane is knwown as the Drawing Layer, so this is the layer that the user will actually draw on. Now, whenever the user drags his finger across the screen, only the Drawing Layer will be re-rendered in real-time. The other paths that were previosuly drawn remain intact and static on the Display Layer. Only when the user lifts up his finger, the path rendered will be "transferred" onto the Display Layer. This reduces the amount of unneccesary re-renders, hence optimizing performance.
Another challenging part of this task was how do we persist the user canvas in our backend. The first approach we conjured was to get a snapshot of the whiteboard canvasm, export it to an image (i.e maybe a Base64 string encoding) and save in our Amazon S3 (since image storage was relatively cheap). However, one drawback of this was that we would lose the whiteboard state (i.e the IPaths[] context) and the user wouldnt be able to amend/erase existing paths from the canvas. Another approach consists of stringifying the whiteboard state (i.e IPaths[]) and writing the string as a entry into our PosgreSQL database. So whenever the user re-opens the question, the state of the whiteboard will be restored and the user will be able to resume drawing at where he/she left off. In the end, we went with both approaches, since we also needed to support the functionality that teachers should be able to mark over the students' canvas submissions (i.e like a physical homework).
Probably my second most challenging task. While there were libaries that provided a simple media player interface, one of the product requirements was that the seekbar must be not be seekable. On top of that, the interface had to support a double-tap-rewind feature (similiar to Youtube), which none of the libaries supported. To that end, I had to create a custom video player inferface component, with at least 10 different states, each tracking a specific piece of contextual information.
Also, notice the little green markers along the seekbar? When the video streams to the corresponding green marker, a lesson question modal (which was to be rendered with RichText to support rendering of Latex mathematical equations, images etc.) has to be shown to validate the student's understanding of the concepts explained in the video. I learnt a lot about race conditions, state management and importance of technical documentation in this task.
The integration with the backend was not any easier. I had to save the state of where the user left off (so that the user will be able to resume watching), and keep track of the total time the user spent watching the video in a single session. This data, amongst many others was later ingested to churn out meaningful student data analytics, to give parents (and students themselves) insights into their learning habits.
But to give credit where it's due, the libaries that I've used already supported streaming and buffering out of the box, so it made implementation of so-called "own" video libary a lot less painful than it could be.
This was a very frontend-heavy task that I tackled. Luckily there were libraries such as react-native-carousel that made things a lot easier for me. Though this task was particularly frustating for me as I had to adjust and fine-tune the numbers such that the snap effect on the carousell appear smooth and slick.






