Announcing SparkEd V2
Introduction
This project was inspired from working on a variety of educational programs in sub-Saharan Africa over the past decade where lack of access to educational resources and poor and/or expensive internet remain challenges. After working with a number of different platforms, we could not find one with flexibility to add local content (which in many cases is extensive and well developed) organized around local curriculum, programs and resources.
SparkEd was designed as an open source resource with a focus on uploading locally developed resources for delivery as a central/library resource but also for delivery of modular/grade level resources on mobile devices.
Initial Development
Initial programming was done in Zambia by programmers at Hackers Guild (@hackersguild) who trained programmers in Ethiopia (@supedplc). End product of the project in Ethiopia now includes all Secondary and Preparatory subjects and content (grades 9-12) with suplemental content delivered on servers that are radiolinked for updating. (see Elearning Tool Development ).
Software is meant to be available for use in any setting where content is developed and could be uploaded.
Tech Stack
SparkEd is written entirely in Javascript apart from some utilities written in Bash.
- Meteorjs
- Reactjs
- MongoDB
- Docker
- Travis CI
Meteorjs was used as a backend for file storage, database communication and fast reactive data serving. Meteorjs has been a good option in this case as it also provides users with real time usage data. The cons for Meteorjs were on initial development and deployment where we found testing meteor apps difficult and empirical and significant mocking needed to be done. Once established though the platform was very stable.
Reactjs SparkEd product development leads to significant dynamic and real time data and we found it wise to use Reactjs and we don't regret this choice.
React allowed for implementing features like language translation and dark mode which was much easier than it could have been with other platforms.
MongoDB was used as our database although we really didn't have much choice since Meteorjs comes bundled with it and made for seemless integration.
We also used MongoDB GRIDFS for file storage to easily manage file backup and real time serving.
We used Docker hub to host the image of SparkEd which is publicly available and you can find it here.
For continuous integration and continuous deployment, we used TravisCI which has been working for our early development models but the plan is to migrate to Jenkins. We used TravisCI to make sure that all required tests and lint rules pass before any pull request is merged into the master.
Since SparkEd end products are designed to run mostly in offline environments, we don't have it hosted online, however we keep the docker image updated on every pull request merged or any changes committed to the master, this makes it easy for us to avoid the need of doing this manually.
Notable new features
Dark Mode
The implementation of night mode was an important implementation. Many student users would be using the product in homes without electricity in the evenings and so it was best to provide a way for users to choose night or day mode.
To accomodate this we had to do a little change on some pages but most of other components were ready for this update, for pages that had lists, it was a bit hard to implement this in a more pleasing way and we decided to re-design such pages, the included notification, the modal wrapper and the feedback page. A simple example implementation of this is shown here.
/* eslint class-methods-use-this: "off" */
/* eslint import/no-unresolved: "off" */
import React, { Fragment } from 'react';
import { PropTypes } from 'prop-types';
import Header from '../components/layouts/Header';
export const ThemeContext = React.createContext();
export default class AppWrapper extends React.Component {
state = {
isDark: JSON.parse(localStorage.getItem('isDark')),
mainDark: '#212121',
main: '#005555',
};
toggleDarkMode = async () => {
await this.setState(state => ({
isDark: !state.isDark,
}));
await localStorage.setItem('isDark', JSON.parse(this.state.isDark));
};
render() {
const { children } = this.props;
const { isDark } = this.state;
return (
<ThemeContext.Provider
value={{ state: this.state, toggle: this.toggleDarkMode }}
>
<div style={{ backgroundColor: isDark ? '#252829' : '#fff' }}>
<Header />
<Fragment>{children}</Fragment>
</div>
</ThemeContext.Provider>
);
}
}
AppWrapper.propTypes = {
children: PropTypes.node.isRequired,
color: PropTypes.object,
};
You can see the theme context being used here in the same component that toggles the day/night here.
// This file has been truncated for brevity
import React, { Fragment, Component } from 'react';
import { Meteor } from 'meteor/meteor';
import PropTypes from 'prop-types';
import i18n from 'meteor/universe:i18n';
import { ThemeContext } from '../../containers/AppWrapper';
const T = i18n.createComponent();
class UserInfo extends Component {
render() {
const user = Meteor.user();
return (
<ThemeContext.Consumer>
{({ state }) => (
<div>
<MainModal
show={isOpen}
onClose={this.close}
subFunc={this.handleSubmit}
title={title}
confirm={confirm}
reject={reject}
/>
<ul
id="slide-out"
className="sidenav"
style={{
backgroundColor: state.isDark ? state.mainDark : '#ffffff', // an implementation of the theme from the context
}}
>
{/* The switch that toggles day/night mode */}
<div className="switch">
<label>
Day Mode
<input
type="checkbox"
onChange={this.props.handleNightMode}
checked={this.props.checked}
/>
<span className="lever" />
Night Mode
</label>
</div>
</ul>
</div>
)}
</ThemeContext.Consumer>
);
}
}
UserInfo.propTypes = {
handleNightMode: PropTypes.func.isRequired,
checked: PropTypes.bool.isRequired,
};
export default UserInfo;
Internalisation and Localisation
Another new feature is that SparkEd v 2.0 supports multiple languages, and boilerplates are provided for use with different languages.
// this is truncated and don't mind this comment in JSON
{
"_locale": "en-us",
"_namespace": "common",
"language": {
"enUS": "English - U.S.",
"esES": "Spanish - SP",
"frFr": "French - FR",
"Language": "Language"
},
"header": {
"openExternalLink": "Click here to Open all the external links in a page",
"externalLink": "External Links"
},
"notFound": "Oooops Page not Found Take me",
"titles": {
"addreference": "Add Reference",
"referenceDisplaced": "References displayed",
"feedback": "Feedback",
"source": "Source",
"usersfeedback": "Users Feedback",
"notifications": "Notifications",
"bookmarks": "Bookmarks"
}
}
Normally the file above is long and includes all translations that are used across teh SparkEd, the translation files are according to how many languages you want to support if you have your own fork of this project.
This is the English version but if you want to support French you would then have a similar file then change on the values like the following(Edited for brevity).
"titles": {
"addreference": "Ajouter Références",
"referenceDisplaced": "Références affichées",
"feedback": "Commentaire",
"source": "Source",
"usersfeedback": "Commentaires de l'utilisateur",
"notifications": "Notifications",
"bookmarks": "Signets"
}
This can be applied to as many languages as you would want to support. The interesting part of this implementation is that it doesn't only affect the user interface language but also the contents that users are able to see. We found this very helpful with the SparkEd deployment in Ethiopia where they have different regional languages that are retained as classes at the secondary/prep level. As resources are developed for lower grades where language of instruction is regional with English and Amharic classes, this will also be an important feature.
Users still have an option to choose what they want to see and what language they use to navigate within SparkEd resources and Administrators can choose a language for their use as well.
e.g: If an administrator or a content-manager is uploading a video, they can choose what language that video is in, this helps users get the right content
Statistics
Another feature we added was ability to monitor usage statistics, Administrators initially were able to see who is viewing what resource and how many times(Thanks to Brian Mukuka) but they needed a way to also view statistics for all the content within SparkEd. This was a simple but important implementation.
ErrorBoundary
ErrorBoundaries were introduced in React v16 and this was an important feature for us because some components in one way or another would break in production leading to total app dysfunction However with ErrorBoundary you can choose what to render and thus provide a better experience to the user. ErrorBoundary also provides guidance on how the user can troubleshoot the problem, how they can contact the maintainer or even the administrator all while the rest of the app is still functioning. The <ErrorBoundary />
is a component like any other, it takes children as other components.
Consider this example shown below.
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { formatText } from '../utils/utils';
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true, info });
Meteor.call('logger', formatText(error, Meteor.userId(), 'error-boundary'), 'error');
}
takeBack = e => {
e.preventDefault();
return history.go(-1);
};
render() {
const { hasError } = this.state;
const { children } = this.props;
if (hasError) {
return (
<>
<h1 className="notFoundHead">
Error Happened <i className="fa fa-frown-o" />
</h1>
<h3 className="notFound">
{' '}
Sorry it seems like something went wrong <br />Try
<a href="" onClick={e => this.takeBack(e)}>
{' '}
go back
</a>
</h3>
</>
);
}
return children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
};
An implementation of the above component is simply to wrap another component inside the <ErrorBoundary />
<ErrorBoundary>
<ComponentThatCanCauseAnError />
{ /* If you want you can even nest this */ }
<ErrorBoundary>
<AnotherComponent />
</ErrorBoundary>
</ErrorBoundary>
The errors caught in the nested component are sent to the parent component which in this case is the ErrorBoundary and you can do whatever you want to do with error.
You can read more about error boundaries from the react docs here
Bugs Fixed
Video Updating
There was a bug that was reported with video format not updating, If you were viewing a pdf resource then you change back to video the UI wasn't noticing the change. You can read the issue details here. Videos doesn’t update · Issue #13 · SparkEdUAB/SparkEd The videos don’t update only when you change the resource type and go back to the video.
Broken Sidebar
The Sidenav was conflicting with another implementation of a slightly different sidenav added on the dashboard and this would disrupt closing the component.
Sidenav is broken · Issue #71 · SparkEdUAB/SparkEd
List of uncaught errors
These weren't really bugs but errors that were not caught and app instability. These were catalogued and fixed. List of uncaught errors · Issue #41 · SparkEdUAB/SparkEd
Releases
SparkEd releases follow semantic versioning(semver) where every release carries changes that lead up to a major release.
Major.Minor.Patch
Consider version 1.8.2, The major version would be 1, the minor 8 and patch 2, patches are only counted as bug fixes that don't make any changes that affect the project then the minor releases follow an addition of some functionality the major release is when there is some big noticeable changes that users would need to know and mostly carries breaking changes.
Find out more here for all SparkEd releases and release notes.
Contributors
As an opensource project we are always open for contributions and we appreciate those who have contributed in the past.
What's next
We are planning to migrate the stack to something different but we will keep the RFC open so that it's indicated we need the change.
V3 of SparkEd will be a complete re-write of the whole application, we will start and incrementally finish it all. The reason for this is to make sure we increase the test coverage, provide better performance and a good user interface.
Edited by Dr. Craig Wilson