diff --git a/.formatter.exs b/.formatter.exs
index 47616780..e945e12b 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,4 +1,5 @@
[
import_deps: [:phoenix],
- inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"]
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3f6bfe55..1924c967 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14.2' # Define the elixir version [required]
- otp-version: '24.3.4' # Define the OTP version [required]
+ otp-version: '25.1.2' # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
diff --git a/README.md b/README.md
index 15a41f96..8548e6e4 100644
--- a/README.md
+++ b/README.md
@@ -128,11 +128,13 @@ elixir -v
You should expect to see output similar to the following:
```elixir
-Elixir 1.13.4 (compiled with Erlang/OTP 24)
+Elixir 1.14.2 (compiled with Erlang/OTP 25)
```
-This informs us we are using `Elixir version 1.13.4`
+This informs us we are using `Elixir version 1.14.2`
which is the _latest_ version at the time of writing.
+Some of the more advanced features of Phoenix 1.7 during compilation time require elixir
+1.14 although the code will work in previous versions.
@@ -148,7 +150,7 @@ mix phx.new -v
You should see something similar to the following:
```sh
-Phoenix installer v1.6.9
+Phoenix installer v1.7.0-rc.0
```
If you have an earlier version,
@@ -287,15 +289,16 @@ And then run the following `mix` command:
mix test
```
-You should see:
+The first time it will compile Phoenix and will take some time.
+You should see something similar to this:
```
-Compiling 14 files (.ex)
+Compiling 17 files (.ex)
Generated live_view_counter app
-...
-Finished in 0.03 seconds (0.02s async, 0.01s sync)
-3 tests, 0 failures
+.....
+Finished in 0.1 seconds (0.05s async, 0.1s sync)
+5 tests, 0 failures
```
Tests all pass.
@@ -329,7 +332,7 @@ And add the following code to it:
```elixir
defmodule LiveViewCounterWeb.Counter do
- use Phoenix.LiveView
+ use LiveViewCounterWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, :val, 0)}
@@ -344,12 +347,15 @@ defmodule LiveViewCounterWeb.Counter do
end
def render(assigns) do
- ~L"""
+ ~H"""
+
"""
end
end
@@ -406,7 +412,8 @@ end
returns a tuple of:
`{:noreply, update(socket, :val, &(&1 + 1))}`
where the `:noreply` just means
-"do not send any further messages to the caller of this function".
+"do not send any further messages to the caller of this function".
+
`update(socket, :val, &(&1 + 1))` as it's name suggests,
will _update_ the value of `:val` on the `socket`
to the
@@ -441,9 +448,9 @@ receives the `assigns` argument which contains the `:val` state
and renders the template using the `@val` template variable.
The `render/1` function renders the template included in the function.
-The `~L"""` syntax just means
+The `~H"""` syntax just means
"_treat this multiline string as a LiveView template_"
-The `~L` [sigil](https://elixir-lang.org/getting-started/sigils.html)
+The `~H` [sigil](https://elixir-lang.org/getting-started/sigils.html)
is a macro included when the `use Phoenix.LiveView` is invoked
at the top of the file.
@@ -501,35 +508,36 @@ in the previous step, the test in
will now _fail_:
```sh
-Compiling 1 file (.ex)
-..
+Compiling 6 files (.ex)
+Generated live_view_counter app
+....
-1) test GET / (LiveViewCounterWeb.PageControllerTest)
- test/live_view_counter_web/controllers/page_controller_test.exs:4
- Assertion with =~ failed
- code: assert html_response(conn, 200) =~ "Welcome to Phoenix!"
- left: "LiveViewCounter 路 Phoenix Framework
The count is: 0
"
- right: "Welcome to Phoenix!"
- stacktrace:
- test/live_view_counter_web/controllers/page_controller_test.exs:6: (test)
+ 1) test GET / (LiveViewCounterWeb.PageControllerTest)
+ test/live_view_counter_web/controllers/page_controller_test.exs:4
+ Assertion with =~ failed
+ code: assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ left: "\n\n \n \n \n \n \nLiveViewCounter\n 路 Phoenix Framework\n \n \n \n \n
\n \n"
+ right: "Peace of mind from prototype to production"
+ stacktrace:
+ test/live_view_counter_web/controllers/page_controller_test.exs:6: (test)
-Finished in 0.1 seconds
-3 tests, 1 failure
+Finished in 0.1 seconds (0.06s async, 0.09s sync)
+5 tests, 1 failure
```
This just tells us that the test is looking for the string
-`"Welcome to Phoenix!"` in the page and did not find it.
+`"Peace of mind from prototype to production"` in the page and did not find it.
To fix the broken test, open the
`test/live_view_counter_web/controllers/page_controller_test.exs`
file and locate the line:
```elixir
-assert html_response(conn, 200) =~ "Welcome to Phoenix!"
+assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
```
-Update the string from `"Welcome to Phoenix!"`
+Update the string from `"Peace of mind from prototype to production"`
to something we _know_ is present on the page,
e.g:
`"The count is"`
@@ -546,11 +554,11 @@ mix test
You should see output similar to:
```
-Generated live_view_counter app
-...
+.....
+Finished in 0.1 seconds (0.03s async, 0.07s sync)
+5 tests, 0 failures
-Finished in 0.05 seconds
-3 tests, 0 failures
+Randomized with seed 244388
```
@@ -583,7 +591,7 @@ We created a _single_ new file
[`lib/live_view_counter_web/live/counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/7e75ba0cfd7f170dc022cfdf62af380d70cc1496/lib/live_view_counter_web/live/counter.ex)
that contains all the code required to
initialise, render and update the counter.
-Then we set the `live("/", Counter)` route
+Then we set the `live "/", Counter` route
to invoke the `Counter` module in `router.ex`.
In total our counter App is **25 lines** of code.
@@ -1189,6 +1197,84 @@ many are running.
+## Some more tests
+
+Once you have implemented the solution - before if you are using TDD - you need to make sure that the new code is properly tested.
+
+We had a small test that showed that we were showing the counter in the web page, but let's test some of the new logic we added to the "test/live_view_counter_web/live/counter_test.exs" file
+
+```elixir
+defmodule LiveViewCounterWeb.CounterTest do
+ use LiveViewCounterWeb.ConnCase
+ import Phoenix.LiveViewTest
+
+ test "connected mount", %{conn: conn} do
+ {:ok, _view, html} = live(conn, "/")
+ current = LiveViewCounter.Count.current()
+ assert html =~ "count is: #{current}"
+ end
+end
+```
+
+We load the cases and the LiveViewTest and start testing that the connnection shows the current number of users when connecting.
+
+Let's add logic to test increments and decrements
+
+```elixir
+ test "Increment", %{conn: conn} do
+ {:ok, view, html} = live(conn, "/")
+ current = LiveViewCounter.Count.current()
+ assert html =~ "count is: #{current}"
+ assert render_click(view, :inc) =~ "count is: #{current + 1}"
+ end
+
+ test "Decrement", %{conn: conn} do
+ {:ok, view, html} = live(conn, "/")
+ current = LiveViewCounter.Count.current()
+ assert html =~ "count is: #{current}"
+ assert render_click(view, :dec) =~ "count is: #{current - 1}"
+ end
+
+```
+
+Some more tests for the logic when a new user is connected
+```elixir
+ test "handle_info/2 Count Update", %{conn: conn} do
+ {:ok, view, disconnected_html} = live(conn, "/")
+ current = LiveViewCounter.Count.current()
+ assert disconnected_html =~ "count is: #{current}"
+ assert render(view) =~ "count is: #{current}"
+ send(view.pid, {:count, 2})
+ assert render(view) =~ "count is: 2"
+ end
+```
+
+And lastly the logic that follows presence
+
+```elixir
+
+ test "handle_info/2 Presence Update - Joiner", %{conn: conn} do
+ {:ok, view, html} = live(conn, "/")
+ assert html =~ "Current users: 1"
+ send(view.pid, %{
+ event: "presence_diff",
+ payload: %{joins: %{"phx-Fhb_dqdqsOCzKQAl" => %{metas: [%{phx_ref: "Fhb_dqdrwlCmfABl"}]}},
+ leaves: %{}}})
+ assert render(view) =~ "Current users: 2"
+ end
+
+ test "handle_info/2 Presence Update - Leaver", %{conn: conn} do
+ {:ok, view, html} = live(conn, "/")
+ assert html =~ "Current users: 1"
+ send(view.pid, %{
+ event: "presence_diff",
+ payload: %{joins: %{},
+ leaves: %{"phx-Fhb_dqdqsOCzKQAl" => %{metas: [%{phx_ref: "Fhb_dqdrwlCmfABl"}]}}}})
+ assert render(view) =~ "Current users: 0"
+ end
+```
+
+
## Credits & Thanks! 馃檶
Credit for inspiring this tutorial goes to Dennis Beatty
diff --git a/assets/css/app.css b/assets/css/app.css
index 2023bcd5..378c8f90 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -1,99 +1,5 @@
-/* This file is for your main application css. */
-@import "./phoenix.css";
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
-#livecount_container {
- display: table;
- margin: auto;
-}
-
-button {
- margin: 2px
-}
-
-/* LiveView specific classes for your customizations */
-.invalid-feedback {
- color: #a94442;
- display: block;
- margin: -1rem 0 2rem;
-}
-
-.phx-no-feedback.invalid-feedback, .phx-no-feedback .invalid-feedback {
- display: none;
-}
-
-.phx-click-loading {
- opacity: 0.5;
- transition: opacity 1s ease-out;
-}
-
-.phx-disconnected{
- cursor: wait;
-}
-.phx-disconnected *{
- pointer-events: none;
-}
-
-.phx-modal {
- opacity: 1!important;
- position: fixed;
- z-index: 1;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgb(0,0,0);
- background-color: rgba(0,0,0,0.4);
-}
-
-.phx-modal-content {
- background-color: #fefefe;
- margin: 15% auto;
- padding: 20px;
- border: 1px solid #888;
- width: 80%;
-}
-
-.phx-modal-close {
- color: #aaa;
- float: right;
- font-size: 28px;
- font-weight: bold;
-}
-
-.phx-modal-close:hover,
-.phx-modal-close:focus {
- color: black;
- text-decoration: none;
- cursor: pointer;
-}
-
-
-/* Alerts and form errors */
-.alert {
- padding: 15px;
- margin-bottom: 20px;
- border: 1px solid transparent;
- border-radius: 4px;
-}
-.alert-info {
- color: #31708f;
- background-color: #d9edf7;
- border-color: #bce8f1;
-}
-.alert-warning {
- color: #8a6d3b;
- background-color: #fcf8e3;
- border-color: #faebcc;
-}
-.alert-danger {
- color: #a94442;
- background-color: #f2dede;
- border-color: #ebccd1;
-}
-.alert p {
- margin-bottom: 0;
-}
-.alert:empty {
- display: none;
-}
+/* This file is for your main application CSS */
diff --git a/assets/css/phoenix.css b/assets/css/phoenix.css
deleted file mode 100644
index 3767b31d..00000000
--- a/assets/css/phoenix.css
+++ /dev/null
@@ -1,101 +0,0 @@
-/* Includes some default style for the starter application.
- * This can be safely deleted to start fresh.
- */
-
-/* Milligram v1.3.0 https://milligram.github.io
- * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license
- */
-
-*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}
-
-/* General style */
-h1{font-size: 3.6rem; line-height: 1.25}
-h2{font-size: 2.8rem; line-height: 1.3}
-h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35}
-h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5}
-h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4}
-h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2}
-pre{padding: 1em;}
-
-.container{
- margin: 0 auto;
- max-width: 80.0rem;
- padding: 0 2.0rem;
- position: relative;
- width: 100%
-}
-select {
- width: auto;
-}
-
-/* Phoenix promo and logo */
-.phx-hero {
- text-align: center;
- border-bottom: 1px solid #e3e3e3;
- background: #eee;
- border-radius: 6px;
- padding: 3em 3em 1em;
- margin-bottom: 3rem;
- font-weight: 200;
- font-size: 120%;
-}
-.phx-hero input {
- background: #ffffff;
-}
-.phx-logo {
- min-width: 300px;
- margin: 1rem;
- display: block;
-}
-.phx-logo img {
- width: auto;
- display: block;
-}
-
-/* Headers */
-header {
- width: 100%;
- background: #fdfdfd;
- border-bottom: 1px solid #eaeaea;
- margin-bottom: 2rem;
-}
-header section {
- align-items: center;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-}
-header section :first-child {
- order: 2;
-}
-header section :last-child {
- order: 1;
-}
-header nav ul,
-header nav li {
- margin: 0;
- padding: 0;
- display: block;
- text-align: right;
- white-space: nowrap;
-}
-header nav ul {
- margin: 1rem;
- margin-top: 0;
-}
-header nav a {
- display: block;
-}
-
-@media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */
- header section {
- flex-direction: row;
- }
- header nav ul {
- margin: 1rem;
- }
- .phx-logo {
- flex-basis: 527px;
- margin: 2rem 1rem;
- }
-}
diff --git a/assets/js/app.js b/assets/js/app.js
index 7a347daf..44a81220 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1,29 +1,41 @@
-// We need to import the CSS so that webpack will load it.
-// The MiniCssExtractPlugin is used to separate it out into
-// its own CSS file.
-import "../css/phoenix.css"
-import "../css/app.css"
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
-// webpack automatically bundles all modules in your
-// entry points. Those entry points can be configured
-// in "webpack.config.js".
+// You can include dependencies in two ways.
//
-// Import deps with the dep name or local files with a relative path, for example:
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
//
-// import {Socket} from "phoenix"
-// import socket from "./socket"
+// import "../vendor/some-package.js"
//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", info => topbar.delayedShow(200))
+window.addEventListener("phx:page-loading-stop", info => topbar.hide())
+
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
-// >> liveSocket.enableLatencySim(1000)
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
+
diff --git a/assets/static/favicon.ico b/assets/static/favicon.ico
deleted file mode 100644
index 73de524a..00000000
Binary files a/assets/static/favicon.ico and /dev/null differ
diff --git a/assets/static/images/phoenix.png b/assets/static/images/phoenix.png
deleted file mode 100644
index 9c81075f..00000000
Binary files a/assets/static/images/phoenix.png and /dev/null differ
diff --git a/assets/static/robots.txt b/assets/static/robots.txt
deleted file mode 100644
index 26e06b5f..00000000
--- a/assets/static/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
-# To ban all spiders from the entire site uncomment the next two lines:
-# User-agent: *
-# Disallow: /
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
new file mode 100644
index 00000000..b6117019
--- /dev/null
+++ b/assets/tailwind.config.js
@@ -0,0 +1,26 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+const plugin = require("tailwindcss/plugin")
+
+module.exports = {
+ content: [
+ "./js/**/*.js",
+ "../lib/*_web.ex",
+ "../lib/*_web/**/*.*ex"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#FD4F00",
+ }
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
+ ]
+}
\ No newline at end of file
diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js
index 1f622097..4176ede1 100644
--- a/assets/vendor/topbar.js
+++ b/assets/vendor/topbar.js
@@ -1,7 +1,9 @@
/**
* @license MIT
* topbar 1.0.0, 2021-01-06
- * https://buunguyen.github.io/topbar
+ * Modifications:
+ * - add delayedShow(time) (2022-09-21)
+ * http://buunguyen.github.io/topbar
* Copyright (c) 2021 Buu Nguyen
*/
(function (window, document) {
@@ -35,10 +37,11 @@
})();
var canvas,
- progressTimerId,
- fadeTimerId,
currentProgress,
showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
addEvent = function (elem, type, handler) {
if (elem.addEventListener) elem.addEventListener(type, handler, false);
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
@@ -95,6 +98,11 @@
for (var key in opts)
if (options.hasOwnProperty(key)) options[key] = opts[key];
},
+ delayedShow: function(time) {
+ if (showing) return;
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), time);
+ },
show: function () {
if (showing) return;
showing = true;
@@ -125,6 +133,8 @@
return currentProgress;
},
hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
if (!showing) return;
showing = false;
if (progressTimerId != null) {
diff --git a/config/config.exs b/config/config.exs
index 2e87b770..cfc0f45e 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,19 +1,52 @@
# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
+# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
-import Mix.Config
+import Config
# Configures the endpoint
config :live_view_counter, LiveViewCounterWeb.Endpoint,
url: [host: "localhost"],
- secret_key_base: "s0e+LZ/leTtv3peHaFhnd2rbncAeV5qlR1rNShKXDMSRbVgU2Aar8nyXszsQrZ1p",
- render_errors: [view: LiveViewCounterWeb.ErrorView, accepts: ~w(html json), layout: false],
+ render_errors: [
+ formats: [html: LiveViewCounterWeb.ErrorHTML, json: LiveViewCounterWeb.ErrorJSON],
+ layout: false
+ ],
pubsub_server: LiveViewCounter.PubSub,
- live_view: [signing_salt: "iluKTpVJp8PgtRHYv1LSItNuQ1bLdR7c"]
+ live_view: [signing_salt: "iwg//jCM"]
+
+# Configures the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :live_view_counter, LiveViewCounter.Mailer, adapter: Swoosh.Adapters.Local
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.14.41",
+ default: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configure tailwind (the version is required)
+config :tailwind,
+ version: "3.1.8",
+ default: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/app.css
+ --output=../priv/static/assets/app.css
+ ),
+ cd: Path.expand("../assets", __DIR__)
+ ]
# Configures Elixir's Logger
config :logger, :console,
@@ -25,12 +58,4 @@ config :phoenix, :json_library, Jason
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
-import_config "#{Mix.env()}.exs"
-
-config :esbuild,
- version: "0.13.4",
- default: [
- args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
- cd: Path.expand("../assets", __DIR__),
- env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
- ]
+import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
index 8bdb2934..73f4721a 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -1,19 +1,22 @@
-import Mix.Config
+import Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
-# with webpack to recompile .js and .css sources.
+# with esbuild to bundle .js and .css sources.
config :live_view_counter, LiveViewCounterWeb.Endpoint,
- http: [port: 4000],
- debug_errors: true,
- code_reloader: true,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "Nsb0hNCqmJh669/MOTjRZp0rTuvHjJK1tAnrCV5sysOx6XQLdDA6D1MO320BSX9v",
watchers: [
- # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
- esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
# ## SSL Support
@@ -24,7 +27,6 @@ config :live_view_counter, LiveViewCounterWeb.Endpoint,
#
# mix phx.gen.cert
#
-# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
@@ -51,6 +53,9 @@ config :live_view_counter, LiveViewCounterWeb.Endpoint,
]
]
+# Enable dev routes for dashboard and mailbox
+config :live_view_counter, dev_routes: true
+
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
@@ -60,3 +65,6 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
index 59030473..96f79647 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -1,25 +1,21 @@
-import Mix.Config
+import Config
# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
-#
+
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
-config :live_view_counter, LiveViewCounterWeb.Endpoint,
- load_from_system_env: true,
- http: [port: {:system, "PORT"}],
- url: [scheme: "https", host: "live-view-counter.herokuapp.com", port: 443],
- force_ssl: [rewrite_on: [:x_forwarded_proto]],
- cache_static_manifest: "priv/static/cache_manifest.json",
- secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
+config :live_view_counter, LiveViewCounterWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Configures Swoosh API Client
+config :swoosh, :api_client, LiveViewCounter.Finch
# Do not print debug messages in production
config :logger, level: :info
-# Finally import the config/prod.secret.exs which loads secrets
-# and configuration from environment variables.
-# import_config "prod.secret.exs"
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/config/runtime.exs b/config/runtime.exs
new file mode 100644
index 00000000..516e3d5a
--- /dev/null
+++ b/config/runtime.exs
@@ -0,0 +1,100 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/live_view_counter start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :live_view_counter, LiveViewCounterWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :live_view_counter, LiveViewCounterWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :live_view_counter, LiveViewCounterWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your endpoint, ensuring
+ # no data is ever sent via http, always redirecting to https:
+ #
+ # config :live_view_counter, LiveViewCounterWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Also, you may need to configure the Swoosh API client of your choice if you
+ # are not using SMTP. Here is an example of the configuration:
+ #
+ # config :live_view_counter, LiveViewCounter.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # For this example you need include a HTTP client required by Swoosh API client.
+ # Swoosh supports Hackney and Finch out of the box:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+end
diff --git a/config/test.exs b/config/test.exs
index 1bfc0410..fa49ce9c 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,10 +1,21 @@
-import Mix.Config
+import Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :live_view_counter, LiveViewCounterWeb.Endpoint,
- http: [port: 4002],
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "i3VK2GAzdHdtGN0EVVWAQAHryZJZr+NQgQpwIV+5x1GSMSKrDfAE1W+vQDmTvFsg",
server: false
+# In test we don't send emails.
+config :live_view_counter, LiveViewCounter.Mailer,
+ adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
+
# Print only warnings and errors during test
-config :logger, level: :warn
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/coveralls.json b/coveralls.json
index 16aa4e4b..81c48eed 100644
--- a/coveralls.json
+++ b/coveralls.json
@@ -10,6 +10,9 @@
"lib/live_view_counter_web/views/error_helpers.ex",
"lib/live_view_counter_web/router.ex",
"lib/live_view_counter_web/live/page_live.ex",
+ "lib/live_view_counter_web/components/core_components.ex",
+ "lib/live_view_counter_web/controllers/error_json.ex",
+ "lib/live_view_counter_web/controllers/error_html.ex",
"test/"
]
}
diff --git a/elixir_buildpack.config b/elixir_buildpack.config
index 60b00ef3..058eab74 100644
--- a/elixir_buildpack.config
+++ b/elixir_buildpack.config
@@ -1,9 +1,12 @@
# Elixir version
-elixir_version=1.12.3
+# We need 1.14 for all the features in live view to work, although it will work with other versions
+# elixir_version=1.12.3
+elixir_version=1.14.2
# Erlang version
# available versions https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
-erlang_version=23.3.2
+#erlang_version=23.3.2
+erlang_version=25.1.2
# build assets
hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild"
\ No newline at end of file
diff --git a/lib/live_view_counter/application.ex b/lib/live_view_counter/application.ex
index 7472a9f9..a1704b75 100644
--- a/lib/live_view_counter/application.ex
+++ b/lib/live_view_counter/application.ex
@@ -5,15 +5,19 @@ defmodule LiveViewCounter.Application do
use Application
+ @impl true
def start(_type, _args) do
children = [
- # Start the App State
+ # Start the app state
LiveViewCounter.Count,
# Start the Telemetry supervisor
LiveViewCounterWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: LiveViewCounter.PubSub},
+ # Add Presence
LiveViewCounter.Presence,
+ # Start Finch
+ {Finch, name: LiveViewCounter.Finch},
# Start the Endpoint (http/https)
LiveViewCounterWeb.Endpoint
# Start a worker by calling: LiveViewCounter.Worker.start_link(arg)
@@ -28,6 +32,7 @@ defmodule LiveViewCounter.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
+ @impl true
def config_change(changed, _new, removed) do
LiveViewCounterWeb.Endpoint.config_change(changed, removed)
:ok
diff --git a/lib/live_view_counter/count.ex b/lib/live_view_counter/counter.ex
similarity index 80%
rename from lib/live_view_counter/count.ex
rename to lib/live_view_counter/counter.ex
index e400aa40..d62c1cf7 100644
--- a/lib/live_view_counter/count.ex
+++ b/lib/live_view_counter/counter.ex
@@ -7,6 +7,8 @@ defmodule LiveViewCounter.Count do
@start_value 0
+ # ------------ External API (runs in client process) ---------
+
def topic do
"count"
end
@@ -16,13 +18,14 @@ defmodule LiveViewCounter.Count do
end
def incr() do
- GenServer.call @name, :incr
+ GenServer.call(@name, :incr)
end
def decr() do
GenServer.call @name, :decr
end
+
def current() do
GenServer.call @name, :current
end
@@ -31,8 +34,10 @@ defmodule LiveViewCounter.Count do
{:ok, start_count}
end
+ # --------- Implementation (runs in GenServer process) ----------start_count
+
def handle_call(:current, _from, count) do
- {:reply, count, count}
+ {:reply, count, count}
end
def handle_call(:incr, _from, count) do
@@ -48,4 +53,5 @@ defmodule LiveViewCounter.Count do
PubSub.broadcast(LiveViewCounter.PubSub, topic(), {:count, new_count})
{:reply, new_count, new_count}
end
+
end
diff --git a/lib/live_view_counter/mailer.ex b/lib/live_view_counter/mailer.ex
new file mode 100644
index 00000000..c5b641fd
--- /dev/null
+++ b/lib/live_view_counter/mailer.ex
@@ -0,0 +1,3 @@
+defmodule LiveViewCounter.Mailer do
+ use Swoosh.Mailer, otp_app: :live_view_counter
+end
diff --git a/lib/live_view_counter/presence.ex b/lib/live_view_counter/presence.ex
index d8aa9578..e3c4cb53 100644
--- a/lib/live_view_counter/presence.ex
+++ b/lib/live_view_counter/presence.ex
@@ -1,5 +1,5 @@
defmodule LiveViewCounter.Presence do
use Phoenix.Presence,
- otp_app: :live_view_counter,
- pubsub_server: LiveViewCounter.PubSub
+ otp_app: :live_view_counter,
+ pubsub_server: LiveViewCounter.PubSub
end
diff --git a/lib/live_view_counter_web.ex b/lib/live_view_counter_web.ex
index f99e4977..a3496c55 100644
--- a/lib/live_view_counter_web.ex
+++ b/lib/live_view_counter_web.ex
@@ -1,55 +1,61 @@
defmodule LiveViewCounterWeb do
@moduledoc """
The entrypoint for defining your web interface, such
- as controllers, views, channels and so on.
+ as controllers, components, channels, and so on.
This can be used in your application as:
use LiveViewCounterWeb, :controller
- use LiveViewCounterWeb, :view
+ use LiveViewCounterWeb, :html
- The definitions below will be executed for every view,
- controller, etc, so keep them short and clean, focused
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
- below. Instead, define any helper function in modules
- and import those modules here.
+ below. Instead, define additional modules and import
+ those modules here.
"""
- def controller do
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
quote do
- use Phoenix.Controller, namespace: LiveViewCounterWeb
+ use Phoenix.Router, helpers: false
+ # Import common connection and controller functions to use in pipelines
import Plug.Conn
- import LiveViewCounterWeb.Gettext
- alias LiveViewCounterWeb.Router.Helpers, as: Routes
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
- import Phoenix.LiveView.Controller
+ def channel do
+ quote do
+ use Phoenix.Channel
end
end
- def view do
+ def controller do
quote do
- use Phoenix.View,
- root: "lib/live_view_counter_web/templates",
- namespace: LiveViewCounterWeb
+ use Phoenix.Controller,
+ namespace: LiveViewCounterWeb,
+ formats: [:html, :json],
+ layouts: [html: LiveViewCounterWeb.Layouts]
- # Import convenience functions from controllers
- import Phoenix.Controller,
- only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
+ import Plug.Conn
+ import LiveViewCounterWeb.Gettext
- # Include shared imports and aliases for views
- unquote(view_helpers())
+ unquote(verified_routes())
end
end
def live_view do
quote do
use Phoenix.LiveView,
- layout: {LiveViewCounterWeb.LayoutView, "live.html"}
+ layout: {LiveViewCounterWeb.Layouts, :app}
- unquote(view_helpers())
+ unquote(html_helpers())
end
end
@@ -57,41 +63,45 @@ defmodule LiveViewCounterWeb do
quote do
use Phoenix.LiveComponent
- unquote(view_helpers())
+ unquote(html_helpers())
end
end
- def router do
+ def html do
quote do
- use Phoenix.Router
+ use Phoenix.Component
- import Plug.Conn
- import Phoenix.Controller
- import Phoenix.LiveView.Router
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
end
end
- def channel do
+ defp html_helpers do
quote do
- use Phoenix.Channel
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import LiveViewCounterWeb.CoreComponents
import LiveViewCounterWeb.Gettext
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
end
end
- defp view_helpers do
+ def verified_routes do
quote do
- # Use all HTML functionality (forms, tags, etc)
- use Phoenix.HTML
-
- # Import LiveView helpers (live_render, live_component, live_patch, etc)
- import Phoenix.LiveView.Helpers
-
- # Import basic rendering functionality (render, render_layout, etc)
- import Phoenix.View
-
- import LiveViewCounterWeb.ErrorHelpers
- import LiveViewCounterWeb.Gettext
- alias LiveViewCounterWeb.Router.Helpers, as: Routes
+ use Phoenix.VerifiedRoutes,
+ endpoint: LiveViewCounterWeb.Endpoint,
+ router: LiveViewCounterWeb.Router,
+ statics: LiveViewCounterWeb.static_paths()
end
end
diff --git a/lib/live_view_counter_web/channels/user_socket.ex b/lib/live_view_counter_web/channels/user_socket.ex
deleted file mode 100644
index ccbde471..00000000
--- a/lib/live_view_counter_web/channels/user_socket.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule LiveViewCounterWeb.UserSocket do
- use Phoenix.Socket
-
- ## Channels
- # channel "room:*", LiveViewCounterWeb.RoomChannel
-
- # Socket params are passed from the client and can
- # be used to verify and authenticate a user. After
- # verification, you can put default assigns into
- # the socket that will be set for all channels, ie
- #
- # {:ok, assign(socket, :user_id, verified_user_id)}
- #
- # To deny connection, return `:error`.
- #
- # See `Phoenix.Token` documentation for examples in
- # performing token verification on connect.
- @impl true
- def connect(_params, socket, _connect_info) do
- {:ok, socket}
- end
-
- # Socket id's are topics that allow you to identify all sockets for a given user:
- #
- # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
- #
- # Would allow you to broadcast a "disconnect" event and terminate
- # all active sockets and channels for a given user:
- #
- # LiveViewCounterWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
- #
- # Returning `nil` makes this socket anonymous.
- @impl true
- def id(_socket), do: nil
-end
diff --git a/lib/live_view_counter_web/components/core_components.ex b/lib/live_view_counter_web/components/core_components.ex
new file mode 100644
index 00000000..aba97bbf
--- /dev/null
+++ b/lib/live_view_counter_web/components/core_components.ex
@@ -0,0 +1,623 @@
+defmodule LiveViewCounterWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ The components in this module use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
+ customize the generated components in this module.
+
+ Icons are provided by [heroicons](https://heroicons.com), using the
+ [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+ import LiveViewCounterWeb.Gettext
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ Are you sure?
+ <:confirm>OK
+ <:cancel>Cancel
+
+
+ JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
+ for the caller to react to each button press, for example:
+
+ <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
+ Are you sure you?
+ <:confirm>OK
+ <:cancel>Cancel
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ attr :on_confirm, JS, default: %JS{}
+
+ slot :inner_block, required: true
+ slot :title
+ slot :subtitle
+ slot :confirm
+ slot :cancel
+
+ def modal(assigns) do
+ ~H"""
+
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ transition:
+ {"transition-all transform ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all transform ease-in duration-200",
+ "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
+ js
+ |> JS.show(to: "##{id}")
+ |> JS.show(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+ )
+ |> show("##{id}-container")
+ |> JS.focus_first(to: "##{id}-content")
+ end
+
+ def hide_modal(js \\ %JS{}, id) do
+ js
+ |> JS.hide(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+ )
+ |> hide("##{id}-container")
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+ |> JS.pop_focus()
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate "is invalid" in the "errors" domain
+ # dgettext("errors", "is invalid")
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ # This requires us to call the Gettext module passing our gettext
+ # backend as first argument.
+ #
+ # Note we use the "errors" domain, which means translations
+ # should be written to the errors.po file. The :count option is
+ # set by Ecto and indicates we should also apply plural rules.
+ if count = opts[:count] do
+ Gettext.dngettext(LiveViewCounterWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(LiveViewCounterWeb.Gettext, "errors", msg, opts)
+ end
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+
+ defp input_equals?(val1, val2) do
+ Phoenix.HTML.html_escape(val1) == Phoenix.HTML.html_escape(val2)
+ end
+end
diff --git a/lib/live_view_counter_web/components/layouts.ex b/lib/live_view_counter_web/components/layouts.ex
new file mode 100644
index 00000000..f19592e0
--- /dev/null
+++ b/lib/live_view_counter_web/components/layouts.ex
@@ -0,0 +1,5 @@
+defmodule LiveViewCounterWeb.Layouts do
+ use LiveViewCounterWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/lib/live_view_counter_web/components/layouts/app.html.heex b/lib/live_view_counter_web/components/layouts/app.html.heex
new file mode 100644
index 00000000..fa10840f
--- /dev/null
+++ b/lib/live_view_counter_web/components/layouts/app.html.heex
@@ -0,0 +1,55 @@
+
+
+
diff --git a/lib/live_view_counter_web/components/layouts/root.html.heex b/lib/live_view_counter_web/components/layouts/root.html.heex
new file mode 100644
index 00000000..9edd9af9
--- /dev/null
+++ b/lib/live_view_counter_web/components/layouts/root.html.heex
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <.live_title suffix=" 路 Phoenix Framework">
+ <%= assigns[:page_title] || "LiveViewCounter" %>
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/lib/live_view_counter_web/controllers/error_html.ex b/lib/live_view_counter_web/controllers/error_html.ex
new file mode 100644
index 00000000..7455e640
--- /dev/null
+++ b/lib/live_view_counter_web/controllers/error_html.ex
@@ -0,0 +1,19 @@
+defmodule LiveViewCounterWeb.ErrorHTML do
+ use LiveViewCounterWeb, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/live_view_counter_web/controllers/error/404.html.heex
+ # * lib/live_view_counter_web/controllers/error/500.html.heex
+ #
+ # embed_templates "error/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/lib/live_view_counter_web/controllers/error_json.ex b/lib/live_view_counter_web/controllers/error_json.ex
new file mode 100644
index 00000000..16a4f311
--- /dev/null
+++ b/lib/live_view_counter_web/controllers/error_json.ex
@@ -0,0 +1,15 @@
+defmodule LiveViewCounterWeb.ErrorJSON do
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/lib/live_view_counter_web/controllers/page_html.ex b/lib/live_view_counter_web/controllers/page_html.ex
new file mode 100644
index 00000000..119ea3ba
--- /dev/null
+++ b/lib/live_view_counter_web/controllers/page_html.ex
@@ -0,0 +1,5 @@
+defmodule LiveViewCounterWeb.PageHTML do
+ use LiveViewCounterWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/lib/live_view_counter_web/controllers/page_html/home.html.heex b/lib/live_view_counter_web/controllers/page_html/home.html.heex
new file mode 100644
index 00000000..ca89da64
--- /dev/null
+++ b/lib/live_view_counter_web/controllers/page_html/home.html.heex
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+ Phoenix Framework
+
+ v1.7
+
+
+
+ Peace of mind from prototype to production.
+
+
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+
-
-
diff --git a/lib/live_view_counter_web/router.ex b/lib/live_view_counter_web/router.ex
index 6382bcf1..1e292a1d 100644
--- a/lib/live_view_counter_web/router.ex
+++ b/lib/live_view_counter_web/router.ex
@@ -5,7 +5,7 @@ defmodule LiveViewCounterWeb.Router do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
- plug :put_root_layout, {LiveViewCounterWeb.LayoutView, :root}
+ plug :put_root_layout, {LiveViewCounterWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
@@ -16,8 +16,8 @@ defmodule LiveViewCounterWeb.Router do
scope "/", LiveViewCounterWeb do
pipe_through :browser
-
- live("/", Counter)
+
+ live "/", Counter
end
# Other scopes may use custom stacks.
@@ -25,19 +25,20 @@ defmodule LiveViewCounterWeb.Router do
# pipe_through :api
# end
- # Enables LiveDashboard only for development
- #
- # If you want to use the LiveDashboard in production, you should put
- # it behind authentication and allow only admins to access it.
- # If your application does not have an admins-only section yet,
- # you can use Plug.BasicAuth to set up some basic authentication
- # as long as you are also using SSL (which you should anyway).
- if Mix.env() in [:dev, :test] do
+ # Enable LiveDashboard and Swoosh mailbox preview in development
+ if Application.compile_env(:live_view_counter, :dev_routes) do
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
- scope "/" do
+ scope "/dev" do
pipe_through :browser
+
live_dashboard "/dashboard", metrics: LiveViewCounterWeb.Telemetry
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end
diff --git a/lib/live_view_counter_web/telemetry.ex b/lib/live_view_counter_web/telemetry.ex
index 0a93ac15..1bc7980e 100644
--- a/lib/live_view_counter_web/telemetry.ex
+++ b/lib/live_view_counter_web/telemetry.ex
@@ -22,13 +22,34 @@ defmodule LiveViewCounterWeb.Telemetry do
def metrics do
[
# Phoenix Metrics
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
+ summary("phoenix.router_dispatch.start.system_time",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.exception.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
+ summary("phoenix.socket_connected.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_join.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_handled_in.duration",
+ tags: [:event],
+ unit: {:native, :millisecond}
+ ),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
diff --git a/lib/live_view_counter_web/templates/counter.html.leex b/lib/live_view_counter_web/templates/counter.html.leex
deleted file mode 100644
index 5d6405c0..00000000
--- a/lib/live_view_counter_web/templates/counter.html.leex
+++ /dev/null
@@ -1,5 +0,0 @@
-