jnl – a daily goal journal

Here’s my average week day:

5:00 – Wake up, enter dream in dream journal, get ready

5:30 – Read a book (currently reading Long-Distance Real Estate Investing by David Greene)

6:00 – Work

8:00 – Eat something & journal it (using an app)

6:00 – 14:00 – Toilet time = WaniKani time (Japanese Kanji learning app)

11:30 – Read Japanese novel (currently the Japanese version of Psycho Pass which I bought in Japan).

14:00 – Personal time

19:30 – Journal time

  1. Review daily goal setting journal (what went well, what didn’t, and general notes about the day)
  2. Write down tomorrows goals
  3. Write 1 line diary entry summarizing the day

20:30 – Sleep

I really enjoy these habits. I plan to keep them, slightly tweaking them as I go. The point though is that I’m being very deliberate about them.

  • I want to read more, so I read before I start my work day.
  • I want to eat well, so I take pictures of everything I eat and have my wife give it a rating at the end of the day.
  • I use toilet time to do WaniKani reviews to constantly train my Japanese vocabularly and reading abilities.
  • I read a Japanese novel in order to become more fluent in written Japanese (language that doesn’t come up in while conversing with my wife)
  • I journal everyday to evaluate my progress as well as to make sure that these habits are serving me instead of me serving them.

They really only take about 1 hour of my total day but absolutely contribute to my overall feeling of being focused, prepared, and inspired.

But I wanna get even nerdier.

One thing that comes up a lot in my daily goal setting journal review is my thoughts about how I felt the day went. I used to “rate” it from 1 to 10, but I have been thinking that it would be really cool to be able to detect patterns in what makes a day “good” for me personally.

But in order to do that, I need data.

I figured that I need to add a new habit in order to collect this data. But I am at the point now where adding a new habit does feel a bit tedious. I would rather build a tool to consolidate all of my journaling at 19:30 into a 10 minute form with a couple button clicks. But it would be nice if it also was flexible enough to allow for modifying down the road (I’m talking years after the initial prototype). I do not want to use spreadsheets. I want a simple journaling tool that will replace most (but not all) of my journaling tasks. I think these are:

  • Review today’s goals
  • Set tomorrows goals and bigger goal(s) (something that you see everyday as you are setting your daily goals — mine is currently $100/mo of passive income)
  • Write quick summary of the day
  • Food score (1-5) – wife to determine
  • Rate the day – poor, average, great (I think numbers are hard to digest when “feeling” is involved. Poor is 1, average is 2, and great is 3 for data collection purposes)
  • Write notes (optional)

I won’t try and replace the features in the food journal app or the dream journal app app I’m using. I like the apps I am using a lot, and they are very low friction. I think it’s mostly the handcrafted goals and summaries that are time consuming, and not analyzable.

So what am I to do?

Build a SaaS app of course!

The requirements of this app are outlined above. We want to be flexible with changing things later, but also have the ability to use aggregate functions and other cool queries for reporting against the data.

Here’s the schema draft. We’re gonna use Prisma to handle the ORM and migrations:

model Epic {
  id         Int       @id @default(autoincrement())
  name       String
  by_date    DateTime?
  goals      Goal[]
  created_at DateTime  @default(now())
  updated_at DateTime  @default(now())
  updated_by String
}

model Entry {
  id           Int      @id @default(autoincrement())
  author_id    String // nickname (but will be FK later)
  dream        String   @default("")
  reading      String   @default("")
  summary      String   @default("")
  sleep        Int      @default(2)
  diet         Int      @default(2)
  productivity Int      @default(2)
  mood         Int      @default(2)
  notes        String?
  goals        Goal[]
  created_at   DateTime @default(now())
  updated_at   DateTime @default(now())
}

model Goal {
  id          Int     @id @default(autoincrement())
  entry_id    Int
  entry       Entry   @relation(fields: [entry_id], references: [id])
  epic_id     Int?
  Epic        Epic?   @relation(fields: [epic_id], references: [id])
  completed   Boolean @default(false)
  description String
}

Which should help us record all the data that we need for the queries (which are TBD).

I want to use SvelteKit for this project since I really enjoy building frontend UIs with Svelte. We’re going to use the endpoints feature to build out some APIs that return some data from our database, and then we’ll build out the pages and use the load function to properly fetch the data for the page.

