From 5c6eb3c63ae29a8689688b3c46ee50e5cb324fc8 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Sun, 7 Aug 2022 21:31:34 +0100 Subject: [PATCH] add remaining code/sections to BUILDIT.md #89 --- BUILDIT.md | 620 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 588 insertions(+), 32 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index aafdb579..db0bb7df 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -1,6 +1,7 @@
-# Build Log 👩‍💻 [![Build Status](https://img.shields.io/travis/com/dwyl/app-mvp/master?color=bright-green&style=flat-square)](https://travis-ci.org/dwyl/app-mvp) +# Build Log 👩‍💻 +[![Build Status](https://img.shields.io/travis/com/dwyl/app-mvp/master?color=bright-green&style=flat-square)](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 ![phoenix-default-homepage](https://user-images.githubusercontent.com/194400/174807257-34120dc5-723e-4b2c-9e8e-4b6f3aefca14.png) -### 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: Phoenix tests passing coverage 100% -### 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: ![liveveiw-page-with-tailwind-style](https://user-images.githubusercontent.com/194400/176137805-34467c88-add2-487f-9593-931d0314df62.png) -### 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 %> + +
    + + + +
    + + +

    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! ⭐