A positive reinforcement habit builder with a growth mindset.
This app is designed to provide an intuitive and encouraging way to build habits and achieve personal goals. We are already hard enough on ourselves as it is. Oftentimes what we need is just a small push in the right direction. That's where Bloom comes in.
Through a combination of visually appealing rewards, an easy-to-use interface, and sensible defaults, Bloom makes it easy to set your goals and achieve them.
Many technologies came together in the process of building this app.
In the backend we used MongoDB for our database and Mongoose to define schemas and models. We used JWT for authentication and BCrypt for password hashing. Our server runs on Express, and we used Validator for validating input.
Our frontend is built with React and Redux. We used Axios to make api calls, and all our styles are written in Sass. The jar was built with the p2.js physics library. We also used React Beautiful Dnd for drag and drop functionality.
In the process of developing this app, we added a number of unique features. Two are highlighted below.
Designing the petal jar to function correctly proved to be quite challenging. We used the physics library p2.js to calculate the physics, but the rendering logic was custom made. Particularly difficult was rendering the jar itself, which was accomplished with the following code:
_renderConvexShape(shape, ctx) {
const [x, y] = shape.body.position;
const [shapeX, shapeY] = shape.position;
ctx.save();
ctx.fillStyle = this.jarColor;
ctx.strokeStyle = this.jarColor;
ctx.beginPath();
ctx.translate(x + shapeX, y + shapeY);
ctx.rotate(shape.body.angle);
ctx.moveTo(shape.vertices[0][0], shape.vertices[0][1]);
shape.vertices.forEach((_, idx) => {
const [nextX, nextY] = shape.vertices[(idx + 1) % shape.vertices.length];
ctx.lineTo(nextX, nextY);
});
ctx.fill();
ctx.stroke();
ctx.restore();
}
The ability to drag and drop tasks to reorder them also was a complicated feature. We used the React Beautiful Dnd library, but managing the ordering in the backend was also quite difficult. Because of our unique schema in which tasks are nested inside of models, our users had to maintain a separate list of task ids indicating their ordering. This required some sophisticated backend logic to update said tasks list when users create/delete habits and tasks.
router.post("/:id/tasks", passport.authenticate("jwt", { session: false }), async (req, res) => {
const { errors, isValid } = validateTask(req.body);
if (!isValid) {
return res.status(422).json(errors);
}
let myHabit;
let owner;
try {
myHabit = await Habit.findOne({ _id: req.params.id });
owner = await User.findById(req.user.id);
} catch(err) {
return res.status(422).json({ ...err, message: "Bad request." });
}
// Check that we are the author
if (!myHabit) {
return res.status(404).json(`Could not find habit with id ${ req.params.id }`);
}
// Prevent the user from accessing another's habits
if (req.user.id != myHabit.user) {
return res.status(403).json("Cannot edit another user's habits!");
}
const {
title,
periodNum,
periodUnit,
numPetals
} = req.body;
const newTask = new Task({
habit: myHabit,
title,
periodNum,
periodUnit,
numPetals
});
myHabit.tasks.push(newTask);
owner.dailyTaskList.push(newTask.id);
owner.save();
myHabit.save()
.then(obj => res.json(obj.tasks[obj.tasks.length - 1]))
.catch(err => res.status(422).json(err));
});