# Creating a web app from start to finish

![Our web app "Remindly"](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FJ1lgSxMTHdxVVQyabBgS%2Fremindly.jpeg?alt=media\&token=1930fded-1c02-467d-a2f5-19cb05bedfe5)

{% hint style="info" %}
**Note:** This tutorial was written using Petal Pro v1.5.0.&#x20;
{% endhint %}

## Getting up and running

### What you need

For this tutorial you will need:

* Access to Petal Pro (you can purchase it on [petal.build](https://petal.build))
* Elixir & Erlang installed (you can follow our [installation guide](https://docs.petal.build/petal-pro-documentation/v1.5.0/fundamentals/installation) to get up and running)
* PostgreSQL running: [Postgres.app](https://postgresapp.com) works well for Mac or we have a docker-compose.yaml file that will run Postgres for you
* For deployment (optional) you will need:
  * An account on [Fly.io](https://fly.io/)
  * An [Amazon Web Services account](https://aws.amazon.com/developer/?developer-center-activities-cards.sort-by=item.additionalFields.startDateTime\&developer-center-activities-cards.sort-order=asc) with [Simple Email Service](https://docs.aws.amazon.com/ses/latest/dg/setting-up.html) (SES) for emails (you can replace with another service if you like - it just won't be covered in this guide)

### Download

Head over to the [projects page](https://petal.build/pro/projects) and create a new project, then download v1.5.0. You can unzip it in your terminal with the command `unzip petal_pro_1.5.0.zip`.

### Renaming your project

Once it's unzipped we can go ahead and rename the folder to whatever we want to call it. In our case, we're going to call it "remindly".

{% code title="Terminal" %}

```
mv petal_pro_1.5.0 remindly
```

{% endcode %}

While this changes the folder name, we still need to change the name of all the files and module names (eg. `petal_pro_web.ex` should be `remindly_web.ex`, etc). Petal Pro allows you to easily rename your project with a bash script. Open up `rename_phoenix_project.sh` and you'll see some instructions:

{% code title="rename\_phoenix\_project." %}

```bash
...

# 1. Ensure you've checked your files into a git repo (`git init .`, `git add -A`, `git commit -m 'first'`)
# 2. Modify these two values to your new app name
NEW_NAME="YourAppName"
NEW_OTP="your_app_name"
# 3. LINUX USERS ONLY - Scroll down and comment out lines 25/25 and uncomment lines 29/30
# 4. Execute the script in terminal: `sh rename_phoenix_project.sh`

...
```

{% endcode %}

Let's follow these instructions in our terminal:

{% code title="Terminal" %}

```
git init .
git add -A
git commit -m 'first'
```

{% endcode %}

Now, let's put our desired name ("remindly") into the script:

{% code title="rename\_phoenix\_project.sh" %}

```
...

# 1. Ensure you've checked your files into a git repo (`git init .`, `git add -A`, `git commit -m 'first'`)
# 2. Modify these two values to your new app name
NEW_NAME="Remindly"
NEW_OTP="remindly"
# 3. LINUX USERS ONLY - Scroll down and comment out lines 25/25 and uncomment lines 29/30
# 4. Execute the script in terminal: `sh rename_phoenix_project.sh`

...
```

{% endcode %}

And finally, run the script:

{% code title="Terminal" %}

```bash
sh rename_phoenix_project.sh
```

{% endcode %}

If it produces no output, then you know it's run correctly. You can verify by looking at your git changes - there should be a lot of changes and file renaming.

{% hint style="info" %}
**For Linux users**: as per the instructions you will need to do an additional task - simply follow the instructions in the script
{% endhint %}

If you like you can commit this renaming to git so you have a fresh starting point.

```bash
git add -A
git commit -m "Renamed to remindly"
```

### Test run

Let's get the server running so we can see Petal Pro in action. Simply run in your terminal:

```bash
mix setup
mix phx.server
```

And now navigate to `http://localhost:4000` in your browser to see the dummy landing page:

![The Petal Pro dummy landing page](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FqGLwMqcyTJiuz6orGtc2%2Fpetal_pro_landing.png?alt=media\&token=d4141300-8057-4074-b31d-b52188582b19)

Feel free to have a look around. The `mix setup` command helpfully ran our `seeds.ex` file, which if you open you will see this code:

```elixir
alias Remindly.Accounts.{User, UserToken, UserTOTP, UserSeeder}
alias Remindly.Logs.Log
alias Remindly.Orgs.OrgSeeder
alias Remindly.Orgs.{Org, Membership, Invitation}

if Mix.env() == :dev do
  Remindly.Repo.delete_all(Log)
  Remindly.Repo.delete_all(UserTOTP)
  Remindly.Repo.delete_all(Invitation)
  Remindly.Repo.delete_all(Membership)
  Remindly.Repo.delete_all(Org)
  Remindly.Repo.delete_all(UserToken)
  Remindly.Repo.delete_all(User)

  admin = UserSeeder.admin()

  normal_user =
    UserSeeder.normal_user(%{
      email: "user@example.com",
      name: "Sarah Cunningham",
      password: "password",
      confirmed_at: Timex.now() |> Timex.to_naive_datetime()
    })

  org = OrgSeeder.random_org(admin)
  Remindly.Orgs.create_invitation(org, %{email: normal_user.email})

  UserSeeder.random_users(20)
end
```

The `UserSeeder.admin()` command has created an admin for us. You can sign in as this admin with "<admin@example.com>" and the password "password". We won't explain too much here, as you'll touch on different parts of Petal Pro throughout different parts of this tutorial.

#### Getting tests to work

Run `mix test` to run the tests. They should be all green.

Petal Pro also comes with some end-to-end (E2E) tests (see `signup_test.exs`). It doesn't run as part of the normal test suite because it's a bit slow.&#x20;

In order for E2E tests to work, you will need to install [Chromedriver](https://chromedriver.chromium.org/downloads), which will run the tests like you were clicking things in a browser. On a Mac you can install it with:

```
brew install --cask chromedriver
```

Then to run the E2E tests run `mix test --only feature`. Or you could use our alias `mix wallaby` (see aliases defined in `mix.exs`).

On Mac, you may get the error:

> Error: “chromedriver” cannot be opened because the developer cannot be verified. Unable to launch the chrome browser

&#x20;If this happens, you can run this command:

```
xattr -d com.apple.quarantine $(which chromedriver)
```

### Adding your brand

#### Configuration

Obviously, the Petal branding will need to be replaced by your own brand. If you do a global search for "SETUP\_TODO" you will find the code you need to modify.

![Search for SETUP\_TODO to see what you need to change](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FbAm7TfnfHBJUtGGumRcP%2FCleanShot%202022-05-28%20at%2008.56.11%402x.png?alt=media\&token=526decac-292b-4f97-8e20-543af5626328)

The first two mentions you can ignore; one is in the README which is similar to this tutorial. The other is in `rename_phoenix_project.sh`, which we've already done. The 3rd match is in our `config.exs` file. Here we can update our web apps details:

{% code title="config.exs" %}

```elixir
...

# SETUP_TODO - ensure these details are correct
# Option descriptions:
# app_name: This appears in your email layout and also your meta title tag
# support_email: In your transactional emails there is a "Contact us" email - this is what will appear there
# mailer_default_from_name: The "from" name for your transactional emails
# mailer_default_from_email: The "from" email for your transactional emails
# dark_logo_for_emails: Your logo must reference a URL for email client (not a relative path)
# light_logo_for_emails: Same as above
# seo_description: Will go in your meta description tag
# css_theme_default: Can be "light" or "dark" - if "dark" it will add the class "dark" to the <html> element by default
config :remindly,
  app_name: "Remindly",
  business_name: "Remindly Pty Ltd",
  support_email: "matt@petal.build",
  mailer_default_from_name: "Remindly",
  mailer_default_from_email: "matt@petal.build",
  logo_url_for_emails: "https://res.cloudinary.com/wickedsites/image/upload/v1646277219/petal_marketing/remindly_logo_tsvksl.png",
  seo_description: "Reminder app",
  twitter_url: "https://twitter.com/PetalFramework",
  github_url: "https://github.com/petalframework",
  discord_url: "https://discord.gg/exbwVbjAct"
...
```

{% endcode %}

#### Logo

The next SETUP\_TODO is found in the file `brand.ex`.&#x20;

```elixir
  # SETUP_TODO
  # This module relies on the following images. Replace these images with your logos
  # /priv/static/images/logo_dark.svg
  # /priv/static/images/logo_light.svg
  # /priv/static/images/logo_icon_dark.svg
  # /priv/static/images/logo_icon_light.svg
  # /priv/static/images/favicon.png
```

You don't need to change anything in this file, but rather it's asking you to update the logos with your own. For this tutorial, I went into Figma and just wrote "Remindly" in an interesting font. You can duplicate [my Figma file](https://www.figma.com/file/GKXx5wG22gSp6ljrAZ7HBb/Remindly?node-id=1%3A3) and replace with your logo if you like.

![Creating a logo in Figma](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FBUnVJgEjbSdx1IfJCFMq%2Fremindly_figma.png?alt=media\&token=584e41c7-27d2-44ec-baae-757f42b2dd70)

The first 4 logos should be SVG. You can select them and export as SVG:

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2Fn0zglEp7kaxVg7qivYvr%2FCleanShot%202022-10-09%20at%2010.29.25%402x.png?alt=media&#x26;token=41351ab5-8196-4313-98fb-fa4ae530d78e" alt=""><figcaption></figcaption></figure>

The rest can all be exported PNG.

Once you've exported them all you can drag them into your project:

![Import your new logo files](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FqsW8TlvZRihP8oWmO5F7%2Flogo_files.png?alt=media\&token=f9a72526-6f4c-4156-839e-d08be83a8af2)

{% hint style="info" %}
**Note** that in case you missed it - since emails can be opened in all kinds of email clients, the logo must be hosted online somewhere.&#x20;
{% endhint %}

I recommend uploading your logo in png format to some kind of image hosting you like. We personally like Cloudinary so let's upload our logo and then paste the URL into our config file. After you've done this, you can just paste the link in and your emails should reflect your new logo. We'll also discuss this later in the "Reminder emails" section of this tutorial.

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FM6YtXNZ2LMcrHJmqi3EU%2FCleanShot%202022-10-12%20at%2015.51.55%402x.png?alt=media&#x26;token=7192d208-87b1-425f-b39c-6786c97db7c9" alt=""><figcaption><p>Host your email logo somewhere and paste the link </p></figcaption></figure>

Now reload your browser and check out your new logo.

The other two SETUP\_TODO's are to do with adding your license and privacy policy content as most SAAS applications will require them. You can add this content in pure markdown and we've provided some dummy content as an example to get you started.

![Add your license and privacy policy content in page\_view.ex](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FTRIbVvOvReO0isLvlFVp%2FCleanShot%202022-05-28%20at%2009.28.46%402x.png?alt=media\&token=308248f5-5e18-4384-8fbe-46d981b44cc8)

#### Tailwind primary and secondary colors

Petal Components relies heavily on having a primary and secondary brand color set in your `tailwind.config.js` file.&#x20;

By default, primary is blue and secondary is pink:

{% code title="tailwind.config.js" %}

```javascript
const colors = require("tailwindcss/colors");

module.exports = {
  content: [
    "../lib/*_web.ex",
    "../lib/*_web/**/*.*ex",
    "../lib/_petal_framework/web/**/*.*ex",
    "./js/**/*.js",
    "../deps/petal_components/**/*.*ex",
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: colors.blue,
        secondary: colors.pink,
      },
    },
  },
  plugins: [
    require("@tailwindcss/typography"),
    require("@tailwindcss/forms"),
    require("@tailwindcss/line-clamp"),
    require("@tailwindcss/aspect-ratio"),
  ],
};
```

{% endcode %}

![Buttons in Petal Components - the first two colors are up to you](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FZc5Ek0dPf6wv5pCUgRIs%2FCleanShot%202022-10-12%20at%2013.44.13%402x.png?alt=media\&token=c27ab89e-61df-498a-9f4e-2e4044516048)

You can pick any two colors from [Taildwind's comprehensive list](https://tailwindcss.com/docs/customizing-colors). Or if you want to go custom, you can try out [this site](https://uicolors.app/create).

In our case, we'll go for the colors "amber" and "slate":

{% code title="tailwind.config.js" %}

```javascript
...
  theme: {
    extend: {
      colors: {
        primary: colors.amber,
        secondary: colors.slate,
      },
    },
  },
...
```

{% endcode %}

If you refresh your landing page, you'll notice the color change.

![Now we have our own logo plus some new primary and secondary colors](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FhGnmPwVMYOLN1VFjJzAi%2FCleanShot%202022-05-12%20at%2014.33.07.png?alt=media\&token=9aa37874-7926-4fba-a513-ad303dfb5925)

## Public-facing pages

### Landing page

The landing page consists of content from two core files:

* `lib/remindly_web/components/landing_page.ex` - a set of components like `<.hero>`, `<.features>`, `<.header>` etc. The end goal of Petal Pro is to have a comprehensive set of landing page components that can be used as building blocks for your landing page. Ideally, you won't modify these too much but will use them just like Petal Components. For now, you can modify them as you wish.
* `/lib/remindly_web/templates/page/landing_page.html.heex` - a template file that uses the components from `landing_page.ex`

To keep this tutorial moving quickly, we won't do much design work on the landing page - if you open up `landing_page.html.heex` we can strip it down to merely a hero component:

{% code title="landing\_page.html.heex" %}

```html
<.layout
  type="public"
  current_user={assigns[:current_user]}
  current_page={:landing}
>
  <LandingPage.hero
    image_src={Routes.static_path(@conn, "/images/landing_page/hero.svg")}
    logo_cloud_title={gettext("Trusted by brands all over the world")}
  >
    <:title>
      <span><%= gettext("Always be") %></span>
      <span class="text-primary-600">
        <%= gettext("reminded") %>
      </span>
      <span><%= gettext("to do things") %>.</span>
    </:title>
    <:action_buttons>
      <.button
        label={gettext("Get started")}
        link_type="a"
        color="primary"
        to={Routes.user_registration_path(@conn, :new)}
        size="lg"
      />

      <.button
        label={gettext("Guide")}
        link_type="a"
        color="white"
        to="https://docs.petal.build"
        size="lg"
      />
    </:action_buttons>
    <:description>
      <%= gettext(
        "To be a functioning adult you need to be reminded to do adult-like tasks, like getting a hair cut or going to the dentist."
      ) %>
    </:description>
  </LandingPage.hero>
</.layout>

<LandingPage.load_js_animations />

```

{% endcode %}

Now refresh your landing page and it should be a lot more simple. It is up to you how detailed you want to be with your landing page.

![Our rather basic landing page](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FADT8KErtcMJ8X8Dtq2H9%2Fimage.png?alt=media\&token=a40fd646-df16-4bb3-91c0-fb01ad9eb82b)

### About page

To complement the landing page it would be nice to have an "About" page. This is a good chance to show off Petal Pro's "Page Builder" functionality.&#x20;

Usually, when you want to create a new page, you would have to create a new template file or live view file. Then you would create a route and point it either to the template's corresponding controller action or directly to the live view which all takes valuable minutes. To skip these steps, we made it so that if you navigate to a path with no route on it then it will show what we call "The Page Builder" 👏. &#x20;

The Page Builder is simply a form that helps you construct a page at that route. Submitting the form will result in the route being automatically added to the `router.ex` file and either a template or live view file is created for you.

Let's try it out. In your browser go to "[localhost:4000/about](http://localhost:4000/about)". This page won't exist so the "Page Builder" will load up. Here is where we can input the details of our new page.

We'll make a change before submitting: for "Page type", change to "Traditional static view" - we won't be needing any live view functionality.

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2F0wh0vmy4y14f4M0B5WPO%2FCleanShot%202022-10-09%20at%2010.58.55%402x.png?alt=media&#x26;token=26a81acf-a096-48e1-a1a2-5c39e62de09b" alt=""><figcaption></figcaption></figure>

After submitting the form the page should refresh itself because the `about.html.heex` template file has been added to the file system. The "Page Builder" has done 3 things for us:

1. Added a new "/about" route in `router.ex`
2. Added a new `about/2` function in `page_controller.ex`&#x20;
3. Added a new template `about.html.heex`&#x20;

![About page done!](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2F29B9z7A9S6Xr966aIfNZ%2FXnapper-2022-10-12-13.59.08.png?alt=media\&token=f5760ef2-8cef-44ea-a97e-d61f88db7877)

Now it should look more like our landing page. Obviously, it looks unfinished, but we've shown how you can use "Page Builder" to speed up your development for these stray pages.

{% hint style="info" %}
How does the Page Builder know where to insert the route? If you look inside the `router.ex` file, you will see some comments spread out through the file that looks like this: `page_builder:live:public` .  Any comment like this is like a target for the Page Builder to add routes beneath it. When you load the Page Builder it will scan the router for these strings starting with "page\_builder:" and then allow you to select one of them from a list. Whichever one you select, the route will be added AFTER that line.

You can add more route targets that Page Builder will pick up - just write a comment in the format `page_builder:<type>:<name>`. The `type` must be `live` or `static`. The `name` can be whatever you like, eg: `pagebuilder:live:my_special_routes`.
{% endhint %}

Let's change our menu items to include this about page. The public menu can be found in the file `menus.ex` in a function `public_menu_items/1`. Modify it to this:

{% code title="menus.ex" %}

```markup
...

# Public menu (marketing related pages)
def public_menu_items(_),
  do: [
    %{label: gettext("About"), path: "/about"},
  ]

...
```

{% endcode %}

And now there should be a menu item in the navbar:

![Our nav will update automatically after changing public\_menu\_items](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FmFfM6FfJRe0qe0ZCF3c5%2FCleanShot%202022-03-13%20at%2007.32.02.png?alt=media\&token=67cb6833-7dba-4843-894b-c4139c578934)

You can learn more about menus [here](https://docs.petal.build/petal-pro-documentation/v1.5.0/fundamentals/layouts-and-menus).

## The reminders page

Now that the public-facing side of things is looking good enough, we can work on the app itself.&#x20;

When a user signs in we want them to see a list of their reminders that they can check off or delete, with a button to create new ones. Normally this would be a job for a Phoenix generator - luckily Petal Pro has its own version of the `mix phx.gen.live` generator - `mix petal.gen.live`. Our generator produces the same files as the Phoenix one, but the resulting HTML will use Tailwind and Petal Components so we don't have to worry about styling.&#x20;

### Using petal.gen.live

Let's run the generator to create the following schema:

| Field     | Type                     |
| --------- | ------------------------ |
| label     | `:string`                |
| due\_date | `:date`                  |
| is\_done  | `:boolean`               |
| user\_id  | `:integer` (foreign key) |

```
mix petal.gen.live Reminders Reminder reminders label:string due_date:date is_done:boolean user_id:references:users
```

The command will output the routes needed to make the new pages work. Highlight them and just copy them to your clipboard for now.

Let's run the migration the generator just created:

```
mix ecto.migrate
```

Now we need to add these routes to the router. We can use the Page Builder tags to help us know where to put them. Do a search for "page\_builder:live:authenticated" to see live routes that require a logged in user.&#x20;

Let's paste the routes we copied from the generator command below that line.

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FboAXsraMr1Wo9FrBfTXi%2FCleanShot%202022-10-09%20at%2014.17.44.png?alt=media&#x26;token=1d4a44f1-3fbc-416c-a577-fac1a48cdc92" alt=""><figcaption><p>Paste the routes from the generator into router.ex</p></figcaption></figure>

Petal Pro defaults to scoping authenticated routes to within in the “/app” namespace. To continue this naming convention we need to edit the routes within the generated code. To do this you can do a "find and replace all" within the `reminder_live` folder.

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FLkR3DeUlhSWUmeOA7Kur%2FCleanShot%202023-01-18%20at%2014.13.58%402x.png?alt=media\&token=75cf456f-42d1-4f0f-80a0-67034702411c)

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FuBaFRIsb5gjKTtaoMksY%2FCleanShot%202023-01-18%20at%2014.14.26%402x.png?alt=media\&token=bb948d42-101e-4344-94fd-5ee7c8b5bf78)

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FcYdk81RLsdCaFLWZsxJL%2FCleanShot%202023-01-18%20at%2014.05.08%402x.png?alt=media\&token=f0894e11-526a-4f0e-aa40-725b07aac669)

Since it's in a protected area, we'll have to sign in before being able to reach this new page. Click the sign-in button in the top right dropdown:

![The sign in button](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FABcJvU7OyJIfT5krOrmg%2FCleanShot%202022-05-12%20at%2015.24.54.png?alt=media\&token=47aae35d-a377-47d6-b57a-672e579cd838)

Sign in as admin:

* Email: `admin@example.com`
* Password: `password`

Upon your first sign in you will be greeted with an onboarding screen.

![Every new user is shown the onboarding screen.](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FcveDyC9SnBlczw90MiLj%2FCleanShot%202022-05-12%20at%2015.23.33.png?alt=media\&token=1672e964-448e-4850-bcfc-2566efcd6f0d)

Showing this screen is determined by the user field `user.is_onboarded.`

All new users have this defaulted to false, and thus will see this screen thanks to the `OnboardingPlug` that redirects to this page if `current_user.is_onboarded == false`.&#x20;

Over time you might add more onboarding fields like "Where did you hear about us?" or "How many people are in your company?".&#x20;

Onboarding can be switched off by removing the `OnboardingPlug` from all pipelines in the router. There is further explanation for this in `onboarding_plug.ex`

Just hit submit to get through the onboarding and you will then be shown a dashboard that we will ignore.

Instead, manually navigate to "<http://localhost:4000/app/reminders>" to see the newly created CRUD functionality.&#x20;

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FFr4phi9VjFE0Adk7J9uf%2FCleanShot%202022-10-09%20at%2017.07.09%402x.png?alt=media&#x26;token=04db27c1-cd40-4e66-a92c-f61552bfe334" alt=""><figcaption><p>Our CRUD screen for the reminders resource.</p></figcaption></figure>

### Assigning reminders to the current user

One issue with the generator is that it doesn't handle foreign keys very well. Currently, the reminders page shows reminders from ALL users. In fact, the `reminder.user_id` is not even being set when creating a new reminder, so they're not really owned by anyone - every user who signs in will see them. We need to make it so everything is scoped to the currently signed-in user:

* the index page table should only show the current users' reminders
* when you create a new reminder, it should set the user\_id to the current user's id
* the show page should not be allowed to see other user's reminders

Firstly, there are a couple of problems with the schema file:

![Generators aren't perfect](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FT7sQkYKetyRDMg5oswgx%2FCleanShot%202022-03-07%20at%2008.12.35%402x.png?alt=media\&token=09619793-2376-4cf2-bfe3-fe057042e5be)

Let's fix those up:

{% code title="reminder.ex" %}

```elixir
defmodule Remindly.Reminders.Reminder do
  use Ecto.Schema
  import Ecto.Changeset

  schema "reminders" do
    field :due_date, :date
    field :is_done, :boolean, default: false
    field :label, :string

    belongs_to :user, Remindly.Accounts.User

    timestamps()
  end

  @doc false
  def changeset(reminder, attrs) do
    reminder
    |> cast(attrs, [:label, :due_date, :is_done, :user_id])
    |> validate_required([:label, :due_date, :is_done, :user_id])
  end
end
```

{% endcode %}

{% hint style="info" %}
**Note** that we've added `:user_id` to the `validate_required` function too, as we always want a reminder to belong to someone.&#x20;
{% endhint %}

Now go to index.ex in your `reminder_live` folder and make sure you alias Remindly.Repo

Next, you'll want to modify the list\_reminders function as below.

{% code title="index.ex" %}

```elixir
defmodule RemindlyWeb.ReminderLive.Index do
  ...

  defp list_reminders(user) do
    Repo.preload(user, :reminders).reminders
  end
  
  ...
end
```

{% endcode %}

Finally, you'll want to modify the mount and handle\_event to include the following.

{% code title="index.ex" %}

```elixir
defmodule RemindlyWeb.ReminderLive.Index do
  ...

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :reminders, list_reminders(socket.assigns.current_user))}
  end
  
  ...

 @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    reminder = Reminders.get_reminder!(id)
    {:ok, _} = Reminders.delete_reminder(reminder)

    {:noreply, assign(socket, :reminders, list_reminders(socket.assigns.current_user))}
  end
  
  ...
end
```

{% endcode %}

The reminder schema now knows it belongs to a user, but we still have to let the user schema know that it "has\_many" reminders. Let's add that to the user schema file:

{% code title="user.ex" %}

```elixir
defmodule Remindly.Accounts.User do
  ...

  schema "users" do
    # ...other fields omitted for brevity

    many_to_many :orgs, Org, join_through: "orgs_memberships", unique: true    
    # Add the line below:
    has_many :reminders, Remindly.Reminders.Reminder

    timestamps()
  end
  
  ...
end
```

{% endcode %}

Great, the association is now complete. Now let's make it so that when you create a new reminder, the user\_id gets populated with the signed-in user's id.

The code for creating the reminder is located in `lib/remindly_web/live/reminder_live/form_component.ex` :

{% code title="form\_component.ex" %}

```elixir
defmodule RemindlyWeb.ReminderLive.FormComponent do
  ...

  defp save_reminder(socket, :new, reminder_params) do
    case Reminders.create_reminder(reminder_params) do
      {:ok, _reminder} ->
        {:noreply,
         socket
         |> put_flash(:info, "Reminder created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
  
  ...
end
```

{% endcode %}

Let's modify this to ensure the `:user_id` is set in the params:

```elixir
defmodule RemindlyWeb.ReminderLive.FormComponent do
  ...

  defp save_reminder(socket, :new, reminder_params) do
    reminder_params = Map.put(reminder_params, "user_id", socket.assigns.current_user.id)

    case Reminders.create_reminder(reminder_params) do
      {:ok, _reminder} ->
        {:noreply,
         socket
         |> put_flash(:info, "Reminder created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
  
  ...
end
```

Now when you try and create a new reminder, unfortunately you get an error:

![Error upon creating a new reminder](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FTT0dfQNrguVGYOYBhUkd%2Fimage.png?alt=media\&token=ca83644f-dbc2-4cb5-8868-b191c64eda33)

The `current_user` isn't in `socket.assigns`. This can be confusing because it is usually in the socket's assigns thanks to Petal Pro setting it in `user_on_mount_hooks.ex`.&#x20;

The reason it isn't in this case is that `form_component.ex` is a `live_component`, not a `live_view`. Thus it has its own state and it's up to us to pass any `assigns` into it. We can see in `index.html.heex` the assigns being passed into it:

![The assigns of FormComponent are set here](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FAlzBSjdVYcFxtg4PZcXE%2Fimage.png?alt=media\&token=b4dce33d-d049-4db6-be82-abcd43fd4a2b)

So all we need to do is pass in the current\_user to this `live_component` function:

{% code title="index.html.heex" %}

```markup
...

<%= if @live_action in [:new, :edit] do %>
  <.modal title={"#{Atom.to_string(@live_action) |> String.capitalize()} Reminder"}>
    <.live_component
      module={RemindlyWeb.ReminderLive.FormComponent}
      id={@reminder.id || :new}
      action={@live_action}
      reminder={@reminder}
      return_to={Routes.reminder_index_path(@socket, :index)}
      current_user={@current_user}
    />
  </.modal>
<% end %>

...
```

{% endcode %}

And now creating a new reminder works correctly 🎊.

The next problem is that the date picker starts too far in the past - ideally it'll show today's date. So let's add a default date for the form. This also will be done in the `RemindlyWeb.ReminderLive.Index` live view (`index.ex`). We can modify the existing function, so it looks like this:

```elixir
defmodule RemindlyWeb.ReminderLive.Index do
  ...
  
  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Reminder")
    |> assign(:reminder, %Reminder{due_date: Timex.now() |> Timex.to_date()})
  end
  
  ...
end
```

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2F9t0t1Kl0is9jBvcu6UGa%2FCleanShot%202022-10-09%20at%2017.14.29%402x.png?alt=media&#x26;token=b2c87b6f-7eb2-462f-bdb5-625c40683263" alt=""><figcaption><p>The due date is now set to today.</p></figcaption></figure>

We use the Timex library (included in Petal Pro) to get today's date. Now the default is set correctly when creating new reminders (although since we're using UTC time, it may be 1 day off depending on where you live, but good enough for this tutorial).

### Adding a "Done" checkbox

The "Is done" column with true or false is a bit ugly.&#x20;

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FQp8z4RneACzRexuKitJP%2FCleanShot%202022-05-12%20at%2017.32.37.png?alt=media\&token=e7f697ce-ff88-45b8-ae5f-f41b4162e8c2)

Let's move it to the front column and make it a checkbox. Working in the file `index.html.heex`, let's begin by rearranging the headers:

{% code title="index.html.heex" %}

```markup
<thead>
  <.tr>
    <.th>Done?</.th>
    <.th>Label</.th>
    <.th>Due date</.th>
    <.th></.th>
  </.tr>
</thead>
```

{% endcode %}

Next, we'll rearrange the table cells and also add a checkbox in the first cell. Since we want it to submit every time we modify the checkbox, we'll wrap it in a form and add a `phx-change` event listener on it. So every time the form changes an event will be sent to the live view.

{% code title="index.html.heex" %}

```markup
<%= for reminder <- @reminders do %>
  <.tr id={"reminder-#{reminder.id}"}>
    <.td>
      <.form
        :let={f}
        for={Reminders.change_reminder(reminder)}
        phx-change="toggle_reminder"
      >
        <.hidden_input form={f} field={:id} />
        <.checkbox form={f} field={:is_done} />
      </.form>
    </.td>
    <.td><%= reminder.label %></.td>
    <.td><%= reminder.due_date %></.td>
    <.td class="text-right whitespace-nowrap">
      <.button
        color="primary"
        variant="outline"
        size="xs"
        link_type="live_redirect"
        label="Show"
        to={~p"/app/reminders/#{reminder}"}
      />

      <.button
        color="white"
        variant="outline"
        size="xs"
        link_type="live_patch"
        label="Edit"
        to={~p"/app/reminders/#{reminder}/edit"}
      />

      <.button
        color="danger"
        variant="outline"
        link_type="a"
        to="#"
        size="xs"
        label="Delete"
        phx-click="delete"
        phx-value-id={reminder.id}
        data-confirm="Are you sure?"
      />
    </.td>
  </.tr>
<% end %>
```

{% endcode %}

Now we just need to create the event handler for "toggle\_reminder". Add this event handler underneath the event handler for "delete":

{% code title="index.ex" %}

```elixir
@impl true
def handle_event("toggle_reminder", %{"reminder" => reminder_params}, socket) do
  reminder = Reminders.get_reminder!(reminder_params["id"])

  case Reminders.update_reminder(reminder, %{is_done: !reminder.is_done}) do
    {:ok, reminder} ->
      updated_reminders = Util.replace_object_in_list(socket.assigns.reminders, reminder)
      {:noreply, assign(socket, reminders: updated_reminders)}
    {:error, _changeset} ->
      {:noreply, put_flash(socket, :error, "Something went wrong.")}
  end
end
```

{% endcode %}

Now our checkbox is in sync. Live view works so fast that you can barely notice it's not a Single Page App. However, this won't always be the case once if someone is a long way from the server and has some latency.

![Our checkbox is now "live"](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2Fhe1mxMvft9hwcXH6vh9A%2FCleanShot%202022-05-12%20at%2017.35.39.png?alt=media\&token=2421f4c6-d93d-4a1b-a325-ef5bd551e27f)

With LiveView you can simulate latency. If you look in `app.js` you'll find a comment with a command to run the in the browser console. This will add 1 second of latency: `liveSocket.enableLatencySim(1000)`.

It feels the same, but you can see the delay more clearly with the console open. LiveView adds classes when it's performing some kind of communication with the server. Petal Pro comes with some helper classes "show-if-loading" and "hide-if-loading" that we can make use of. Let's hide the checkbox button and show a spinner until LiveView has saved the update in the database:

{% code title="index.html.heex" %}

```markup
...

<.form
  let={f}
  for={Reminders.change_reminder(reminder)}
  phx-change="toggle_reminder"
>
  <.hidden_input form={f} field={:id} />
  <.checkbox form={f} field={:is_done} class="hide-if-loading" />
  <.spinner class="show-if-loading !w-4 !h-4" />
</.form>

...
```

{% endcode %}

![A spinner shows when there is a slow connection](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FTQ40BIvCGHuaqjviiqPn%2FCleanShot%202022-05-12%20at%2017.39.21.png?alt=media\&token=1ce2c31a-56e1-4c2e-b5e6-dfde109fa9a0)

With that working correctly, we can disable the latency in the browser console: `liveSocket.disableLatencySim()`.

### Adding a menu item

In Petal Pro we try to keep our menu items all following the same data structure (`name`, `label`, `path`, and `icon`, which refers to a [Heroicon](https://heroicons.com)).  Menu items can be constructed on the fly, but if it's commonly used we can store them in `menus.ex`.  This file is like a small database for menu items. It also can omit menu items if a user doesn't have the right privileges (eg. admin menu items).&#x20;

Each menu item has a function that looks like this:

```elixir
def get_link(:dashboard, _current_user) do
  %{
    name: :dashboard,
    label: gettext("Dashboard"),
    path: Routes.live_path(Endpoint, RemindlyWeb.DashboardLive),
    icon: :template
  }
end
```

The icon is passed directly to the [<.icon /> component from Petal Components](https://petal.build/components/heroicons#icon).

You can browse [Heroicons](https://heroicons.com/) and when you've found the one you like, just write the atom version of it. For example, the icon "bookmark-alt" would be `:bookmark_alt`.

From these function definitions, we can construct menus. There are two core menus - the main menu, which appears on this side panel, and also the user menu, which is in this dropdown.

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2Fd86hOQbeqrGpDj5WwEqw%2FMenus.png?alt=media\&token=f0f376ad-d4db-405d-b5d4-47fb314b09e9)

Since we want to modify the sidebar, we want to change the main menu for a signed-in user. Let's replace `:dashboard` with `:reminders`:&#x20;

{% code title="menus.ex" %}

```elixir
defmodule RemindlyWeb.Menus do
  ...
  
  # Signed in main menu
  def main_menu_items(current_user),
    do:
      build_menu(
        [
          :reminders
        ],
        current_user
      )
  
  ...
end
```

{% endcode %}

Also, make sure you rename `:dashboard` with `:reminders` in the user menu.

{% code title="menus.ex" %}

```elixir
defmodule RemindlyWeb.Menus do
  ...
  
  # Signed in user menu
  def user_menu_items(current_user),
    do:
      build_menu(
        [
          :reminders,
          :settings,
          :admin,
          :dev,
          :sign_out
        ],
        current_user
      )
  
  ...
end
```

{% endcode %}

However, this won't work yet as we still need to write a definition function for `:reminders`. Scroll down the file until you see the `get_link/2` function for `:dashboard`. Let's  overwrite this function and replace it with our reminders menu item:

{% code title="menus.ex" %}

```elixir
defmodule RemindlyWeb.Menus do
  ...
  
  # Replace the get_link(:dashboard, ...) function with this:
  def get_link(:reminders = name, _current_user) do
    %{
      name: name,
      label: gettext("Reminders"),
      path: ~p"/app",
      icon: :clock
    }
  end
  
  ...
end
```

{% endcode %}

Next, we want this new route to be our kind of "home" route for signed-in users (eg. redirect here after being signed in, or when you click the logo in a layout). To set this, go to `helpers.ex` and look for the `home_path/2` function:

{% code title="helpers.ex" %}

```elixir
  def home_path(nil), do: "/"
  def home_path(_current_user), do: ~p"/app"
```

{% endcode %}

Here we can simply add the path of the new menu item:

{% code title="helpers.ex" %}

```elixir
  def home_path(nil), do: "/"
  def home_path(_current_user), do: ~p"/app/reminders"
```

{% endcode %}

Now refresh the page and we can see the sidebar has updated!

![A new menu item has appeared!](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2F6hjRdCqXtQWZt2ZYTvLW%2Fimage.png?alt=media\&token=eadb0d2b-a8d2-4cfc-8c72-a56437102f52)

However, it's not highlighted 😤. To highlight it, we need to pass the `current_page` attribute to the `<.layout>` component. Open up `index.html.heex` for reminders and you'll see the `<.layout>` call up the top.

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FN61vpuIibCzZ2fEhAfCR%2FCleanShot%202022-05-12%20at%2017.44.53.png?alt=media\&token=c538434a-766d-481f-9ff3-016f6341bb50)

We need to match this atom to the `name` field in the menus `get_link/2` function. Set it to `:reminders` both here in `index.html.heex` and also at the top of `show.html.heex`.

And now our menu item should be highlighted both on the index page and when you click "Show" for an individual reminder.

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FTz59ikZUayt88QmvSGmJ%2FCleanShot%202022-05-16%20at%2018.17.02%402x.png?alt=media\&token=05e6ac74-4d64-4b56-a5fe-cf1f42be2286)

## User activity logging&#x20;

We've found that at some point of building a web application you need to implement some kind of logging functionality. You or your client will want to see some stats on how your users are using your web application.

Petal Pro puts logging at the forefront, and encourages you to log as much as you can. By default, it will store logs in your postgres database, but there's no reason why you can't shoot them off to a 3rd party provider.

To showcase Petal Pro logging, we'll create a log every time a user creates a reminder and toggles it complete. If you look in the file `log.ex`, you will see we have a list of actions that can be logged. Let's add our new log actions:

{% code title="log.ex" %}

```elixir
defmodule Remindly.Logs.Log do
  ...

  @action_options [
    # ... other logs omitted. Add the two below:
    "create_reminder", 
    "complete_reminder"
  ]
  
  ...
```

{% endcode %}

Now we can do the actual logging. Go to `form_component.ex` and add a log in the create action.

{% code title="form\_component.ex" %}

```elixir
  defp save_reminder(socket, :new, reminder_params) do
    reminder_params = Map.put(reminder_params, "user_id", socket.assigns.current_user.id)

    case Reminders.create_reminder(reminder_params) do
      {:ok, reminder} ->
        Remindly.Logs.log("create_reminder", %{
          user: socket.assigns.current_user,
          metadata: %{
            reminder_id: reminder.id,
            reminder_label: reminder.label
          }
        })

        {:noreply,
         socket
         |> put_flash(:info, "Reminder created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
```

{% endcode %}

You will see we're adding some metadata. It is up to you whether or not for new tables like "reminders" you create a migration and add a foreign key to the logs table (`log.reminder_id`). To keep it simple we can make use of a JSONB column on the logs table called metadata (JSONB is better than a JSON column as it allows for faster searching and even indexing). In this column, we can store any kind of map we want. We'll store the ID and label of the reminder here so we can track it if need be.

Now **create a new reminder** and we'll check that our logs have been updated. Logs are in the admin section - try clicking the user menu (your avatar up the top right), and select "Admin".

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FLaWWBonCLASd8T5UGbre%2Fimage.png?alt=media\&token=dcd53b15-8774-48d5-8117-e97625928e62)

Then click logs on the sidebar menu. Here you should see a new log for creating a reminder.

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FacWaa3SWUNfFzgofPiJG%2FCleanShot%202022-10-10%20at%2007.34.18%402x.png?alt=media&#x26;token=a118dbdb-a89c-48df-ae43-867b7d143db9" alt=""><figcaption><p>A log will appear when you create a reminder.</p></figcaption></figure>

Let's do another log for when we complete a reminder. We'll need to modify our `toggle_reminder` function in `index.ex`.

{% code title="index.ex" %}

```elixir
@impl true
def handle_event("toggle_reminder", %{"reminder" => reminder_params}, socket) do
  reminder = Reminders.get_reminder!(reminder_params["id"])

  case Reminders.update_reminder(reminder, %{is_done: !reminder.is_done}) do
    {:ok, reminder} ->
      if reminder.is_done do
        Remindly.Logs.log("complete_reminder", %{
          user: socket.assigns.current_user,
          metadata: %{
            reminder_id: reminder.id,
            reminder_label: reminder.label
          }
        })
      end

      updated_reminders = Util.replace_object_in_list(socket.assigns.reminders, reminder)
      {:noreply, assign(socket, reminders: updated_reminders)}
    {:error, _changeset} ->
      {:noreply, put_flash(socket, :error, "Something went wrong.")}
  end
end
```

{% endcode %}

Now, when we toggle a reminder to "done", a new log will appear.

#### Live logs

You might have noticed a toggle for "Live logs" on the logs screen. If you turn this on, you will actually get a live feed of what's happening in your web app. You can try it out by opening two browser windows side by side: one on logs with "Live logs" checked, and one on the reminders screen. If you toggle the reminder on and off, you will see the logs appearing on the admin screen in real-time:

![Real time logs thanks to PubSub](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FhpRup64MD0Thq1PamWkY%2FCleanShot%202022-03-08%20at%2006.29.49.png?alt=media\&token=44c4233a-7402-47e1-9a26-56d51063294d)

Note that if it isn't working you might have to restart your server.

## Reminder emails

Next, we need to implement an email reminder that gets sent the day a reminder is overdue. If we head to "Dev", and then click on "Email templates" we can see a list of our transactional emails.&#x20;

![How to get to "Dev"](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FJUDYk1lOiELflG7jGWYe%2Fimage.png?alt=media\&token=0f04a254-5716-4d2c-b046-d9ea07c3ee42)

<figure><img src="https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FfUjFoODEvAGg829XFRzq%2FCleanShot%202022-10-10%20at%2008.24.11%402x.png?alt=media&#x26;token=0b4f4ce6-4123-4b24-aad3-790a447585ce" alt=""><figcaption></figcaption></figure>

We want to create a new one and show it here so we can visualise it while we develop it. Before we do, let's do a quick overview of how emails work in Petal Pro. Open up `email.ex` and you will see a list of functions that generate Swoosh email structs. Eg:

```elixir
def confirm_register_email(email, url) do
  base_email()
  |> to(email)
  |> subject("Confirm instructions")
  |> render_body("confirm_register_email.html", %{url: url})
  |> premail()
end
```

If I run this function in IEX I can see the Swoosh struct:

![](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FFYq3UZcdTG2scznnYKuU%2Fimage.png?alt=media\&token=43279a72-924e-44a3-9fc3-c5375968f2b1)

A `Swoosh.Email` struct can be delivered to an email address by a Swoosh mailer (see `mailer.ex`). Eg:

```
  Remindly.Email.confirm_register_email(user.email, url)
  |> Remindly.Mailer.deliver()
```

So `email.ex` creates the Swoosh structs and the functions for actually delivering emails like the previous code example are in `user_notifier.ex`. Think of `email.ex` functions like view templates, and `user_notifer.ex` functions like controller actions.&#x20;

So our steps will be:

1. Create the function to generate a Swoosh struct in `email.ex`
2. Ensure it looks good by creating a new action in `email_testing_controller.ex` that simply renders the html\_body value of the struct
3. Create a new `user_notifier.ex` function for use in our application
4. Create a CRON job that runs once a day that finds overdue reminders and emails the user using the `user_notifier.ex` function

### Designing the email

In `email.ex`, let's create a function for our new email:

{% code title="email.ex" %}

```elixir
defmodule Remindly.Email do
  ...
  
  def reminder(email, reminder, reminder_url) do
    base_email()
    |> to(email)
    |> subject("Reminder!")
    |> render_body("reminder.html", %{reminder: reminder, url: reminder_url})
    |> premail()
  end
  
  ...
end
```

{% endcode %}

Now it expects a template `reminder.html.heex`. Let's add one to the `/lib/remindly_web/templates/email` folder:

![Our new reminder.html.heex template](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FFCB95d1tjAH90CRpOlK1%2Fimage.png?alt=media\&token=7d95ab52-859a-4912-804c-c528b35fa53d)

{% code title="reminder.html.heex" %}

```markup
<h1>Reminder</h1>

<p><%= @reminder.label %></p>

<EmailComponents.button_centered to={@url}>
  View
</EmailComponents.button_centered>
```

{% endcode %}

Emails are notoriously hard to code and work across the myriad of email providers, so we created a template and included some components to help with the basics like a centered button or gap. See `email_components.ex` for these.&#x20;

Now we can try seeing how it looks. Open up `email_testing_controller.ex` and do two things:

1. Append "reminder" to the end of `@email_templates`&#x20;
2. Create a new `generate_email/2` function for our new email

```elixir
defmodule RemindlyWeb.EmailTestingController do
  ...
  
  @email_templates [
    "template",
    "register_confirm_email",
    "reset_password",
    "change_email",
    "org_invitation",
    "passwordless_pin",
    "reminder"
  ]
  
  defp generate_email("reminder", current_user) do
    reminder = %Remindly.Reminders.Reminder{
      user_id: current_user.id,
      label: "Do the washing.",
      due_date: Timex.now() |> Timex.to_date()
    }

    Email.reminder(current_user.email, reminder, "/")
  end
  
  ...
end
```

Now if you refresh the "Email templates" page you can see your new email design:

![Out stunning email design](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FhFFuh8GyiEg9dMprmIhS%2Fimage.png?alt=media\&token=41dada8b-af46-4c47-8700-99b2eec8a7a0)

{% hint style="info" %}
**Note** that if you missed it earlier - since emails can be opened in all kinds of email clients, the logo must be hosted online somewhere.&#x20;
{% endhint %}

I recommend uploading your logo in png format to some kind of image hosting you like. We personally like Cloudinary so let's chuck our logo up there and then paste the URL into our config file.

{% code title="config.exs" %}

```elixir
config :remindly,
  app_name: "Remindly",
  business_name: "Remindly Pty Ltd",
  support_email: "matt@petal.build",
  mailer_default_from_name: "Remindly",
  mailer_default_from_email: "matt@petal.build",
  logo_url_for_emails: "https://res.cloudinary.com/wickedsites/image/upload/v1646277219/petal_marketing/remindly_logo_tsvksl.png",
  seo_description: "Reminder app",
  css_theme_default: "dark"
```

{% endcode %}

Great, now that the email is ready to go we can implement the `user_notifier.ex` function that accepts a `reminder` Ecto struct as a parameter and sends the reminder email for us:

```elixir
defmodule Remindly.Accounts.UserNotifier do
  ...
  
  def deliver_reminder(reminder) do
    reminder = Remindly.Repo.preload(reminder, :user)
    url = RemindlyWeb.Router.Helpers.reminder_show_url(RemindlyWeb.Endpoint, :show, reminder)

    Email.reminder(reminder.user.email, reminder, url)
    |> deliver()
  end
  
  ...
end
```

Now we can test a sent email in IEX:

{% code title="iex -S mix phx.server" %}

```elixir
reminder = Repo.last(Remindly.Reminders.Reminder)
user = Repo.last(User)
Remindly.Accounts.UserNotifier.deliver_reminder(reminder)
```

{% endcode %}

Click on "Sent emails" in the sidebar to see that it worked properly. If we cmd-click the button it should take us to the right reminder.&#x20;

![The "Sent emails" section shows any emails that have been sent off](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FTeIBhw9pYWWWAPg4Ph8x%2Fimage.png?alt=media\&token=18d8194e-83d1-4029-8595-a8bdec95a486)

### Sending the email for overdue reminders

With an email ready to be sent to people who have been lazy, let's create a CRON job to send off the emails. We use [Oban](https://github.com/sorentwo/oban) for jobs, which supports a CRON schedule (CRON just means a time schedule). Here's what we'll do:

1. We create a worker `Remindly.Workers.ReminderWorker`, which has a function `perform/1` that will find overdue reminders and send the reminder emails&#x20;
2. We tell Oban to run this worker once per day&#x20;

#### Reminder worker

Petal Pro comes with an example worker you can check out (`example_worker.ex`). In the same folder, let's create `reminder_worker.ex`:

![Our new worker](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FkCiyRCknpgCMwkB0ZzEl%2Fimage.png?alt=media\&token=895a2dfd-1786-460f-95f4-8c23833811af)

And then enter this code snippet:

{% code title="reminder\_worker.ex" %}

```elixir
defmodule Remindly.Workers.ReminderWorker do
  @moduledoc """
  Run this with:
  Oban.insert(Remindly.Workers.ReminderWorker.new(%{}))
  """
  use Oban.Worker, queue: :default
  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{} = _job) do
    today = Timex.now() |> Timex.to_date()
    Logger.info("ReminderWorker: Sending reminders for #{today}")

    # TODO: Find all overdue reminders and send the user an email

    :ok
  end
end

```

{% endcode %}

Now we have a skeleton worker we can work with. Try testing it out in IEX:

```
Oban.insert(Remindly.Workers.ReminderWorker.new(%{}))
```

{% hint style="info" %}
**Note** you may need to restart your server if you get an error here.
{% endhint %}

![If we run the job in IEX, it seems to be working](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FWVY6y6e8v69dGUcaVJtR%2Fimage.png?alt=media\&token=ba9aeb76-dc17-4f3d-acd8-32556683d0d7)

Now we need to find all the overdue reminders. This is a job for the context file `reminders.ex`. In there we can add the following function:

```elixir
defmodule Remindly.Reminders do
  ...
  
  def list_overdue_reminders do
    today = Timex.now() |> Timex.to_date()

    Repo.all(
      from r in Reminder,
        where: r.due_date < ^today,
        where: r.is_done == false
    )
  end
  
  ...
```

Normally we'd write a test for this, but for brevity, we'll assume it works correctly (or you can test it in IEX if you like).

Back to the worker, let's finish it off:

{% code title="reminder\_worker.ex" %}

```elixir
defmodule Remindly.Workers.ReminderWorker do
  @moduledoc """
  Run this with:
  Oban.insert(Remindly.Workers.ReminderWorker.new(%{}))
  """
  use Oban.Worker, queue: :default
  alias Remindly.{Repo, Reminders}
  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{} = _job) do
    today = Timex.now() |> Timex.to_date()
    Logger.info("ReminderWorker: Sending reminders for #{today}")

    Reminders.list_overdue_reminders()
    |> Repo.preload(:user)
    |> Enum.each(fn reminder ->
      Logger.info("Reminding #{reminder.user.name} to #{reminder.label}")
      Remindly.Accounts.UserNotifier.deliver_reminder(reminder)
    end)

    :ok
  end
end
```

{% endcode %}

We can test it by setting one of our reminders in the past (making sure it's unchecked) and then running the worker in IEX again.

![The worker seems to have successfully worked](https://3399528933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPrds3tZMdNMoQM7suYlt%2Fuploads%2FvRhruciXKnGIPFWKmYzz%2FCleanShot%202022-03-08%20at%2016.20.37.png?alt=media\&token=6b44224c-d601-4132-bace-6d67567af097)

You can double-check by also looking at the "Sent emails" page again.

#### CRON job

Now that we know our worker works, we want to schedule it to run every day. This is a simple task thanks to Oban - just modify `config.exs`:

```elixir
...

config :remindly, Oban,
  repo: Remindly.Repo,
  queues: [default: 5],
  plugins: [
    {Oban.Plugins.Pruner, max_age: (3600 * 24)},
    {Oban.Plugins.Cron,
      crontab: [
        {"@daily", Remindly.Workers.ReminderWorker}
        # {"* * * * *", Remindly.EveryMinuteWorker},
        # {"0 * * * *", Remindly.EveryHourWorker},
        # {"0 */6 * * *", Remindly.EverySixHoursWorker},
        # {"0 0 * * SUN", Remindly.EverySundayWorker},
        # More examples: https://crontab.guru/examples.html
      ]}
  ]
  
...
```

To keep it simple we'll use the `@daily` code for scheduling. If you want to fine tune it to the exact minute, you can use the usual CRON syntax (you might need [a website](http://www.cronmaker.com/) to help you generate it).

And that's it! Our job will now run daily.

## Deployment with Fly.io

We have found Fly.io to be the best combination of cheap and easy. Petal Pro has been set up for users to quickly deploy on Fly.io's servers.

If you haven't already, download the [Fly.io CLI](https://fly.io/docs/getting-started/installing-flyctl). Then you will need to [register or sign in](https://fly.io/docs/getting-started/log-in-to-fly/).

Once signed in, you can create a new project with:

`fly launch`

* We'll call the app remindly.
* Hit Y to setting up a DB as we'll need that.
* I'll go with the cheapest option for a server - not sure my reminders app will catch on.
* When it asks "Do you want to deploy now", hit N - we need to make a change before we deploy.

We need a service to send our emails out. We've found that the simplest and cheapest solution is Amazon SES, and so Petal defaults to using this. We don't really use Amazon for much else, but its email service is cheap and the emails don't get sent to spam as easily as other services we've tried (cough cough Sendgrid).

Setting up Amazon SES is beyond the scope of this tutorial - you can google how to do it. The end result should be you are able to provide the following secrets that we'll provide to our production server:&#x20;

```
fly secrets set AWS_ACCESS_KEY="xxx" AWS_SECRET="xxx" AWS_REGION="xxx"
```

Finally, we can run `fly deploy`.

After deploying you can run `fly open` to see it in your browser. If you've made it this far, congratulations! You've just gone from nothing to production. Obviously, this app needs some touching up, but it gives you an idea of how to do things. We look forward to seeing what people create with Petal Pro.&#x20;

If you have any feedback, head [here](https://petal.build/feedback) to see how to get in touch.
