-# Build Log 👩💻 [](https://travis-ci.org/dwyl/app-mvp)
+# Build Log 👩💻
+[](https://travis-ci.org/dwyl/app-mvp)
@@ -24,9 +25,9 @@ it in **20 minutes**. 🏁
> but they are linked in case you get stuck.
In this log we have written the "CRUD" functions first
-and _then_ built the UI.
+and _then_ built the UI.
We were able to to do this because we had a good idea
-of which functions we were going to need.
+of which functions we were going to need.
If you are reading through this
and scratching your head
wondering where a particular function will be used,
@@ -64,7 +65,7 @@ where (_hopefully_) it will all be clear.
- [8. Implement the `LiveView` UI Template](#8-implement-the-liveview-ui-template)
-## 1. Create a New `Phoenix` App
+# 1. Create a New `Phoenix` App
Open your terminal and
**create** a **new `Phoenix` app**
@@ -88,7 +89,7 @@ will be in the _main_
We're excluding them here
to reduce complexity/dependencies.
-### 1.1 Run the `Phoenix` App
+## 1.1 Run the `Phoenix` App
Run the `Phoenix` app with the command:
@@ -118,7 +119,7 @@ You should see something similar to the following

-### 1.2 Run the tests:
+## 1.2 Run the tests:
To run the tests with
@@ -135,7 +136,7 @@ Finished in 0.1 seconds (0.07s async, 0.07s sync)
That tells us everything is working as expected. 🚀
-#### Test Coverage?
+### Test Coverage?
If you prefer to see **test coverage** - we certainly do -
then you will need to add a few lines to the
@@ -169,7 +170,7 @@ You should see output similar to the following:
-### 1.3 Setup `Tailwind`
+## 1.3 Setup `Tailwind`
As we're using **`Tailwind CSS`**
for the **UI** in this project
@@ -193,7 +194,7 @@ please retrace the steps
and open an issue:
[learn-tailwind/issues](https://github.com/dwyl/learn-tailwind/issues)
-### 1.4 Setup `LiveView`
+## 1.4 Setup `LiveView`
Create the `lib/app_web/live` directory
and the controller at `lib/app_web/live/app_live.ex`:
@@ -242,7 +243,7 @@ update the contents of the `` to:
```
-### 1.5 Update `router.ex`
+## 1.5 Update `router.ex`
Now that you've created the necessary files,
open the router
@@ -269,7 +270,7 @@ you should see the following:

-### 1.6 Update Tests
+## 1.6 Update Tests
At this point we have made a few changes
that mean our automated test suite will no longer pass ...
@@ -334,7 +335,7 @@ Finished in 0.1 seconds (0.08s async, 0.1s sync)
Randomized with seed 796477
```
-### 1.7 Delete Page-related Files
+## 1.7 Delete Page-related Files
Since we won't be using the `page` meatphore in our App,
we can delete the default files created by `Phoenix`:
@@ -387,7 +388,7 @@ to watch this talk.
-->
-## 2. Create Schemas to Store Data
+# 2. Create Schemas to Store Data
Create database schemas
to store the data
@@ -412,11 +413,11 @@ We created **2 database tables**;
`items` and `timers`.
Let's run through them.
-### _Explanation_ of the Schemas
+## _Explanation_ of the Schemas
This is a quick breakdown of the schemas created above:
-#### `item`
+### `item`
An `item` is the most basic unit of content.
An **`item`** is just a **`String`** of **`text`**.
@@ -448,7 +449,7 @@ We aren't expecting more than
***2 billion*** people to use the MVP. 😜
-->
-#### `timer`
+### `timer`
A `timer` is associated with an `item`
to track how long it takes to ***complete***.
@@ -485,7 +486,7 @@ so we always know when the record was created/updated.
-### 2.1 Run Tests!
+## 2.1 Run Tests!
Once we've created the required schemas,
several new files are created.
@@ -530,7 +531,7 @@ See: https://en.wikipedia.org/wiki/Scaffold_(programming)
-## 3. Input `items`
+# 3. Input `items`
We're going to
@@ -599,7 +600,7 @@ This is expected as the code is not there yet!
-### 3.1 Make the `item` Tests Pass
+## 3.1 Make the `item` Tests Pass
Open the
`lib/app/item.ex`
@@ -711,7 +712,7 @@ Once you have saved the file, re-run the tests.
They should now pass.
-## 4. Create `Timer`
+# 4. Create `Timer`
Open the `test/app/timer_test.exs` file and add the following tests:
@@ -783,7 +784,7 @@ defmodule App.TimerTest do
end
```
-### Make `timer` tests pass
+## Make `timer` tests pass
Open the `lib/app/timer.ex` file
and replace the contents with the following code:
@@ -902,7 +903,7 @@ We have written the function using "raw" `SQL`
so that it's easier for people who are `new`
to `Phoenix`, and _specifically_ `Ecto` to understand.
-## 5. `items` with `timers`
+# 5. `items` with `timers`
The _interesting_ thing we are UX-testing in the MVP
is the _combination_ of (todo list) `items` and `timers`.
@@ -918,7 +919,7 @@ If you know of one,
please share!
-### 5.1 Test for `accummulate_item_timers/1`
+## 5.1 Test for `accummulate_item_timers/1`
This might feel like we are working in reverse,
that's because we _are_!
@@ -1011,7 +1012,7 @@ This is a large test but most of it is the test data (`items_with_timers`) in th
With that test in place, we can write the function.
-### 5.2 Implement the `accummulate_item_timers/1` function
+## 5.2 Implement the `accummulate_item_timers/1` function
Open the
`lib/app/item.ex`
@@ -1085,7 +1086,7 @@ We're also _very_ happy for anyone `else` to refactor it!
[Please open an issue](https://github.com/dwyl/app-mvp/issues/)
so we can discuss. 🙏
-### 5.3 Test for `items_with_timers/1`
+## 5.3 Test for `items_with_timers/1`
Open the
`test/app/item_test.exs`
@@ -1112,7 +1113,7 @@ file and the following test to the bottom:
end
```
-### 5.4 Implement `items_with_timers/1`
+## 5.4 Implement `items_with_timers/1`
Open the
`lib/app/item.ex`
@@ -1213,7 +1214,7 @@ and added inline comments to clarify the code.
But again, if anything is unclear please let us know!!
-## 6. Add Authentication
+# 6. Add Authentication
This section borrows heavily from:
[dwyl/phoenix-liveview-chat-example](https://github.com/dwyl/phoenix-liveview-chat-example#12-authentication)
@@ -1260,11 +1261,11 @@ defmodule AppWeb.AuthController do
end
```
-## 7. Create `LiveView` Functions
+# 7. Create `LiveView` Functions
_Finally_ we have all the "backend" functions we're going to need.
-### 7.1 Write `LiveView` Tests
+## 7.1 Write `LiveView` Tests
Opent the
`test/app_web/live/app_live_test.exs`
@@ -1470,13 +1471,568 @@ Feel free to comment out all but one at a time
to implement the functions gradually.
-### 7.2 Implement the `LiveView` functions
+## 7.2 Implement the `LiveView` functions
Open the
`lib/app_web/live/app_live.ex`
-file and
+file and replace the contents with the following code:
+
+```elixir
+defmodule AppWeb.AppLive do
+ use AppWeb, :live_view
+ alias App.{Item, Timer}
+ # run authentication on mount
+ on_mount AppWeb.AuthController
+
+ @topic "live"
+
+ defp get_person_id(assigns) do
+ if Map.has_key?(assigns, :person) do
+ assigns.person.id
+ else
+ 0
+ end
+ end
+
+ # assign default values to socket:
+ defp assign_socket(socket) do
+ person_id = get_person_id(socket.assigns)
+ assign(socket, items: Item.items_with_timers(person_id), active: %Item{}, editing: nil)
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ # subscribe to the channel
+ if connected?(socket), do: AppWeb.Endpoint.subscribe(@topic)
+ {:ok, assign_socket(socket)}
+ end
+
+ @impl true
+ def handle_event("create", %{"text" => text}, socket) do
+ person_id = get_person_id(socket.assigns)
+ Item.create_item(%{text: text, person_id: person_id, status: 2})
+ # IO.inspect(socket.assigns, label: "handle_event create socket.assigns")
+ socket = assign_socket(socket)
+
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("toggle", data, socket) do
+ # Toggle the status of the item between 3 (:active) and 4 (:done)
+ status = if Map.has_key?(data, "value"), do: 4, else: 3
+
+ # need to restrict getting items to the people who own or have rights to access them!
+ item = Item.get_item!(Map.get(data, "id"))
+ Item.update_item(item, %{status: status})
+ Timer.stop_timer_for_item_id(item.id)
+ socket = assign_socket(socket)
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => item_id}, socket) do
+ Item.delete_item(item_id)
+ socket = assign_socket(socket)
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "delete", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("start", data, socket) do
+ item = Item.get_item!(Map.get(data, "id"))
+ person_id = get_person_id(socket.assigns)
+ {:ok, _timer} =
+ Timer.start(%{
+ item_id: item.id,
+ person_id: person_id,
+ start: NaiveDateTime.utc_now()
+ })
+
+ socket = assign_socket(socket)
+
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "start|stop", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("stop", data, socket) do
+ timer_id = Map.get(data, "timerid")
+ {:ok, _timer} = Timer.stop(%{id: timer_id})
+ socket = assign_socket(socket)
+
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "start|stop", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("edit-item", data, socket) do
+ {:noreply, assign(socket, editing: String.to_integer(data["id"]))}
+ end
+
+ @impl true
+ def handle_event("update-item", %{"id" => item_id, "text" => text}, socket) do
+ current_item = Item.get_item!(item_id)
+ Item.update_item(current_item, %{text: text})
+ socket = assign_socket(socket)
+ AppWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_info(
+ %{event: "start|stop", payload: %{items: _items}},
+ socket
+ ) do
+ {:noreply, assign_socket(socket)}
+ end
+
+ @impl true
+ def handle_info(%{event: "update", payload: %{items: _items}}, socket) do
+ {:noreply, assign_socket(socket)}
+ end
+
+ @impl true
+ def handle_info(%{event: "delete", payload: %{items: _items}}, socket) do
+ {:noreply, assign_socket(socket)}
+ end
+
+ # Check for status 4 (:done)
+ def done?(item), do: item.status == 4
+
+ # Check if an item has an active timer
+ def started?(item) do
+ not is_nil(item.start) and is_nil(item.stop)
+ end
+
+ # An item without an end should be counting
+ def timer_stopped?(item) do
+ not is_nil(item.stop)
+ end
+
+ def timers_any?(item) do
+ not is_nil(item.timer_id)
+ end
+
+ # Convert Elixir NaiveDateTime to JS (Unix) Timestamp
+ def timestamp(naive_datetime) do
+ DateTime.from_naive!(naive_datetime, "Etc/UTC")
+ |> DateTime.to_unix(:millisecond)
+ end
+
+
+ # Elixir implementation of `timer_text/2`
+ def leftPad(val) do
+ if val < 10, do: "0#{to_string(val)}", else: val
+ end
+
+ def timer_text(item) do
+ if is_nil(item) or is_nil(item.start) or is_nil(item.stop) do
+ ""
+ else
+ diff = timestamp(item.stop) - timestamp(item.start)
+
+ # seconds
+ s = if diff > 1000 do
+ s = diff / 1000 |> trunc()
+ s = if s > 60, do: Integer.mod(s, 60), else: s
+ leftPad(s)
+ else
+ "00"
+ end
+
+ # minutes
+ m = if diff > 60000 do
+ m = diff / 60000 |> trunc()
+ m = if m > 60, do: Integer.mod(m, 60), else: m
+ leftPad(m)
+ else
+ "00"
+ end
+
+ # hours
+ h = if diff > 3600000 do
+ h = diff / 3600000 |> trunc()
+ leftPad(h)
+ else
+ "00"
+ end
+
+ "#{h}:#{m}:#{s}"
+ end
+ end
+end
+```
+
+Again, a bunch of code here.
+Please work through each function
+to understand what is going on.
+
+
+# 8. Implement the `LiveView` UI Template
+
+_Finally_ we have all the `LiveView` functions,
+
+## 8.1 Update the `root` layout/template
+
+Open the
+`lib/app_web/templates/layout/root.html.heex`
+file and replace the contents with the following:
+
+```html
+
+
+
+
+
+
+ <%= live_title_tag assigns[:page_title] || "dwyl mvp"%>
+ <%= render "icons.html" %>
+
+
+
+
+
+
+
+ <%= @inner_content %>
+
+
+```
+
+## 8.2 Create the `icons` template
+
+To make the App more Mobile-friendly,
+we define a bunch of iOS/Android related icons.
+
+Create a new file with the path
+`lib/app_web/templates/layout/icons.html.heex`
+and add the following code to it:
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+This is static and very repetitive,
+hence creating a partial to hide it from the root layout.
+
+Finally ...
+
+
+# 9. Update the `LiveView` Tempalte
+
+Open the `app_live.html.heex`
+file and replace the contents
+with the following template code:
+
+```html
+
+
+
+
+
+ <%= for item <- @items do %>
+
+
+
+ <%= if done?(item) do %>
+
+
+
+
+
+
+
+
+
+ <%= timer_text(item) %>
+
+
+
+
+
+
+ <% else %>
+
+
+
+
+ <%= if item.id == @editing do %>
+
+ <% else %>
+
+
+ <% end %>
+
+ <%= if timers_any?(item) do %>
+
+ <%= if timer_stopped?(item) do %>
+
+
+
+
+
+
+ <%= timer_text(item) %>
+
+
+
+
+ <% else %>
+ <%= if started?(item) do %>
+
+
+
+ <%= timestamp(item.start) %>
+
+
+
+
+
00:00:00
+
+
+ <% end %>
+ <% end %>
+ <% else %>
+
+
+ <% end %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+```
+
+The bulk of the App is containted in this one template file.
+work your way through it and if anything is unclear,
+let us know!
-## 8. Implement the `LiveView` UI Template
+Thanks for reading this far.
+If you found it interesting,
+please let us know by starring the repo on GitHub! ⭐