Dan Denney

Building a Small PWA with Preact and Firebase

Read time: 29:33

Disclaimer: This is not a tutorial!

I have a ton of respect for the hard work that goes into using a project to teach the fundamentals of a technology. This isn't that, it's me sharing a process of learning by building something of my own.

If you’re looking for related tutorials, I learned from React for Beginners, Up and running with Preact, Firebase + React: Real-time, Serverless Web Apps, and Intro to Firebase and React.

In this post, I’m sharing how I did something, and it is currently at the “make it work” level. I’m 100% open to feedback on how to improve it, and I’ve taken extra steps in the process to make giving feedback easy. This doesn't just help me, but also anyone who reads this in the future. If you know of a way to make it better, please leave a comment on the post or a commit, @ me, create a PR, or write up a reaction post.

Sections

  1. Intro (1:45)
  2. Getting Started (1:40)
  3. Adding Firebase for Auth (3:45)
  4. Preact Auth and Organization (5:23)
  5. Retrieving Data from Firebase (4:25)
  6. Saving Data to Firebase (5:44)
  7. Styling (1:45)
  8. Wrapping Up (1:44)

Post Goals

The goals for this post are to share what I’ve learned, try out some new features (for me) in a blog post. It seems like a long post sharing open-source code could benefit from a Table of Contents, read time indicators, and commit links (41 of em).

Project Goals

The goals of this project were: reading/writing/manipulating data, designing a mobile UI and learning Preact. Preact is overkill for the base functionality, but the CLI version has features that are very beneficial.

App Goals

I exercise because I greatly enjoy beer and food. I'm not that into it, but since I'm going to do it, I should follow a system created by people who are. I'm simplifying, but a handful of the systems tell you to set an initial weight, do a specific number of reps, and then raise the weight after 5 sessions of reps. (Some useful apps exist, like Strong Lifts, but I wanted a customized version.) Most importantly, I wanted a dead simple UI that makes it clear what weight or speed I need to set when I get to the machine.

Getting Started With a Project

Since this was an app being designed for a demographic of 1 person, research was limited. I knew what was missing for me in other apps, and the style that I wanted. I use Bear for project details, and this had requirements, inspiration images, and my best guess at the data structure.

To allow for continually working on my version, I’m sharing the steps as I recreate the app in a repo specifically for this post.

Initializing a Project with Preact CLI

I was inspired by Addy Osmani’s talk on Production PWAs with JS frameworks, in which he shares how the various CLIs added PWA support by default. Automated service worker setup alone makes this tool fantastic, but the feature list is insane for 4.5kb.

My first step is always creating a repo via GitHub’s UI. It’s a personal preference, but it adds a step when you’re using a generator. That’s still preferable to me vs. initializing from the command line after generating. commit

Sass support requires a flag, so after installing the CLI, I ran preact create app --sass and then dragged the files out to my main folder. commit

A known fixed issue

In my first build, the 1.3 version would enable Sass support but still generate .less files. In getting the link to the issue, I found that it was closed and fixed with 1.4, so I updated and regenerated. commit

It’s alive!

Running preact watch (or the command of your choice) fires up a server on 0.0.0.0:8080 and the starter app is visible.