Lets get started!

First, I will initialize my project with SvelteKit. At the time of this blog post writing, the way to do that is:

mkdir jnl
cd jnl
npm init svelte@next
npm install
npm run dev

Which gives us a bunch of files for our SvelteKit project. Super. I’ve gone ahead and already created the Prisma database and created this file in the project:

// src/lib/db/index.js
import { PrismaClient } from '@prisma/client/index.js'

const db = new PrismaClient()

export default db

This will allow me to query the SQLite database by simply doing something like:

// src/routes/entries/index.json.js
import db from '$lib/db'
import response from '$lib/response'

export async function get() {
  try {
    const entries = await db.entry.findMany({
      include: { goals: true },
      orderBy: { updated_at: 'desc' },
    })
    return response.success(entries)
  } catch (err) {
    return response.error(500, err.message)
  }
}

Notice that SvelteKit gives me a cool alias out of the box to the src/lib directory through $lib. Neat!

Now that we have an endpoint to fetch journal entries from, lets build the UI to render the entries in a list.

SvelteKit allows us to “load” the data before the page is actually rendered. In order to do this, the component needs to export a load function that returns a LoadOutput type. Basically the three fields that I personally care about for this are props, status, and error. Essentially, if the request succeeds we will put the data into a prop using props. If it fails, we will set the status and the error fields and return it.

<script context="module">
  import response from '$lib/response'

  export default async function ({ fetch }) {
    const url = '/entries.json'
    const res = await fetch(url)

    if (res.ok) {
      return {
        props: {
          entries: await res.json()
        }
      }
    }

    return response.error(res.status, `Could not load ${url}`)
  }
</script>

Now all I have to do is access the entries prop in the Svelte page component and my data should be there!

<script context="module">
  import response from '$lib/response'

  export default async function ({ fetch }) {
    const url = '/entries.json'
    const res = await fetch(url)

    if (res.ok) {
      return {
        props: {
          entries: await res.json()
        }
      }
    }

    return response.error(res.status, `Could not load ${url}`)
  }
</script>

<script>
  export let entries = [];
</script>

