Tutorbook is the best way to manage tutoring and
mentoring programs (virtually). See the
ROADMAP
for a high-level overview of what's being worked on and what's coming.
It's an online app used by organizations (i.e. nonprofits, K-12 schools) to:
- Match students with tutors and mentors (e.g. by subjects, availability, languages spoken).
- Manage and track those matches (e.g. via a communications timeline and tags).
Students use Tutorbook to:
- Search their school's tutors/mentors themselves (instead of having an admin match them up).
- Keep track of appointments and availability (e.g. via the schedule view).
Parents and teachers use Tutorbook to:
- Request tutors/mentors for their students (those requests are then fulfilled by an admin who matches the student with the appropriate tutor/mentor).
- Track their student's matches (e.g. via the communications timeline).
This is a high-level overview of the various resources ("things") manipulated by and created through the app.
Note: This section is not a complete technical definition of our data model.
Instead, please refer to
lib/model
for always up-to-date Typescript data model definitions.
A user is a person. This person could be a tutor, mentor, student, admin or all
of them at the same time. Those roles are not inscribed on each user but rather
implied by role-specific properties (e.g. a mentor will have subjects specified
in their mentoring.subjects
property).
An org is a school or nonprofit or other business entity that is using TB to manage their tutoring and mentoring programs.
A request is a job post. Typically created by parents or teachers, it comprises of:
- The student who needs help (e.g. "Nicholas Chiang").
- When the student is available (this is included in the student's profile).
- The subjects he/she needs help with (e.g. "AP Computer Science A"). This is
also added to the student's profile under their
tutoring.searches
property. - A concise description of what specifically the student is struggling with (e.g. "Nicholas doesn't understand Java arrays and sorting algorithms").
Note: Requests can also be created by admins (and often are). For example, an admin might need to migrate the results of a Google Form to Tutorbook (by creating the requests all at once and then fulfilling them over time).
Once created, a request is fulfilled by an admin, who searches on behalf of the student and creates a match (between the student and an appropriate tutor/mentor).
A match is a pairing of people (typically between a single student and a single tutor/mentor, but there can be group pairings as well). Matches can specify times (e.g. "Every Monday at 3-4pm") and meeting venues (e.g. "Use this Zoom meeting room" or "Use this Google Meet link").
- Students create matches when they "send a request" to a tutor/mentor from the search view.
- Admins can directly create matches (e.g. when migrating from an existing system, admins know who's matched with whom).
- Admins can create matches to fulfill requests (e.g. a teacher requests help for their struggling student and the admin finds that help).
Upon creation, Tutorbook sends an email to everyone in the match (all of the
attendees
) with everyone's anonymous contact info.
Tutorbook has a system like
Craigslist's where each attendee
in each match has a unique anonymous email address (e.g.
88626b40-49c7-bd16-a845-ece7527cded7@mail.tutorbook.org
). Emails can then be
intercepted by Tutorbook and added to the in-app communications timeline before
being relayed to their intended recipients.
Summarized here are descriptions of common data flow patterns and design specs. These are some of the front-end design guidelines that TB follows in order to maintain consistency and display predictable behavior.
TB uses Segment to collect analytics from both the client and the server. When defining events, we use Segment's recommended object-action framework. Each event name includes an object (e.g. Product, Application) and an action on that object (e.g. Viewed, Installed, Created).
TB (Tutorbook) creates new recurring Zoom meetings for every match. To do so, TB stores Zoom OAuth refresh tokens and account IDs within user and org profiles.
Orgs have two options when authorizing TB to use their Zoom account:
- Create new users: TB will create new Zoom users (within the org's Zoom
account) for each user created on TB (using the user's actual email address
and falling back to the user's TB-assigned anonymous email address
(e.g.
dfadf-adwei8e-dw934sd@mail.tutorbook.org
instead ofmy.personal.email@gmail.com
) if that fails). TB then uses those Zoom users when creating Zoom meetings.- Requires both the meeting:write:admin scope and the user:write:admin scope.
- Assume users already exist: TB will assume that Zoom users (using the
user's actual email address) already exist (within the org's Zoom account)
for each user created on TB. TB then reuses those existing Zoom users when
creating Zoom meetings.
- Requires only the meeting:write:admin scope.
Smaller orgs may opt for option one (for convenience) while larger orgs (e.g. entire school districts) will likely use option two (because they already have Zoom users for each of their students and tutors).
Option two is recommended when possible because it does not require users to login to Zoom using their TB-assigned anonymous email address (as they must be logged into the correct Zoom account to host the match Zoom meetings).
Because Zoom pricing is per user license, TB will
only create Zoom user accounts when it has to (i.e. when a match is
created and none of the match's people
already have Zoom user accounts). When
creating a Zoom meeting for a match:
- TB will first try using the tutor or mentor Zoom user accounts.
- If that fails, TB will try using the student (i.e.
tutee
andmentee
) Zoom user accounts.- TB will not attempt to use
people
who do not have anyroles
listed (as those people were likely added just because they were on an email thread with one of the tutors, mentors, or students).
- TB will not attempt to use
- If all of that fails (i.e. there are no existing Zoom user accounts for the match's people within the match's org), TB will try to create a new Zoom user account for the match's tutor or mentor within the match's org.
- If that still fails (i.e. the org chose option two and doesn't allow TB to create new Zoom users), TB will fallback to using Jitsi.
For more info on our Zoom integration, see this issue.
There are two types of data entry forms used throughout TB:
- Single update forms. These are forms that are explicitly submitted by the
user upon completion (think Google Forms; must be submitted to be saved).
- Includes inputs, submission button, loading overlay, and error message.
- Upon submission, these forms:
- Show a loading state that prevents further user input.
- Immediately mutate local data (to start any expensive re-rendering).
- Update remote data with a POST or PUT API request.
- If the server sends an error, reset local data and show error message. Otherwise, mutate local data with the server's response.
- Hide the loading state. Data has been updated or an error has occurred.
- Ex: New request form, edit user form (in people dashboard), sign-up form.
- Continuous update forms. These are forms that continually receive user
input, mutate local data, and update remote data at set intervals (think
Google Docs; continually auto-saves user input).
- Includes inputs (shows error message via a snackbar).
- Upon update, these forms:
- Immediately mutate local data (unless such a mutation would cause too much expensive re-rendering delaying further user input).
- Set a timeout to update the remote data (e.g. after 5secs of no change, update the remote). Clear any existing timeouts.
- Update remote data with a POST or PUT API request.
- If the server sends an error, show an error message via a snackbar and retry the request. Local data stays mutated. Otherwise, mutate local data with the server's response.
- Ex: Org settings form, profile form, query/search form.
Do the following (preferably in order):
- Join our Slack workspace.
- Message
#introductions
with who you are and how you can help (and what you'll find the most interesting to work on). - Check the
#development
channel pins for more information on how you can help out. - Read through the links included below to become familiar with our current tech stack.
- Contribute:
- Choose an issue (from the top of the To Do column; the most pressing issues are at the top).
- Fork this repository.
- Address the issue.
- Create a PR.
Also feel free to check out our recently added tutorials/
directory for
additional information detailing different aspects of this project (e.g. tests,
deployment workflows, CI/CD, etc).
This project uses (please ensure that you're familiar with our tech stack before trying to contribute; it'll save your reputation and a lot of time):
- Typescript - As our language of choice (mostly for static typing, stronger linting capabilities, and the potential for beautifully detailed--and completely automatically generated-- documentation). Typescript is also well supported by Next.js and React.
- Sass - For styling components (i.e. CSS on steroids). Sass, like Typescript, is also well supported by Next.js out-of-box.
- React - As our front-end framework.
- Next.js - To easily support SSR and other performance PWA features.
- SWR - Used to manage global state. SWR fetches data from our back-end, stores it in a global cache, and allows local mutations of that cache (with or without automatic revalidation).
- Yarn - To manage dependencies much faster than NPM (and for better community support, advanced features, etc).
- ESLint - For code linting to avoid common mistakes and to enforce styling. Follow these instructions to install it in the text editor of your choice (such that you won't have to wait until our pre-commit hooks fail to update your code).
- Cypress for integration, UI, and some unit tests. Cypress is like Selenium; but built from the ground-up with the developer in mind. Cypress runs alongside your code in the browser, enabling DOM snapshots, time travel, and overall faster test runs.
- Google's Firebase - For their NoSQL document-based database, Authentication, and other useful (relatively drop-in) solutions.
- Algolia is synced with our Firestore database via GCP Functions. TB uses Algolia for subject and language selection and to power the primary search view capabilities.
To setup a development environment for and to contribute to the TB website:
- Follow these instructions
to install
nvm
(our suggested way to use Node.js) on your machine. Verify thatnvm
is installed by running:
$ command -v nvm
- (Optional) If you use Vim as your preferred text editor, follow these instructions on setting up Vim for editing JavaScript.
- Run the following command to install Node.js v12.18.3 (our current version):
$ nvm i 12.18.3
- (Optional) Run the following command to set Node.js v12.18.3 as your default Node.js version (useful if you have multiple Node.js versions installed and don't want to have to remember to switch to v12.18.3):
$ nvm alias default 12.18.3
- Ensure that you have recent versions of Node.js and it's package manager
npm
by running:
$ node -v
12.18.3
$ npm -v
6.14.7
- (Optional) Install the Cypress system dependencies if you plan on running our integration tests locally.
$ sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- Clone and
cd
into this repository locally by running:
$ git clone https://github.com/tutorbookapp/tutorbook.git && cd tutorbook/
- Follow these instructions to
install
yarn
(our dependency manager for a number of reasons):
$ npm i -g yarn
- Then, install of our project's dependencies with the following command:
$ yarn
- Follow the instructions included below (see "Available Scripts") to start a Next.js development server (to see your updates affect the app live):
$ yarn dev
- Message me (DM @nicholaschiang on
Slack) once (not if) you get the following
error (I have to give you some Firebase API keys to put in the
.env
file):
Error [FirebaseError]: projectId must be a string in FirebaseApp.options
- Finally,
cd
into your desired component or lib utility, make your changes, commit them to a branch off ofdevelop
, push it to a fork of our repository, and open a PR on GitHub. For more details, see our git branching workflow.
All of the below scripts come directly from Next.js. In the project directory, you can run:
This command runs two scripts concurrently:
- Runs
next dev
with the Node.js--inspect
flag on (useful fordebugger
statements) to start the Next.js development server. - Runs
firebase emulators:start
to start the Firebase Emulator Suite.
Open http://0.0.0.0:3000 to view the app in the browser
(note that TB uses 0.0.0.0
instead of the default localhost
for Intercom
support. The page will hot-reload if you make edits.
You will also see any lint errors in the console.
Open http://localhost:4000 to view the (locally-running) Firebase development console. Here, you can manually seed Firestore data and view GCP Function logs.
Runs next build
which builds the application for production usage.
Runs next start
which starts a Next.js production server. I have no use for
this right now because I'm deploying to Vercel NOW which handles that for me.
Runs the build to generate a bundle size visualizer.
Runs all of ESLint tests. This should rarely be necessary because you should
have ESLint integrated into your IDE (and thus it should run as you edit code)
and I have Husky running pretty-quick
before each commit (which should take
care of the styling that ESLint enforces).
Runs our code styling Husky pre-commit hook. TB uses Prettier to enforce consistent code formatting throughout the codebase.
A pre-commit hook is used to format changed files found on commit, however it is still recommended to install the Prettier plugin in your code editor to ensure consistent code style.
I stole (and slightly modified) this GitFlow model from Toggl's mobile team which stole it from Vincent Driessen.
To ensure - as much as possible - the quality and correctness of our code, and to enable many contributors to work on our apps at the same time without getting in each other's way we use a modified version of the GitFlow work flow by Vincent Driessen.
We call this work flow SuperFlow.
Below you will find Vincent's original diagram adapted and extended corresponding to SuperFlow followed by an explanation of the various concepts and steps involved.
Legend:
- Purple bubble: start of a release or hot fix branch
- Yellow bubble: release tag
- Green arrow: merge that requires review
Unless explicitly stated otherwise (either here or by the branch's owner), only a branch's creator may push changes to it. Other developers may always create pull requests to submit changes to the branch.
develop
is our main branch. It corresponds with the current work-in-progress
state of the app, that we deal with most often as developers.
develop
is a protected branch and no commits can be pushed to it directly. The
only way to add features, fix bugs, and make other changes is through pull
requests that pass review and automated tests.
develop
should always be stable and ready for release. Any features that
are merged only partially must be disabled in code or using a pre-compiler
directive so that they do not affect release builds.
Feature branches are branches created by developers based on develop
which are
used to create new features, fix bugs, and make other changes to the app.
These branches are updated with Vercel deploy previews and Cypress integration tests on GitHub.
When a feature or change is done, it is merged into develop
via a reviewed and
tested pull request. This merge should always happen using a squash, unless
there is a special case that requires an exception.
Typically, once a feature or change is merged into develop
I'll then proceed
to release it locally.
Release branches are one of the only two ways of creating a new public release.
They are branched off of develop
and stay alive until the corresponding
version is released.
The only commits that may be pushed directly to release branches are version increments. All other changes must be applied using pull requests from release bug fix branches.
Once work on a release branch is completed and sufficiently tested, the branch
is merged back into develop
to incorporate all changes there. The develop
branch is then merged into master
to trigger a new production build. These
merges always happens using a rebase and merge to ensure the release tags do
not point to orphaned commits.
Typically, I'll just do this all locally (bypassing the release branch):
- Merge
develop
intomaster
(adds features and bug fixes). - Run
release minor
to:- Increment the version number.
- Trigger a new GitHub release.
- Push changes to GitHub to trigger a production build on Vercel.
- Merge
master
intodevelop
(updates the version tag).
Release bug fix branches are branched off of a release branch if bugs are found during testing of a pre-release build. The bug is fixed on that branch, after which it is squashed back into the release branch using a reviewed pull request. The owner of the release branch must always agree to this to ensure only minimally necessary changes are included in the release.
Hot fix branches are branched off of the latest release tag (typically the
HEAD
on both develop
and master
) in the case of a critical bug in a
released build. They are the second of two ways of creating a new public
release.
In most cases the bug can be fixed directly on the hot fix branch and does not require pull requests.
Once work on a hot fix branch is completed and sufficiently tested, the branch
is merged back into develop
to incorporate all changes there. The develop
branch is then merged into master
to trigger a new production build. This
merge always happens using a rebase and merge to ensure the release tags do
not point to orphaned commits.
The above explanations of SuperFlow are not only a sub-set of allowed operations, but are in fact exhaustive. This means that there are no other valid ways to create releases, than outlined above.
In summary:
- Release and hot fix branches are the only two ways of creating new public releases.
- Release branches branch from
develop
and hot fix branches branch from the latest release tag (typically also theHEAD
ofdevelop
). - Release and hot fix branches are always merged into
develop
on completion. This merge is always performed using a rebase and merge (to maintain a linear commit history).
Our apps follows the following versioning scheme:
major.minor[.maintenance]
- The
major
component is changed only upon special considerations. - The
minor
component is incremented by one as the first commit of every release branch (and in no other case). The same commit also removes themaintenance
component. - The
maintenance
is added and incremented by one as the first commit of every hot fix branch (and in no other case).
In essence, the versioning scheme can thus be thought of as:
major.release[.hot_fix]