This project was completed as part of a group learning exercise. The primary goal for this project is exposure to the Remix full stack framework which was just released publicly. The challenge of these tends to be how to integrate the various dependencies. Using supabase for authentication and peristant storage allowed for some interesting exploration into Remix.
I was able to get some code examples from @fergus on the Remix discord server which helped me get it working quickly. The Supabase JavaScript client is browser only currently.
typr_demo.mp4
https://typr-group-learn.netlify.app/
All of the lyrics used in the four demo sessions were written by my brother Harris Allan. The songs can be found on his YouTube channel and Spotify.
- ✅ User can click a 'Start Practice' button to start the practice session.
- ✅ When a practice session starts, the timer starts increasing
These stories are handled by a few options where the user can create their own sessions, or participate in other sessions. There is no visible timer, but the session is timed to allow for the words per minute calculation.
- ✅ User is shown a word
- ✅ User can type the word in a text input box
- ✅ If a user enters an incorrect letter, the text input box is cleared
- ✅ If a user enters all letters correctly, then the text input box is cleared and a new word is shown
The user is shown a bunch of words all at once, up to six lines. Each line should have a least three words. The user cannot proceed untilt they correct any mistakes in a line. When they finish a line, the cursor moves to the next line automatically.
- ✅ User can click "End Practice" button to end the session.
- ✅ When the session ends, the typing speed is shown (words per minute)
The session is ended automatically when all characters are typed correctly.
- ✅ Text box is not cleared when a wrong letter is typed instead as the user is writing the word, the correct letters are marked as green and the incorrect letters are marked as red
The incorrect letters are marked as red. The correct letters have a black background with white text.
- ✅ User can see their statistics across multiple sessions
The results are stored after each completed session.
- ✅ Users can login and see how their score compared with others (leaderboard)
If a user is logged in, their session is stored. In the results view for each session there is a "Top 5" leaderboard.
- ✅ Users can compete with others
Leaderboard.
The main difference between this application and the previous submissions lies in the framework that is used. Remix is a framework that embraces server rendering.
The previous submissions were architected as single page applications with cached server state.
- remix-run
- supabase/js
- xstate
- tailwindcss
- postgres
As with all first attempts at a framework, mistakes were made. The situation I faced was one where I would read documentation telling me not to do the thing I just did. This is what I meant when I claimed I was able to explore Remix. I now have a good baseline for refactoring.
It should be noted that I customized the folder structure slightly. I switched
app
tosrc
as an example. No major changes, but leaping head first into convention is hard.
The typing interface uses xstate to manage the data model for the typing input. The results are calculated from various values that are recorded as the user types.
The user can idle until they press space or click the start button. This action sends the START
event to the state machine causing a transition to the started
state. On entry to the started
state the context will be updated with the current time as timeStarted
.
As the user types it will keep track of their input by sending an INPUT
event as the input string grows. The INPUT
event is the most complicated one in the machine.
First if we have reached the end of the last line, we can transition directly to the results
state.
{
target: "results",
cond: "isEndOfLastLine",
}
If that doesn't happen, then we can update the context in the machine to reflect our cursor pointing to the beginning of the next line. This only happens if the line is complete, including fully valid.
{
actions: assign((context, event) => ({
currentLine: context.currentLine + 1,
currentPosition: 0,
})),
cond: "isLineComplete",
}
And in the most common case we need to keep track of the mistakes as the user types.
{
actions: assign((context, event) => {
/* long way to get the two characters we need to compare */
const current =
context.lines[context.currentLine][context.currentPosition];
const newest = event.input[context.currentPosition];
/* add one to the mistake count if they aren't the same */
return {
mistakeCount:
context.mistakeCount +
(newest && current !== newest ? 1 : 0),
typed: context.typed.map((line, index) =>
index === context.currentLine ? event.input : line
),
currentPosition: event.position,
};
}),
}
Completing the last line will transition to the results
state where the timestamp for the end of the session is recorded on entry. An action is triggered that results in the submission of the form to the /results
endpoint for the session.
The Remix documentation uses async/await heavily. I don't really have a preference either way. I do like to provide public examples using the Promise API directly.
The backend is hosted by supabase. I chose to use discord as a provider since the group is discord based. The user can view sessions and try the typing input without logging in. If the user wants to compete with others then they must login.
The process for authenticating with the supabase client requires some coordination between the browser and the server.
- Present the user with a login button.
- Redirect user back to the site after login.
- Wait for supabase auth to emit the event for the auth state change.
- Submit a post request with the token.
- Handle post request, creating a cookie with the token.
Since I need to make requests on the server with the authenticated user, I am creating a supabase client and providing the token. This is normally handled by the client library.
Both Remix and supabase are early in development. Getting it to work was the goal.