<div>
  {#each entries as entry}
    <p>{entry.summary}</p>
  {/each}
</div>

Now I like to DRY up my code here, so I’m going to pull that load function out so that I can reuse that elsewhere. The main parameters that are variable (pun intended) are url and the set propName.

// src/lib/loaders/get.js
import response from '$lib/response'

export default ({ url, propName }) => async function ({ fetch }) {
  const res = await fetch(url)

  if (res.ok) {
    return {
      props: {
        [propName]: await res.json()
      }
    }
  }

  return response.error(res.status, `Could not load ${url}`)
}

So now I’m able to use this basic get load function for Svelte components like:

<script context="module">
  import get from "$lib/loaders/get";

  export const load = get({ url: `/entries.json`, propName: 'entries' })
</script>

<script>
  export let entries = [];
</script>

<div>
  {#each entries as entry}
    <p>{entry.summary}</p>
  {/each}
</div>

Much cleaner!

I love that Svelte just disappears and allows you to do exactly what you want to do without getting in the way. This is a great example of no boilerplate!

Lets add some more personality to the that list component page:

// src/lib/components/entries/entry-row.svelte
<script>
  import { format } from 'date-fns';

  // props

  export let entry = {}

  //

  const { id, created_at, sleep, diet, productivity, mood, summary } = entry

  function defaultRenderer(v) {
    return ({ 1: 'bad', 2: 'ok', 3: 'great' })[v]
  }
</script>

<div class="m-2 p-2 border-2 rounded">
  <a href={`/entries/${id}`}>
    <h5 class="flex text-xs">
      <span class="flex-initial underline">
        {format(new Date(created_at), 'MMMM d, yyyy')}
      </span>
      <div class="ml-2">
        {#each [
          ['🛏', sleep],
          ['🍱', diet],
          ['📈', productivity],
          ['😁', mood],
        ] as [label, value, renderer = defaultRenderer]}
          <span class="mr-2">&nbsp;{label}: {renderer(value)}</span>
        {/each}
      </div>
    </h5>
    <div class="mt-2">{summary}</div>
  </a>
</div>
// src/routes/entries/index.svelte
<script context="module">
  import EntryRow from "$lib/components/entries/entry-row.svelte";
  import get from "$lib/loaders/get";

  export const load = get({ url: `/entries.json`, propName: 'entries' })

</script>

<script>
  export let entries = [];
</script>

<div>
  {#each entries as entry}
    <EntryRow {entry} /> // Actually use the new component
  {/each}
</div>
Note: I seeded the database before rendering the list here.

Now that we have a list rendering all pretty, lets get the single entry endpoint working so that we might also use that same page to create a new entry.

// src/entries/[entryId].json.js
import db from '$lib/db'
import response from '$lib/response'

export async function get({ params }) {
  try {
    if (!params.entryId) {
      throw new Error('No entry id provided')
    }

    const entry = await db.entry.findFirst({
      where: { id: Number(params.entryId) },
      include: { goals: true },
    })
    return response.success(entry)
  } catch (err) {
    return response.error(500, err.message)
  }
}

And then we’ll create the Svelte page component, using our get loader from before.

// src/routes/entries/[entryId].svelte
<script context="module">
  import EntryRow from "$lib/components/entries/entry-row.svelte";
  import Link from "$lib/components/link.svelte";
  import get from '$lib/loaders/get';

  export const load = get({
    url: ({ params }) => `/entries/${params.entryId}.json`,
    propName: 'entry',
  })
</script>

<script>
  export let entry
</script>

<div class="m-2 p-2">
  <div>
    <Link href="/entries">All entries</Link>
  </div>
  {#if entry}
    <div class="mt-2">
      <EntryRow {entry} />
      <h5 class="mt-2">Goals:</h5>
      <ul class="list-inside list-disc">
        {#each entry.goals as { description, completed }}
          <li>
            <span class:line-through={completed}>{description}</span>
            {#if completed} ✅{/if}
          </li>
        {/each}
      </ul>
    </div>
  {/if}
</div>

Note the class names like mt-2, list-inside which are from Tailwind which plays beautifully with Svelte and is added very easily to SvelteKit through this tool.

Now that we are able to go back to see all of our entries, and click in to see a single entries with goals, it’s about time that we add the ability to create and edit entries.

Normally when I create an entry, I am creating it at night after I have reviewed the goals I had set the previous day, crossing them off if I complete them. With this in mind, I would like the ability to update the entry at any time. This is the likely use case:

  1. Wake up, write down dream
  2. Read book, write down takeaways
  3. Work
  4. Personal time
  5. Nightly review: review goals, mark complete, write notes, write summary
  6. Create entry for tomorrow and write goals

So we see that the creation for today’s entry actually happens the previous night, which is an interesting design problem. What I think is the best solution here is to just default the entry date to the next day upon creation, but to allow editing the date afterwards on the edit entry page. I also want to have the view single entry page be editable in-place and with a save button somewhere to persist the changes This will hopefully reduce clicks and UI clutter with modals and stuff, as well as guard against accidental mistypes/misclicks.

First, lets tackle the create entry button. We’ll need to add a new function to our endpoint:

// src/routes/entries/index.json.js
import db from '$lib/db'
import response from '$lib/response'

export async function get() {
  // ...
}

export async function post(request) {
  try {
    const data = JSON.parse(request.body)
    const newEntry = await db.entry.create({ data })
    return response.success(newEntry)
  } catch (err) {
    return response.error(500, err.message)
  }
}

Then create a button that does the creation.

// src/lib/components/entries/create-entry-button.svelte
<script>
  export let authorId = ''

  let err = ''

  async function handleCreateEntry() {
    try {
      const { id } = await (await fetch('/entries.json', {
        method: 'POST',
        body: JSON.stringify({
          author_id: authorId,
        })
      })).json()
      window.location.assign(`/entries/${id}`)
    } catch (err) {
      err = err.message
    }
  }
</script>

<button on:click={handleCreateEntry}>
  Create Entry
</button>
<div>{err}</div>

Now we gotta use it.

// src/routes/entries/index.svelte
<script context="module">
  import CreateEntryButton from "$lib/components/entries/create-entry-button.svelte";
  import EntryRow from "$lib/components/entries/entry-row.svelte";
  import get from "$lib/loaders/get";

  export const load = get({ url: `/entries.json`, propName: 'entries' })
</script>

<script>
  export let entries = [];
</script>

<div>
  // I plan to make this authorId a setting later
  <CreateEntryButton authorId="pattycakes" />
  {#each entries as entry}
    <EntryRow {entry} />
  {/each}
</div>

Ugly, but it works!

Now we should create the put endpoint for updating entries. I want to morph the current page into something moa betta for this.

Here’s the new look:

I want to remove as much friction as I can from the nightly process of creating journal entries, and I think this will do for now. Here’s the code:

// src/routes/entries/[entryId].svelte
<script context="module">
  import ActionButton from "$lib/components/action-button.svelte";
  import RatingSlider from "$lib/components/entries/rating-slider.svelte";
  import Link from "$lib/components/link.svelte";
  import Section from "$lib/components/section.svelte";
  import get from '$lib/loaders/get';

  let entryId

  export const load = get({
    url: ({ params }) => {
      entryId = params.entryId
      return `/entries/${params.entryId}.json`
    },
    propName: 'entry',
  })
</script>

<script>


  export let entry = {}

  let error = ''
  let saving = false

  function handleKeyUp(goalIdx, e) {
    console.log(e)
    if (e.key === 'Backspace' && !entry.goals[goalIdx].description) {
      const newGoals = [...entry.goals]
      newGoals.splice(goalIdx, 1)
      entry.goals = newGoals
    } else if (e.key === 'Enter') {
      handleCreateGoal()
    }
  }

  async function handleCreateGoal() {
    console.log('handleCreateGoal')
    entry.goals = (entry.goals || []).concat({ entry_id: entry.id, completed: false, description: '' })
    console.log(entry.goals)
  }

  async function handleSave(e) {
    e.preventDefault()

    saving = true
    try {
      await (await fetch(`/entries/${entryId}`, {
        method: 'PUT',
        body: JSON.stringify(entry),
      }))
    } catch (err) {
      error = err.message
    }
    saving = false
  }
</script>

<div class="m-2 p-2">
  <div>
    <Link class="underline" href="/entries">All entries</Link>
  </div>

  {#if entry}
    <div class="mt-2">

      <Section>
        <span slot="title">Goals</span>
        <ul class="mt-2 list-inside">
          {#each entry.goals as goal, i}
            <li class="text-3xl flex flex-1">
              <input type="checkbox" class="h-auto" checked={goal.completed} on:click={() => goal.completed = !goal.completed} />
              <input type="text" class="mt-2 ml-2 text-xl flex-1" class:line-through={goal.completed} placeholder="Enter your goal" bind:value={goal.description} on:keyup={handleKeyUp.bind(null, i)} />
            </li>
          {/each}
        </ul>
        <ActionButton class="mt-4 p-0 pl-2 pr-2" on:click={handleCreateGoal}>Add New</ActionButton>
      </Section>

      <Section>
        <div slot="title">Morning</div>
        <div class="mt-2 p-2 flex"><RatingSlider {entry} field="sleep" /></div>
        <div class="flex border-2">
          <textarea class="flex-1 p-2" placeholder="Write down your dream..." bind:value={entry.dream} />
        </div>
        <div class="mt-2 flex border-2">
          <textarea class="flex-1 p-2" placeholder="Daily reading notes..." bind:value={entry.reading} />
        </div>
      </Section>

      <Section>
        <div slot="title">Evening</div>
        <div class="mt-2 flex border-2">
          <textarea class="flex-1 p-2" placeholder="Write down your day..." bind:value={entry.summary} />
        </div>
        <div class="mt-2 p-2 flex"><RatingSlider {entry} field="diet" /></div>
        <div class="mt-2 p-2 flex"><RatingSlider {entry} field="productivity" /></div>
        <div class="mt-2 p-2 flex"><RatingSlider {entry} field="mood" /></div>
      </Section>

    </div>
    <div class="mt-6 flex">
      <ActionButton type="submit" class="flex-1" on:click={handleSave}>
        {saving ? 'Saving...' : 'Save'}
      </ActionButton>
      <div class="red">{error}</div>
    </div>
  {/if}

</div>

So now we have the frontend for each journal entry, now we have to hook it up by creating that PUT endpoint. There’s work to be done!

// src/routes/entries/[entryId].json.js
import db from '$lib/db'
import response from '$lib/response'

/**
 * @type {import('@sveltejs/kit').RequestHandler}
 */
export async function get({ params }) {
  // ...
}

/**
 * @type {import('@sveltejs/kit').RequestHandler}
 */
export async function put({ params, body }) {
  try {
    if (!params.entryId) {
      throw new Error('No entry id provided')
    }

    const { goals: goalInput, ...data } = JSON.parse(body)
    const entry = await db.entry.update({
      data,
      where: { id: Number(params.entryId) },
    })
    const goals = []
    for (const g of goalInput) {
      if (g.id) {
        goals.push(await db.goal.update({
          data: g,
          where: { id: g.id },
        }))
      } else {
        goals.push(await db.goal.create({ data: g }))
      }
    }
    return response.success({ ...entry, goals  })
  } catch (err) {
    return response.error(500, err.message)
  }
}

I was hoping that I would be able to use Prisma to help me do some magic to not have to do updates by hand for the nested field goals, but alas that was not done for me. At least it’s working now!

The last thing I really want to add (besides some minor touchups and cosmetic changes) is the delete entry button:

// src/lib/components/entries/delete-entry-button.svelte
<script>
  import { createEventDispatcher } from "svelte";
  import ActionButton from "../action-button.svelte";

  const dispatch = createEventDispatcher()

  export let entryId = ''
  let deleting = false
  let error = ''

  async function handleDelete() {
    try {
      await fetch(`/entries/${entryId}.json`, { method: 'DELETE' })
      dispatch('delete', { entryId })
    } catch (err) {
      error = err.message
    }
  }
</script>


<ActionButton {...$$props} on:click={handleDelete}>
  { deleting ? 'Deleting...' : 'Delete' }
</ActionButton>
<div>{error}</div>

These modular components are so powerful… Now time to include it in our detail page at the top:

// src/routes/entries/[entryId].svelte
// ...
  <div class="flex justify-between">
    <div>
      <Link class="underline" href="/entries">All entries</Link>
    </div>
    <div>
      <DeleteEntryButton {entryId} on:delete={() => window.location.assign('/entries')}/>
    </div>
  </div>
// ...

Sweet! Although this doesn’t do anything at the moment, we need to create that endpoint.

// src/routes/entries/[entryId].json.js
import db from '$lib/db'
import response from '$lib/response'

export async function get({ params }) { ... }
export async function put({ params, body }) { ... }

export async function del({ params }) {
  try {
    if (!params.entryId) {
      throw new Error('No entry id provided')
    }

    await db.goal.deleteMany({ where: { entry_id: Number(params.entryId) } })
    const entry = await db.entry.delete({
      where: { id: Number(params.entryId) },
    })

    return response.success(entry)
  } catch (err) {
    return response.error(500, err.message)
  }
 }

Fantastic. We are all up and running. For good measure, I’d like to be able to edit the date on the entry just in case things get out of step. Here is that code:

// src/routes/entries/[entryId].svelte
...
     <div>
        <label for="entryDate">Entry Date:</label>
        <input type="date" name="entryDate" placeholder="Entry date"
          value={format(new Date(entry.date), 'yyyy-MM-dd')}
          on:change={({ target: { value } }) => {
            const newDate = new Date(value)
            entry.date = new Date()
            entry.date.setFullYear(newDate.getUTCFullYear())
            entry.date.setMonth(newDate.getUTCMonth())
            entry.date.setDate(newDate.getUTCDate())
          }} />
      </div>
...

I had to get a little wacky with how I was preserving the timezone offset for JavaScript dates, since I want to stay granola and not pull in any date libraries other than date-fns.

And there you have it! Our app is running, fully CRUDable, and ready for a beating. There will absolutely be more features to add and edge cases to handle, but that’s the gist.

We learned how to create a real SvelteKit app. One that provides API endpoints that act with a database, loaders to populate page component props with data, all within SvelteKits magical filesystem routing system!

Check it out in your own terminal with:

npm i -g jnl
jnl

You have a gander at the whole project here. Note that considering I now use this project as my daily journaling tool, it is subject to change (maybe entirely) and therefore it may not reflect this blog post at all depending on when you are reading this. But at least its roots started here, and that’s what counts!