The out-of-the-box Lighthouse scores are fantastic. (Lighthouse is a tool for rating the code for a PWA. The service worker seems only to be enabled in production, so the PWA score is low locally, but 91 once you deploy. The important one to watch locally is Performance. Since Preact is so light, your code is what makes the difference. We’re starting off with 3.7s to a meaningful paint.

Adding Firebase

To get rolling with Firebase, I created a project “pwa-preact-firebase” (cause 30 character restriction) and grabbed the config info from “web setup” on the authentication page.

If you haven’t used Firebase, it will seem scary that I posted a screenshot of that information, but it’s available in the UI. Firebase handles security via permissions and many tutorials start by changing them to being wide open to get started. I’m skipping that because I know I want authed users.

Config

I learned this organization technique from Steve Kinney. The gist is that you install and include Firebase (commit), set the config, and then add named exports of the pieces that you want to use. commit

import firebase from 'firebase';

const config = {
  apiKey: 'AIzaSyBdk6HFp-9zT4oilTokoo4_e-ZX6uwR_Gg',
  authDomain: 'pwa-preact-firebase.firebaseapp.com',
  databaseURL: 'https://pwa-preact-firebase.firebaseio.com',
  projectId: 'pwa-preact-firebase',
  storageBucket: 'pwa-preact-firebase.appspot.com',
  messagingSenderId: '263234041568'
};
firebase.initializeApp(config);

export default firebase;

export const database = firebase.database();
export const auth = firebase.auth();
export const googleAuthProvider = new firebase.auth.GoogleAuthProvider();

I’m only using Google Auth because it doesn’t require an API key and I’m always logged in on my phone. There are other options (Twitter, FB, email/password) as well.

With that configured, the methods in Firebase are available anywhere that you import them. The next part is the first decision as to where that is.

Questionable Organizational Decision

In my initial version, I put all of the code for the UI in the home folder (within routes) and kept firebase in the global components folder. This led to lengthy imports whenever I imported Firebase. I’m fixing that in this version by adding an ExercisesList component.

Since I knew that ExercisesList would have child components, I created a folder with an index, exported ExercisesList (with placeholder copy), and imported it into the home route. A bare minimum Preact component looks like this:

import { h, Component } from 'preact';

export default class ExercisesList extends Component {
  render() {
    return (
      <section>
        <p>ExercisesList</p>
      </section>
    );
  }
}

And it’s imported like this (depending on its location) import ExercisesList from '../../components/ExercisesList';

commit

You Shall Not Get Data

By default, no one can read or write to a Firebase database unless they are authed. One way of making sure that it’s installed and working is to use logic to toggle an auth UI or the content of a component.

In ExercisesList, I imported auth and database from Firebase. At that point I only needed auth, but I knew that the reason that I was using auth was to access the database, so I brought them both in at the same time. import { auth, database } from '../../firebase';

I needed auth available for the logic, but there a few steps to “toggle” them based on auth status.

Creating Child Components

It’s a personal preference, but my workflow when I’m going to need new components is to start by making them, adding placeholder copy, and then import them. So, I added an Exercises component and a SignIn component.

A Simple SignIn

The only reason this app will exist is to track individual progress, so it’s intentionally useless if you’re not authed. To enable that, I imported auth and googleAuthProvider.

import { h, Component } from 'preact';
import { auth, googleAuthProvider } from '../firebase';

export default class SignIn extends Component {
  render() {
    return (
      <section>
          <h1>Raisercise</h1>
          <button onClick={() => auth.signInWithRedirect(googleAuthProvider)}>
            Sign In
          </button>
      </section>
    );
  }
}

For auth, this is where the magic happens: onClick={() => auth.signInWithRedirect(googleAuthProvider)}. signInWithRedirect is one of a few auth methods from Firebase and I’m passing the Google option. That’s all it takes for the transaction to happen, which is pretty amazing to me.

I’m very visual, so my next step was importing the SignIn component to see it on the page. Because SignIn isn’t directly related to ExercisesList, I made it a sibling component. I don’t see a need to use it any other way yet, but it didn’t feel right nesting it in the folder structure. I am calling it from ExercisesList, though, replacing the placeholder copy. commit

Mo Versions, Mo Problems

It seems that with the new version, the default URL locally is 0.0.0.0:8080 instead of localhost:8080 and 0.0.0.0 isn’t whitelisted for Firebase for OAuth. There were two options for fixing this: change the default host with a flag, like preact watch --host localhost or changing it in Firebase’s admin. Since I’d have to type that about “fifty eleven” times or add an alias, I made the change in Firebase. (I also assumed it was changed for a good reason that I’m not aware of.)

Removing Boilerplate

The default header in the default Preact-CLI template (there are other options) is awesome for getting started, but there won’t be a header in this app until v2, if ever. So, I killed it. commit

The alignment and lack of style looked funky, and that was killing me, but I held strong on leaving CSS for later.

A Snafu

At this point, I realized ExercisesList wasn’t a great name for the primary container since it also contained SignIn. So, I swapped it for Exercises. commit

Adding an Exercises List for Reals

I kinda looked like Larry David as I tried to decide between Exercises List and Exercise List. This job would be great if it weren’t for naming. Ultimately, I wanted the relationship of the word list to be closer to the child Exercise than the parent. ¯\_(ツ)_/¯

For that same reason, I nested ExerciseList in Exercises. I can imagine SignIn possibly being separated in the future, but not Exercises.

Anyhow, I use a pc snippet for a generic Preact component, which looks like this.

import { h, Component } from "preact";

export default class  extends Component {
  constructor () {
    super()
  }

  render () {
    return ()
  }
}

I left the constructor cause I knew I’d need it later and added a placeholder list, simulating the map that I’ll need eventually. Importing that into Exercises made this feel like I was finally getting somewhere. commit

Time to Use Preact

Up until this point, I had been putting HTML into JavaScript. To apply logic, though, I needed to use one of the features of Preact. Technically, state is a feature of React that Preact shrinks down, but I digress.

The gist is that I want to show the SignIn component when there is no authed user and the ExerciseList when there is. To do this, I set a currentUser to null by default, listen for Firebase auth changes, and render based on the value of currentUser in state.

The State of the User

State is a concept that I don’t understand well enough to explain yet, but I’m no longer hung up on the OG front-end definition of it being a visual change to an element. Without a Shadow DOM framework, I would toggle a class on a parent element and use CSS to show the SignIn or ExerciseList HTML. Instead, I can assign values to keys in state to help Preact decide what to render (and when to re-render). We pay the price of learning a new system (and others building and maintaining new systems) to provide a better experience for users.

The best part is that it’s straight forward. Here’s how I added a null user to state in ExerciseList.

import { h, Component } from 'preact';

export default class extends Component {
  constructor() {
    super();

    this.state = {
        currentUser: null
    };
  }

  render() {
    return (
      <ul>
        <li>Exercise</li>
        <li>Exercise</li>
      </ul>
    );
  }
}

Viewing that in the React Developer Tools confirmed that it worked and that I had messed up. When I refactored, I forgot to assign a name to the class in ExerciseList, so it was rendering a <_default> component. Fixed that. commit

Adding a LifeCycle Event

Another significant difference in Shadow DOM frameworks is lifecycle methods. They are another way of helping to decide what and when to render. In this case, I’m using componentDidMount to listen for Firebase auth methods. I also had to bring in auth from Firebase. commit

At this point, clicking the Sign In button changes the state of currentUser from null to an object that Firebase returns for the current user.

Render and Re-render

The final bit to make this work was some conditional logic in the Exercises component’s render function. For that to work, it needed to bring in state and assign it within the render function.

import { h, Component } from 'preact';
import { auth, database } from '../firebase';
import ExerciseList from './ExerciseList';
import SignIn from '../SignIn';

export default class Exercises extends Component {
  constructor() {
    super();

    this.state = {
      currentUser: null
    };
  }

  render() {
    const currentUser = this.state;

    return (
      <section>
        {!currentUser && <SignIn />}
        {currentUser && <ExerciseList />}
      </section>
    );
  }
}

With a user signed in, the UI is showing the ExerciseList component.

While that would eventually be enough to complete the actions that I’ll want, it seemed like adding an avatar and a sign out button was in order. So, I added a CurrentUser component with some placeholders to start. commit

Since this was an additional component within Exercises, I needed to wrap them in a single element (a JSX thing). commit

Passing Data with Props

Since CurrentUser is a child of Exercises, it doesn’t have access to the currentUser state. (I know the naming is getting confusing.) Rather than declare and update state within CurrentUser, I passed it in via props as user. commit

It was at this point that I realized I had messed up again. State had a value for currentUser, but it was passing null to props. I can’t explain why, but I put the componentDidMount code in ExercisesList instead of Exercises. Fixing that made it so that the props was getting my user object. commit

I also had explicitly rebound the currentUser constant in Exercises, which was adding a child object. I had done const currentUser = this.state; instead of const { currentUser } = this.state;. fixed

User Attributes from Google Auth

Now that I had access to the user object via Firebase, I could access the attributes. A console.log revealed all of them, so I used the photoURL and displayName for the alt attribute. This is to give some basic feedback that I’m in the correct account when I auth.

Preact passes props to the render functions, so I shortened up the attributes and added them to the image. commit

import { h, Component } from 'preact';

export default class CurrentUser extends Component {
  constructor() {
    super();
  }

  render() {
    const user = this.props.user;
    return (
      <article>
        <img alt={user.displayName} src={user.photoURL} width="40" />
        <button>Sign Out</button>
      </article>
    );
  }
}

Adding Sign Out Functionality

Arguably, I’d never want to sign out aside from testing, but it feels awful to not have the option. Even more so when it’s 2 lines of code to make it happen. The CurrentUser component needed access to Firebase auth and then the auth.signOut() method.

import { h, Component } from 'preact';
import { auth } from '../firebase';

export default class CurrentUser extends Component {
  constructor() {
    super();
  }

  render() {
    const user = this.props.user;
    return (
      <article>
        <img alt={user.displayName} src={user.photoURL} width="40" />
        <button onClick={() => auth.signOut()}>Sign Out</button>
      </article>
    );
  }
}

Whew. I now had a fully-functional Preact app with auth, all to see 2 lines of static HTML. It was time for the fun.

Planning Data

Knowing that Firebase stores data in JS objects, I imagined what the structure for the data would be in the planning portion of the project. I needed a series of exercises, which would have a name and a settingType. Those would have sessions with a setting and a flippable completed key. Each session would have an individual set with a timestamp and a completed key. I came up with:

- exercises
  - exercise
    - name
    - settingType
    - sessions
      - setting
      - completed
      - sets
        - timestamp
        - completed

No Plan Survives Contact with Code

While that’s a misquote of a classic, it’s very true for me, and it was true for my plan for data. If you want a small test, peek at it and yell out what I’m missing. (It’s ok, the people around you will understand.)

The first thing that I realized that I missed was users. While I thought that I’d be the only user, even for testing there would need to be 2. With this structure, exercises would be read/written by everyone. So, the first step was changing to users first.

- user
  - exercises

I also missed some attributes that I’d need and figured that creating sets after each completion round seemed like overkill. (That part was based on how I’d need to compare the recent data to determine when to raise the setting. Since I’d need to compare regardless, it was more work with no apparent benefit).

The timestamp is unnecessary for the basic functionality, but I knew that I’d want to graph these in a future version and wanted to be sure to have the data. I ended up with:

- user
  - exercise
    - name
    - setting
    - settingType
    - reps
    - raiseAfter
    - raiseBy
    - sets
      - set
        - setting
        - completed
        - completedDate

One of the awesome features about Firebase is that you can create the data in their UI to test it out before needing to hook up a form. (Kinda how you’d mock up with local JSON).

I added 2 to make sure a loop works.

Getting State In Order

I learned the hard way to make sure that you get state configured correctly in your app before trying to render or create a form to add the data. In this app, data will be sent directly to Firebase. The data is then retrieved from Firebase and pushed into the local state.

Retrieving Data from Firebase

Since I’m an authed user, Firebase will allow me to read and write to the database. Reading is done via a URL acting as an endpoint. In this case, it’s https://pwa-preact-firebase.firebaseio.com/user01.

I added exercises: null to state so that it can be updated once the data is retrieved. Some of the URL is in the config, so I access it by piecing together bits with my UID. That doesn’t match the dummy data that I added, so I hard-coded user01 to test.

componentDidMount() {
  auth.onAuthStateChanged(currentUser => {
    this.setState({ currentUser });
  });

  const exercisesRef = database.ref('/' + 'user01' + '/exercises');

  exercisesRef.on('value', snapshot => {
    this.setState({ exercises: snapshot.val() });
  });
}

Boom! Data coming from Firebase was now getting pushed directly into state, which would later determine what and when to render. commit

A Learning Curveball

Speaking of rendering, this was where everything fell apart for me. I didn’t know what I didn’t know, so I thought I needed to manually update state. I asked on SO and had an expensive first try of Code Mentor without resolution. I was stuck for like three days straight, and it made me stop trying to make this a “build with me” video.

The issue was that I was supposed to just push state and output from it, but I had been trying to build a nested local state. It was Steve Kinney’s explanation of how Firebase stores data (and why to use Lodash) that finally made it click.

Rendering Data from Firebase

Lodash’s map method works with nested objects like .map() does for arrays. Installing and importing that was the first step to rendering data. commit

Since I wanted to loop over a user and their exercises, I needed both of those accessible within ExercisesList. I updated the render function in Exercises to include exercises and then assigned both as props.

render() {
  const { currentUser, exercises } = this.state;

  return (
    <section>
      {!currentUser && <SignIn />}
      {currentUser &&
        <section>
            <ExerciseList exercises={exercises} user={currentUser} />
            <CurrentUser user={currentUser} />
        </section>}
    </section>
  );
}

To ensure this worked, I rendered a single attribute in ExercisesList. commit

render() {
  const { user, exercises } = this.props;
  return (
    <section>
      {map(exercises, (exercise, key) => <article>{exercise.name}</article>)}
    </section>
  );
}

I knew that I was going to be adding a lot (well some) more functionality, so I wanted to push the HTML for an individual Exercise into its own component. That required passing the user, the key, and all of the attributes of an exercise in via props. This spread operator {...exercise} made that easy.

{map(exercises, (exercise, key) => (
  <Exercise key={key} {...exercise} user={user} />
))}

To render them I added each of the ones that I wanted as constants before calling them in the HTML. commit

render() {
  const { name, setting, settingType } = this.props;
  return (
    <article>
      <h3>
        {name}
      </h3>
      <p>
        <div>{setting}</div> {settingType}
      </p>
      <p>
        <button setting={setting}>
          Fail
        </button>
        <button setting={setting}>
          Complete
        </button>
      </p>
    </article>
  );
}

At this point, the skeleton was in place. I knew auth was working and I could retrieve and render data from Firebase. It was time to start adding data from the app.

Connecting a Form to Firebase

The data (exercises) needed to be tied to an account and only able to be created by signed in users, so I added NewExercise and rendered it when there was a currentUser. commit

To save this data, I needed to get all the values from the inputs, assign them to keys/values that match up with the data structure and send that structure to Firebase. It turned out that local state was great for handling the first part.

I like to get one small bit working and then replicate, so I got the exercise name saving to state first.

import { h, Component } from 'preact';

export default class NewExercise extends Component {
  constructor() {
    super();

    this.state = {
      name: ''
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.setState({
      [e.target.name]: e.target.value
    });
  }

  render() {
    const name = this.state;
    return (
      <section>
        <h2>New Exercise</h2>

        <form>
          <div>
            <label for="name">Name</label>
            <input
              type="text"
              name="name"
              onChange={this.handleChange}
              placeholder="Chest Press"
              value={this.state.name}
            />
          </div>
        </form>
      </section>
    );
  }
}

I had learned this technique from tutorials. A function is used as a listener (handleChange) for changes to an input. When it’s changed, local state is updated. commit

With that working, I added the rest of the data inputs. This meant adding each as empty to state by default, assign them to a variable from state, and creating an input to listen for events. commit

My favorite little bit (and something I had recently learned) was chaining all of the items into one const.

const {
  name,
  setting,
  settingType,
  raiseAfter,
  raiseBy,
  reps
} = this.state;

With the data in state, I now could use that and Firebase’s push method to handle the submission of the form. The impressive thing about push is that it creates a unique identifier, so I don’t have to things like “exercise01”. To do this, I needed to import the database and allow NewExercise to have access to the current user’s UID. commit

This handleSubmit function blocks the default behavior of the form and sends data to the URL specified, which is based on the UID of the current user. commit

handleSubmit(e) {
  e.preventDefault();
  const exercisesRef = database.ref('/' + this.props.user.uid + '/exercises');
  
  exercisesRef.push({
    name: this.state.name,
    setting: this.state.setting,
    settingType: this.state.settingType,
    reps: this.state.reps,
    raiseAfter: this.state.raiseAfter,
    raiseBy: this.state.raiseBy
  });
}

After submitting the form, I could see that the core data structure was the same, with the added benefit of unique keys.

Adding to Existing Data in Firebase

Now that an exercise existed, I needed to hook up the inputs to add sets to it. Since the bulk of the data that I needed to interact with was in ExerciseList, it seemed best to add the functions there and pass them into Exercise.

Because I like to start small to make sure things are working properly, I started with failed exercises. These technically aren’t useful for anything in this version, but I know that I want to create sparklines of progress in the future, so I want to save the data behind the scenes for when I can’t complete a set. When that happens, I wanted to save the setting, the timestamp, and a boolean of false.

I created a handleFailed function with a slightly more complex URL to point to in Firebase. It looks up the user, then the setting of the current exercise, and pushes data to it. The function itself is passed into Exercise, which requires binding props in the constructor.

handleFailed(key) {
  const currentUser = this.props.user;
  const setting = this.props.exercises[key].setting;

  database
    .ref("/" + currentUser.uid)
    .child("exercises")
    .child(key)
    .child("/sets")
    .push({
      completed: false,
      completedDate: Date.now(),
      setting: setting
    });
}

The hard-coded user01 that I had used for ensuring the FB data worked earlier came back into play here, so I had to update it to use the current user as well: const exercisesRef = database.ref( '/' + this.state.currentUser.uid + '/exercises' );

Since I was looking for that UID too early, I had to move that within Firebase’s onAuthStateChanged method. commit

With that in place, clicking the Fail button adds data with a unique identifier to the current exercise.

Adding Data Conditionally

Adding data for completions required some logic, and I don’t love the organization of this function, but it works. The gist is that before sending the data, the total number of completions needs to be compared to the number of completions to “raise after.” Once the number of completions is 1 less than the “raise after,” it needs to push the completion data and update the setting by the “raise by” amount.

First things first, I created some constants to make the comparisons more readable.

const currentUser = this.props.user;
const raiseAfter = this.props.exercises[key].raiseAfter;
const raiseBy = this.props.exercises[key].raiseBy;
const setting = this.props.exercises[key].setting;
const completedCount = filter(this.props.exercises[key].sets, {
  setting: setting,
  completed: true
}).length;

To get the completed count, I needed to filter the results using lodash. Then I used them to create the basic push (when completed is at least 2 less than the amount to raise by).

if (completedCount < raiseAfter - 1) {
  database
    .ref("/" + currentUser.uid)
    .child("exercises")
    .child(key)
    .child("/sets")
    .push({
      completed: true,
      completedDate: Date.now(),
      setting: setting
    });
}

Because I was dealing with integers and decimals for settings and JavaScript hates math with decimals, I needed to tack on some methods to check for a decimal number and output differently when there is one. Thank goodness for Stack Overflow!

else {
  const newSetting = function checkForDecimal() {
    if (raiseBy.indexOf(".") === -1) {
      return Number(setting) + Number(raiseBy);
    } else {
      return (Number(setting) + Number(raiseBy)).toFixed(1);
    }
  }
}

When the number of completions is one less than the amount to complete before raising, completing one should add the completed data and raise the setting.

database
  .ref("/" + currentUser.uid)
  .child("exercises")
  .child(key)
  .child("/sets")
  .push({
    completed: true,
    completedDate: Date.now(),
    setting: setting
  });
database
  .ref("/" + currentUser.uid)
  .child("exercises")
  .child(key)
  .update({ setting: newSetting });

Similar to handleFailed, I then needed to pass a function into Exercise and call it on the button in there. commit

This part felt magical because it’s so fast. On clicking the “complete” button, it pushes the new setting. Because Preact is always listening for state changes, it grabs that new setting and renders it.

The final bit of functionality for this version was outputting an indicator that the data was updated. As a progress tracker, I wanted visual feedback of progress. To do this, I needed to use filter to only output completed sets and map to loop over the filtered results. commit

import { h, Component } from 'preact';
import { filter, map } from 'lodash';

export default class Exercise extends Component {
  constructor() {
    super();
  }

  render() {
    const {
      name,
      setting,
      settingType,
      sets,
      handleCompleted,
      handleFailed
    } = this.props;
    const filters = filter(sets, {
      setting,
      completed: true
    });

    return (
      <article>
        <h3>
          {name}
        </h3>
        <p>
          <div>{setting}</div> {settingType}
        </p>
        <p>
          <button onClick={handleFailed} setting={setting}>
  Fail
          </button>
          <button onClick={handleCompleted} setting={setting}>
  Complete
          </button>
        </p>
        <ul>
          {sets && map(filters, (filter, key) => <li key={key}>{key}</li>)}
        </ul>
      </article>
    );
  }
}

To verify it was working without digging through Dev Tools, I rendered a list with the key, knowing that I’d make that more like “eye candy” with CSS.

Adding Global Styles

CSS in JS is new to me, and I don’t have a clear methodology yet. However, this was small enough that it doesn’t matter much. I made a variables.scss for a few colors, and it felt like overkill for this, but something I would want to do for future projects.

$c-bg: #313743;
$c-bg-light: #30353f;
$c-negative: #a24335;
$c-positive: #7b9058;
$c-text: #ffffff;

I then added a font from Google Fonts, reset font weights, and added default styles to inputs and buttons. I usually set a variable for spacing and use rems, but I stayed with pixels. Those styles got it headed in the right direction, and I wanted to try component-level styles for the rest. commit

Adding Component-Level Styles

Individual Exercise

The individual exercise got the most work, and it felt a little weird to duplicate so many styles. I’m used to modifier classes, but it was also awesome to use basic names and have them get unique names automatically. commit

New Exercise Form

This form will have minimal use going forward, so I’ll likely tuck it away in a future version, but meanwhile, I wanted it to be a little more usable. commit

Current User

This part is just for sanity’s sake, so I just gave it a touch of alignment. commit

“Home”

The signed out experience needed some love, and I set a max-width in case I ever bring my laptop to the gym. (JK) commit

Cleaning Up Some SCSS

In my original version, I was designing in the browser, so the CSS was being written on the fly. In this, I went through and applied the variables just so that I know it can be done. commit

Save to Home Assets

This web app will be saved on my phone, and there are some settings and icons that can be displayed that make it feel native. I didn’t put much time into it but made the colors match and put an icon of some weights. I have some ideas for v2 that will make this more fun. commit

Deploying to Firebase

Firebase is perfect for side projects like this. In addition to all of the other features, hosting via HTTPS is part of the free plan. Their CLI makes it seamless, as well. Don't be like me and get so excited about it that you forget to build your app with Preact and deploy nothing, which is what I did just now.

Ok, Build First, Then Deploy

After running a build with preact build I saw that I had done something (probably the combo of Firebase and lodash) that added a lot of weight to my app. That will have to wait for later.

Lighthouse Test

The results of this test are why the CLI was valuable. I didn’t have to do any of the service worker and manifest setup, so I have a smooth 100. If you haven’t done that stuff before, you should try it manually, though.

The performance dropped over 10 points from the default, and I’ll have to figure out what is causing that. It won’t matter to me for this app, but I want to learn how to troubleshoot it. My gut says it’s a combo of the font request, the Firebase data request, and lodash.

Make It Better

At this point, it is usable, but not an experience I'd ship to others. When you auth, there is some downtime as it transitions, which feels janky. Once it’s loaded, it’s good, though. Goals for v2 improvements:

I also want to add some reporting because I’m all about that data, that data. It’d be overkill to do full graphs, but I want some sparklines that show when I struggle to advance a setting and counts of the total number of exercises done.

Thank You

If you made it this far, wow, thank you. When you’ve had a nice long rest, I’d love to know that you thought.

comments powered by Disqus