💥 Attention: This project needs your help! Message me if you're interested in becoming a contributor.
👨💻 Currently: Working on migrating from Firebase to the open-source Supabase alternative.
Tutorbook is the best way to manage online tutoring and mentoring programs.
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. Resources may also specify "tags" (filterable attributes) that are totaled daily for our in-app analytics.
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).
- Vetted - Has at least one verification. Set when the user is created, updated, or deleted.
- Matched - In at least one match. Set whenever a match is created, updated, or deleted.
- Meeting - Has at least one meeting. Set whenever a meeting is created, updated, or deleted.
Users also have role-based tags (tutor/tutee/mentor/mentee) that are set when:
- The user is created or updated. Ex: If a user has
tutoring.subjects
, they are a tutor and thus get thetutor
tag. - A match is created or updated. Ex: If a user is a
tutor
in at least one match at some point in time, they are a tutor and thus get thetutor
tag.
Role tags are never removed: once a user has been a tutor at least once, they will always be considered a tutor.
An org is a school, nonprofit, or other business entity that is using TB to manage tutoring and/or mentoring programs.
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 are simply containers for meetings.
- 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).
- Meeting - Has at least one meeting. Set whenever a meeting is created, updated, or deleted.
A meeting is exactly that: a meeting between the people in a match with a specific time and venue (e.g. a specific Zoom link). In order to support complex recurrence rules, a meeting's time consists of:
- From: The start time of this particular meeting instance.
- To: The end time of this particular meeting instance.
- Recur: The time's recurrence rule as defined in the iCalendar
RFC. This is used server-side by
rrule
to calculate individual meeting instances that are then sent to the client. It is manipulated client-side when users select a recurrence rule or choose to add an exception to a recurring meeting. - Last: The last possible meeting end time. If a meeting is recurring,
this will be the end time of the last meeting instance in that recurring
range. Or, if the recurring range is infinite, I use Firestore's max date
(Dec 31 9999) which is more than sufficient. This is calculated and assigned
server-side using
rrule
. It is completely ignored client-side (in favor of theto
property).
Upon creation, Tutorbook sends an email to all of the people
in the new
meeting's match with the meeting time, venue, and everyone's contact info.
- Recurring - Is recurring (has an
rrule
). Set when the meeting is created, updated, or deleted.
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.
Recurring events are always a struggle to implement. There are many resources available that are meant to make implementing such recurrence rules easier.
TB's entire recurrence stack is quite simple:
- Meetings specify complex
RRULE
recur rules with support for event exceptions and everything else supported byrrule
. - At index time, the last possible end date is stored in our Algolia index to make querying data more efficient.
- When a meeting range is requested, our API parses the recur rules for meetings within the requested range (i.e. both the start date and recur end date are within the requested date range) and sends the client individual meeting instances.
- When availability is requested, our API again parses the recur rules for meetings within the requested availability range and excludes the resulting individual meeting instances from the user's weekly availability.
Editing and updating recurring meetings is intuitive:
- When a user updates a single event instance (choosing not to update all
recurring events), an exception is added to the recurring event's
RRULE
and a new regular (i.e non-recurring) meeting is created. - When a user deletes a single event instance (choosing not to delete all
recurring events), an exception is added to the recurring event's
RRULE
.
TB uses Segment to collect analytics from both the client and the server. When defining events, I 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 also has in-app analytics features for orgs to use. It collects a few totals
every day (based on Algolia tags) and stores them in a Firestore subcollection
(/orgs/<orgId>/analytics
). Those totals are constantly being updated as API
requests come in (e.g. when a new user is created, TB increments the "Total
Users" statistic by one) and thus are always up-to-date. All of those totals
are based on filterable tags (e.g. "Total Users Matched") which allows admins to
view all the users/matches/meetings that have certain tags, answering questions
like:
- Who are the students or volunteers that aren't matched? Why aren't they?
- Who doesn't have meetings? Why don't they?
- Which students aren't donating money? Why aren't they?
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.
I have very precise rules over how Git commit messages in Tutorbook's repository must be formatted. This format leads to easier to read commit history.
Please refer to the following documentation for more info:
Commit messages that do not adhere to the following commit style will not be
merged into develop
:
<type>(<scope>): <short summary>
│ │ │
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
│ │
│ └─⫸ Commit Scope: The page, API route, or component modified.
│
└─⫸ Commit Type: ci|docs|feat|fix|perf|refactor|test|deps|chore
The <type>
and <summary>
fields are mandatory, the (<scope>)
field is
optional.
Must be one of the following:
- ci: Changes to our CI configuration files and scripts.
- docs: Documentation only changes.
- feat: A new feature.
- fix: A bug fix.
- perf: A code change that improves performance.
- refactor: A code change that neither fixes a bug nor adds a feature.
- test: Adding missing tests or correcting existing tests.
- deps: A change in dependencies.
- chore: A code change in utility scripts, build configurations, etc.
The scope should refer to the page, API route, or component modified. This can
be flexible however (e.g. the scope for a docs:
commit may be the README
).
Use the summary field to provide a succinct description of the change:
- Use the imperative, present tense: "change" not "changed" nor "changes".
- Don't capitalize the first letter.
- No dot (.) at the end.
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 v16.13.0 (our current version):
$ nvm i 16.13.0
- (Optional) Run the following command to set Node.js v16.13.0 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 v16.13.0):
$ nvm alias default 16.13.0
- Ensure that you have recent versions of Node.js and it's package manager
npm
by running:
$ node -v
16.13.0
$ 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.
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.
Builds TB's service worker and 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.