loren-michael
Posted on October 11, 2022
To wrap up my current phase of learning, I've been tasked to create a React application that utilizes Sinatra. As an avid D&D fan, I decided that for this project I'd create a character manager. I thought it would be a challenging way to create a backend database of users who manage their characters. I also created a DB Diagram to show my tables and relationships:
As an added challenge to myself, I wanted to learn how to utilize a simple login prompt to better organize the characters and show proper ownership over each of them. I decided not to use any kind of password or authenticator, this is simply a persistent username based login. By using the browser's localStorage, I was able to set up a way for users to persist on different visits. When logging in, the app will look for an existing user in the database. If a user is found, a few different states get updated. A currentUser is set, isLoggedIn is set to true, we add a user id to localStorage, and then we navigate to a home page where all of that user's characters are displayed. The login code looks like this:
const login = (user) => {
setCurrentUser(user);
setIsLoggedIn(true);
localStorage.setItem('user_id', user.id);
navigate(`/${user.id}/characters`)
}
I then wrote a useEffect that would fetch the characters of a user whenever the currentUser state gets updated. When the user logs out, all of these states get reverted back to their default settings, including wiping out the state that stores the characters so that there are no data remnants from previous users. In order for this to work properly, I had to find a way to look up users in the backend when entering a username into the Login component. To do this, I wrote a fetch that would send the submitted username to the backend. The backend then searches for the username, and if it exists, sends back the user data and calls the login function shown above. If the user does not exist, it sends a message instead of user data, and the browser will alert the user that the username they are trying to log in with does not exist. (Currently I have not implemented a way to create a new user but I plan to add that in the future.)
On the frontend, the fetch takes the username and POSTs it to the server:
const handleLogin = e => {
e.preventDefault();
fetch('http://localhost:9292/login', {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({ username })
})
.then(resp => resp.json())
.then(data => {
if(!data.message) {
login(data)
} else if (data.message) {
alert(data.message)
}
})
}
In this process I also learned that a project can have multiple Controller files to better organize your routes. When making additional Controller files, they need to be added to your config file so your application knows to access them for routes.
When we make this fetch, we use the SessionsController file, which has a route specifically for login requests. the route looks like this:
post '/login' do
user = User.find_by_username(params[:username])
if user
user.to_json
else
{ message: "This user does not exist."}.to_json
end
end
The next step was writing a basic React frontend that would properly display all of a user's characters and allow the user to level up each of the characters (only to 20, naturally) and delete them if desired. But before being able to display data, I needed to create it! Using Active Record I created a migration for each of the tables that I wanted to utilize (Characters, Users and Games). In each of the migrations, I wrote out a create_table that included the information that I wanted to be able to store in each of the tables.
Here is the character table migration:
class CreateCharacters < ActiveRecord::Migration[6.1]
def change
create_table :characters do |t|
t.string :name
t.string :race
t.integer :level
t.belongs_to :game
t.belongs_to :user
end
end
end
But wait! I forgot some columns! As I progressed with project development I went back to write additional migrations to add columns for character_class (since class is a reserved word, the column could not simply be named "class") and an icon, which holds the string of the URL for the icon images. While going through this process I also made a seed file that would create users, games and characters to test the application's functionality. Once seeded, I was able to display a user's characters after login:
As you can see, there are buttons on each character card that will allow the user to Level Up their character (to a maximum of 20 per traditional D&D rules) and delete them. The routes for these two tasks were fairly simple, either sending a new integer to PATCH to the character or destroying the entry.
This was a challenging way to reinforce everything that I've learned about React, Active Record, Sinatra, and even some basic Javascript/CSS. I look forward to learning so much more in the coming months!
Posted on October 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.