changed .gitignore
 
@@ -22,13 +22,10 @@ erl_crash.dump
22
22
# Ignore package tarball (built via "mix hex.build").
23
23
my_app-*.tar
24
24
25
- # If NPM crashes, it generates a log, let's ignore it too.
26
- npm-debug.log
25
+ # Ignore assets that are produced by build tools.
26
+ /priv/static/assets/
27
27
28
- # The directory NPM downloads your dependencies sources to.
28
+ # In case you use Node.js/npm, you want to ignore these.
29
+ npm-debug.log
29
30
/assets/node_modules/
30
31
31
- # Since we are building assets from assets/,
32
- # we ignore priv/static. You may want to comment
33
- # this depending on your deployment strategy.
34
- /priv/static/
changed README.md
 
@@ -4,8 +4,7 @@ To start your Phoenix server:
4
4
5
5
* Install dependencies with `mix deps.get`
6
6
* Create and migrate your database with `mix ecto.setup`
7
- * Install Node.js dependencies with `npm install` inside the `assets` directory
8
- * Start Phoenix endpoint with `mix phx.server`
7
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
9
8
10
9
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
11
10
removed assets/.babelrc
 
@@ -1,5 +0,0 @@
1
- {
2
- "presets": [
3
- "@babel/preset-env"
4
- ]
5
- }
changed assets/css/app.css
 
@@ -1,7 +1,7 @@
1
- /* This file is for your main application css. */
1
+ /* This file is for your main application CSS */
2
2
@import "./phoenix.css";
3
3
4
- /* Alerts and form errors */
4
+ /* Alerts and form errors used by phx.new */
5
5
.alert {
6
6
padding: 15px;
7
7
margin-bottom: 20px;
 
@@ -34,3 +34,56 @@
34
34
display: block;
35
35
margin: -1rem 0 2rem;
36
36
}
37
+
38
+ /* LiveView specific classes for your customization */
39
+ .phx-no-feedback.invalid-feedback,
40
+ .phx-no-feedback .invalid-feedback {
41
+ display: none;
42
+ }
43
+
44
+ .phx-click-loading {
45
+ opacity: 0.5;
46
+ transition: opacity 1s ease-out;
47
+ }
48
+
49
+ .phx-disconnected{
50
+ cursor: wait;
51
+ }
52
+ .phx-disconnected *{
53
+ pointer-events: none;
54
+ }
55
+
56
+ .phx-modal {
57
+ opacity: 1!important;
58
+ position: fixed;
59
+ z-index: 1;
60
+ left: 0;
61
+ top: 0;
62
+ width: 100%;
63
+ height: 100%;
64
+ overflow: auto;
65
+ background-color: rgb(0,0,0);
66
+ background-color: rgba(0,0,0,0.4);
67
+ }
68
+
69
+ .phx-modal-content {
70
+ background-color: #fefefe;
71
+ margin: 15vh auto;
72
+ padding: 20px;
73
+ border: 1px solid #888;
74
+ width: 80%;
75
+ }
76
+
77
+ .phx-modal-close {
78
+ color: #aaa;
79
+ float: right;
80
+ font-size: 28px;
81
+ font-weight: bold;
82
+ }
83
+
84
+ .phx-modal-close:hover,
85
+ .phx-modal-close:focus {
86
+ color: black;
87
+ text-decoration: none;
88
+ cursor: pointer;
89
+ }
changed assets/css/phoenix.css
 
@@ -2,11 +2,11 @@
2
2
* This can be safely deleted to start fresh.
3
3
*/
4
4
5
- /* Milligram v1.3.0 https://milligram.github.io
6
- * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license
5
+ /* Milligram v1.4.1 https://milligram.github.io
6
+ * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license
7
7
*/
8
8
9
- *,*: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,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%230069d9" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}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}
9
+ *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;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='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-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 .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']: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,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%23d1d1d1" d="M0,0l6,8l6-8"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%230069d9" d="M0,0l6,8l6-8"/></svg>')}select[multiple]{background:none;height:auto}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}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.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-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.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{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;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}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}
10
10
11
11
/* General style */
12
12
h1{font-size: 3.6rem; line-height: 1.25}
changed assets/js/app.js
 
@@ -1,15 +1,44 @@
1
- // We need to import the CSS so that webpack will load it.
2
- // The MiniCssExtractPlugin is used to separate it out into
3
- // its own CSS file.
1
+ // We import the CSS which is extracted to its own file by esbuild.
2
+ // Remove this line if you add a your own CSS build pipeline (e.g postcss).
4
3
import "../css/app.css"
5
4
6
- // webpack automatically bundles all modules in your
7
- // entry points. Those entry points can be configured
8
- // in "webpack.config.js".
5
+ // If you want to use Phoenix channels, run `mix help phx.gen.channel`
6
+ // to get started and then uncomment the line below.
7
+ // import "./user_socket.js"
8
+
9
+ // You can include dependencies in two ways.
9
10
//
10
- // Import deps with the dep name or local files with a relative path, for example:
11
+ // The simplest option is to put them in assets/vendor and
12
+ // import them using relative paths:
11
13
//
12
- // import {Socket} from "phoenix"
13
- // import socket from "./socket"
14
+ // import "./vendor/some-package.js"
14
15
//
16
+ // Alternatively, you can `npm install some-package` and import
17
+ // them using a path starting with the package name:
18
+ //
19
+ // import "some-package"
20
+ //
21
+
22
+ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
15
23
import "phoenix_html"
24
+ // Establish Phoenix Socket and LiveView configuration.
25
+ import {Socket} from "phoenix"
26
+ import {LiveSocket} from "phoenix_live_view"
27
+ import topbar from "../vendor/topbar"
28
+
29
+ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
30
+ let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
31
+
32
+ // Show progress bar on live navigation and form submits
33
+ topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
34
+ window.addEventListener("phx:page-loading-start", info => topbar.show())
35
+ window.addEventListener("phx:page-loading-stop", info => topbar.hide())
36
+
37
+ // connect if there are any LiveViews on the page
38
+ liveSocket.connect()
39
+
40
+ // expose liveSocket on window for web console debug logs and latency simulation:
41
+ // >> liveSocket.enableDebug()
42
+ // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
43
+ // >> liveSocket.disableLatencySim()
44
+ window.liveSocket = liveSocket
removed assets/js/socket.js
 
@@ -1,63 +0,0 @@
1
- // NOTE: The contents of this file will only be executed if
2
- // you uncomment its entry in "assets/js/app.js".
3
-
4
- // To use Phoenix channels, the first step is to import Socket,
5
- // and connect at the socket path in "lib/web/endpoint.ex".
6
- //
7
- // Pass the token on params as below. Or remove it
8
- // from the params if you are not using authentication.
9
- import {Socket} from "phoenix"
10
-
11
- let socket = new Socket("/socket", {params: {token: window.userToken}})
12
-
13
- // When you connect, you'll often need to authenticate the client.
14
- // For example, imagine you have an authentication plug, `MyAuth`,
15
- // which authenticates the session and assigns a `:current_user`.
16
- // If the current user exists you can assign the user's token in
17
- // the connection for use in the layout.
18
- //
19
- // In your "lib/web/router.ex":
20
- //
21
- // pipeline :browser do
22
- // ...
23
- // plug MyAuth
24
- // plug :put_user_token
25
- // end
26
- //
27
- // defp put_user_token(conn, _) do
28
- // if current_user = conn.assigns[:current_user] do
29
- // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
30
- // assign(conn, :user_token, token)
31
- // else
32
- // conn
33
- // end
34
- // end
35
- //
36
- // Now you need to pass this token to JavaScript. You can do so
37
- // inside a script tag in "lib/web/templates/layout/app.html.eex":
38
- //
39
- // <script>window.userToken = "<%= assigns[:user_token] %>";</script>
40
- //
41
- // You will need to verify the user token in the "connect/3" function
42
- // in "lib/web/channels/user_socket.ex":
43
- //
44
- // def connect(%{"token" => token}, socket, _connect_info) do
45
- // # max_age: 1209600 is equivalent to two weeks in seconds
46
- // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
47
- // {:ok, user_id} ->
48
- // {:ok, assign(socket, :user, user_id)}
49
- // {:error, reason} ->
50
- // :error
51
- // end
52
- // end
53
- //
54
- // Finally, connect to the socket:
55
- socket.connect()
56
-
57
- // Now that you are connected, you can join channels with a topic:
58
- let channel = socket.channel("topic:subtopic", {})
59
- channel.join()
60
- .receive("ok", resp => { console.log("Joined successfully", resp) })
61
- .receive("error", resp => { console.log("Unable to join", resp) })
62
-
63
- export default socket
removed assets/package.json
 
@@ -1,28 +0,0 @@
1
- {
2
- "repository": {},
3
- "description": " ",
4
- "license": "MIT",
5
- "scripts": {
6
- "deploy": "webpack --mode production",
7
- "watch": "webpack --mode development --watch"
8
- },
9
- "dependencies": {
10
- "phoenix": "file:../deps/phoenix",
11
- "phoenix_html": "file:../deps/phoenix_html"
12
- },
13
- "devDependencies": {
14
- "@babel/core": "^7.0.0",
15
- "@babel/preset-env": "^7.0.0",
16
- "babel-loader": "^8.0.0",
17
- "copy-webpack-plugin": "^5.1.1",
18
- "css-loader": "^3.4.2",
19
- "sass-loader": "^8.0.2",
20
- "node-sass": "^4.13.1",
21
- "hard-source-webpack-plugin": "^0.13.1",
22
- "mini-css-extract-plugin": "^0.9.0",
23
- "optimize-css-assets-webpack-plugin": "^5.0.1",
24
- "terser-webpack-plugin": "^2.3.2",
25
- "webpack": "^4.41.5",
26
- "webpack-cli": "^3.3.2"
27
- }
28
- }
added assets/vendor/topbar.js
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @license MIT
3
+ * topbar 1.0.0, 2021-01-06
4
+ * http://buunguyen.github.io/topbar
5
+ * Copyright (c) 2021 Buu Nguyen
6
+ */
7
+ (function (window, document) {
8
+ "use strict";
9
+
10
+ // https://gist.github.com/paulirish/1579671
11
+ (function () {
12
+ var lastTime = 0;
13
+ var vendors = ["ms", "moz", "webkit", "o"];
14
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15
+ window.requestAnimationFrame =
16
+ window[vendors[x] + "RequestAnimationFrame"];
17
+ window.cancelAnimationFrame =
18
+ window[vendors[x] + "CancelAnimationFrame"] ||
19
+ window[vendors[x] + "CancelRequestAnimationFrame"];
20
+ }
21
+ if (!window.requestAnimationFrame)
22
+ window.requestAnimationFrame = function (callback, element) {
23
+ var currTime = new Date().getTime();
24
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25
+ var id = window.setTimeout(function () {
26
+ callback(currTime + timeToCall);
27
+ }, timeToCall);
28
+ lastTime = currTime + timeToCall;
29
+ return id;
30
+ };
31
+ if (!window.cancelAnimationFrame)
32
+ window.cancelAnimationFrame = function (id) {
33
+ clearTimeout(id);
34
+ };
35
+ })();
36
+
37
+ var canvas,
38
+ progressTimerId,
39
+ fadeTimerId,
40
+ currentProgress,
41
+ showing,
42
+ addEvent = function (elem, type, handler) {
43
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
44
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
45
+ else elem["on" + type] = handler;
46
+ },
47
+ options = {
48
+ autoRun: true,
49
+ barThickness: 3,
50
+ barColors: {
51
+ 0: "rgba(26, 188, 156, .9)",
52
+ ".25": "rgba(52, 152, 219, .9)",
53
+ ".50": "rgba(241, 196, 15, .9)",
54
+ ".75": "rgba(230, 126, 34, .9)",
55
+ "1.0": "rgba(211, 84, 0, .9)",
56
+ },
57
+ shadowBlur: 10,
58
+ shadowColor: "rgba(0, 0, 0, .6)",
59
+ className: null,
60
+ },
61
+ repaint = function () {
62
+ canvas.width = window.innerWidth;
63
+ canvas.height = options.barThickness * 5; // need space for shadow
64
+
65
+ var ctx = canvas.getContext("2d");
66
+ ctx.shadowBlur = options.shadowBlur;
67
+ ctx.shadowColor = options.shadowColor;
68
+
69
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
70
+ for (var stop in options.barColors)
71
+ lineGradient.addColorStop(stop, options.barColors[stop]);
72
+ ctx.lineWidth = options.barThickness;
73
+ ctx.beginPath();
74
+ ctx.moveTo(0, options.barThickness / 2);
75
+ ctx.lineTo(
76
+ Math.ceil(currentProgress * canvas.width),
77
+ options.barThickness / 2
78
+ );
79
+ ctx.strokeStyle = lineGradient;
80
+ ctx.stroke();
81
+ },
82
+ createCanvas = function () {
83
+ canvas = document.createElement("canvas");
84
+ var style = canvas.style;
85
+ style.position = "fixed";
86
+ style.top = style.left = style.right = style.margin = style.padding = 0;
87
+ style.zIndex = 100001;
88
+ style.display = "none";
89
+ if (options.className) canvas.classList.add(options.className);
90
+ document.body.appendChild(canvas);
91
+ addEvent(window, "resize", repaint);
92
+ },
93
+ topbar = {
94
+ config: function (opts) {
95
+ for (var key in opts)
96
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
97
+ },
98
+ show: function () {
99
+ if (showing) return;
100
+ showing = true;
101
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
102
+ if (!canvas) createCanvas();
103
+ canvas.style.opacity = 1;
104
+ canvas.style.display = "block";
105
+ topbar.progress(0);
106
+ if (options.autoRun) {
107
+ (function loop() {
108
+ progressTimerId = window.requestAnimationFrame(loop);
109
+ topbar.progress(
110
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
111
+ );
112
+ })();
113
+ }
114
+ },
115
+ progress: function (to) {
116
+ if (typeof to === "undefined") return currentProgress;
117
+ if (typeof to === "string") {
118
+ to =
119
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
120
+ ? currentProgress
121
+ : 0) + parseFloat(to);
122
+ }
123
+ currentProgress = to > 1 ? 1 : to;
124
+ repaint();
125
+ return currentProgress;
126
+ },
127
+ hide: function () {
128
+ if (!showing) return;
129
+ showing = false;
130
+ if (progressTimerId != null) {
131
+ window.cancelAnimationFrame(progressTimerId);
132
+ progressTimerId = null;
133
+ }
134
+ (function loop() {
135
+ if (topbar.progress("+.1") >= 1) {
136
+ canvas.style.opacity -= 0.05;
137
+ if (canvas.style.opacity <= 0.05) {
138
+ canvas.style.display = "none";
139
+ fadeTimerId = null;
140
+ return;
141
+ }
142
+ }
143
+ fadeTimerId = window.requestAnimationFrame(loop);
144
+ })();
145
+ },
146
+ };
147
+
148
+ if (typeof module === "object" && typeof module.exports === "object") {
149
+ module.exports = topbar;
150
+ } else if (typeof define === "function" && define.amd) {
151
+ define(function () {
152
+ return topbar;
153
+ });
154
+ } else {
155
+ this.topbar = topbar;
156
+ }
157
+ }.call(this, window, document));
removed assets/webpack.config.js
 
@@ -1,53 +0,0 @@
1
- const path = require('path');
2
- const glob = require('glob');
3
- const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
4
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5
- const TerserPlugin = require('terser-webpack-plugin');
6
- const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
7
- const CopyWebpackPlugin = require('copy-webpack-plugin');
8
-
9
- module.exports = (env, options) => {
10
- const devMode = options.mode !== 'production';
11
-
12
- return {
13
- optimization: {
14
- minimizer: [
15
- new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
16
- new OptimizeCSSAssetsPlugin({})
17
- ]
18
- },
19
- entry: {
20
- 'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
21
- },
22
- output: {
23
- filename: '[name].js',
24
- path: path.resolve(__dirname, '../priv/static/js'),
25
- publicPath: '/js/'
26
- },
27
- devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
28
- module: {
29
- rules: [
30
- {
31
- test: /\.js$/,
32
- exclude: /node_modules/,
33
- use: {
34
- loader: 'babel-loader'
35
- }
36
- },
37
- {
38
- test: /\.[s]?css$/,
39
- use: [
40
- MiniCssExtractPlugin.loader,
41
- 'css-loader',
42
- 'sass-loader',
43
- ],
44
- }
45
- ]
46
- },
47
- plugins: [
48
- new MiniCssExtractPlugin({ filename: '../css/app.css' }),
49
- new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
50
- ]
51
- .concat(devMode ? [new HardSourceWebpackPlugin()] : [])
52
- }
53
- };
changed config/config.exs
 
@@ -1,11 +1,11 @@
1
1
# This file is responsible for configuring your application
2
- # and its dependencies with the aid of the Mix.Config module.
2
+ # and its dependencies with the aid of the Config module.
3
3
#
4
4
# This configuration file is loaded before any dependency and
5
5
# is restricted to this project.
6
6
7
7
# General application configuration
8
- use Mix.Config
8
+ import Config
9
9
10
10
config :my_app,
11
11
ecto_repos: [MyApp.Repo]
 
@@ -18,6 +18,27 @@ config :my_app, MyAppWeb.Endpoint,
18
18
pubsub_server: MyApp.PubSub,
19
19
live_view: [signing_salt: "foo"]
20
20
21
+ # Configures the mailer
22
+ #
23
+ # By default it uses the "Local" adapter which stores the emails
24
+ # locally. You can see the emails in your browser, at "/dev/mailbox".
25
+ #
26
+ # For production it's recommended to configure a different adapter
27
+ # at the `config/runtime.exs`.
28
+ config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Local
29
+
30
+ # Swoosh API client is needed for adapters other than SMTP.
31
+ config :swoosh, :api_client, false
32
+
33
+ # Configure esbuild (the version is required)
34
+ config :esbuild,
35
+ version: "0.12.18",
36
+ default: [
37
+ args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
38
+ cd: Path.expand("../assets", __DIR__),
39
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
40
+ ]
41
+
21
42
# Configures Elixir's Logger
22
43
config :logger, :console,
23
44
format: "$time $metadata[$level] $message\n",
 
@@ -28,4 +49,4 @@ config :phoenix, :json_library, Jason
28
49
29
50
# Import environment specific config. This must remain at the bottom
30
51
# of this file so it overrides the configuration defined above.
31
- import_config "#{Mix.env()}.exs"
52
+ import_config "#{config_env()}.exs"
changed config/dev.exs
 
@@ -1,4 +1,4 @@
1
- use Mix.Config
1
+ import Config
2
2
3
3
# Configure your database
4
4
config :my_app, MyApp.Repo,
 
@@ -14,20 +14,17 @@ config :my_app, MyApp.Repo,
14
14
#
15
15
# The watchers configuration can be used to run external
16
16
# watchers to your application. For example, we use it
17
- # with webpack to recompile .js and .css sources.
17
+ # with esbuild to bundle .js and .css sources.
18
18
config :my_app, MyAppWeb.Endpoint,
19
- http: [port: 4000],
19
+ # Binding to loopback ipv4 address prevents access from other machines.
20
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
21
+ http: [ip: {127, 0, 0, 1}, port: 4000],
20
22
debug_errors: true,
21
23
code_reloader: true,
22
24
check_origin: false,
23
25
watchers: [
24
- node: [
25
- "node_modules/webpack/bin/webpack.js",
26
- "--mode",
27
- "development",
28
- "--watch-stdin",
29
- cd: Path.expand("../assets", __DIR__)
30
- ]
26
+ # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
27
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
31
28
]
32
29
33
30
# ## SSL Support
changed config/prod.exs
 
@@ -1,4 +1,4 @@
1
- use Mix.Config
1
+ import Config
2
2
3
3
# For production, don't forget to configure the url host
4
4
# to something meaningful, Phoenix uses this information
 
@@ -22,14 +22,14 @@ config :logger, level: :info
22
22
# to the previous section and set your `:url` port to 443:
23
23
#
24
24
# config :my_app, MyAppWeb.Endpoint,
25
- # ...
25
+ # ...,
26
26
# url: [host: "example.com", port: 443],
27
27
# https: [
28
+ # ...,
28
29
# port: 443,
29
30
# cipher_suite: :strong,
30
31
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
31
- # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
32
- # transport_options: [socket_opts: [:inet6]]
32
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
33
33
# ]
34
34
#
35
35
# The `cipher_suite` is set to `:strong` to support only the
 
@@ -49,7 +49,3 @@ config :logger, level: :info
49
49
# force_ssl: [hsts: true]
50
50
#
51
51
# Check `Plug.SSL` for all available options in `force_ssl`.
52
-
53
- # Finally import the config/prod.secret.exs which loads secrets
54
- # and configuration from environment variables.
55
- import_config "prod.secret.exs"
removed config/prod.secret.exs
 
@@ -1,41 +0,0 @@
1
- # In this file, we load production configuration and secrets
2
- # from environment variables. You can also hardcode secrets,
3
- # although such is generally not recommended and you have to
4
- # remember to add this file to your .gitignore.
5
- use Mix.Config
6
-
7
- database_url =
8
- System.get_env("DATABASE_URL") ||
9
- raise """
10
- environment variable DATABASE_URL is missing.
11
- For example: ecto://USER:PASS@HOST/DATABASE
12
- """
13
-
14
- config :my_app, MyApp.Repo,
15
- # ssl: true,
16
- url: database_url,
17
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
18
-
19
- secret_key_base =
20
- System.get_env("SECRET_KEY_BASE") ||
21
- raise """
22
- environment variable SECRET_KEY_BASE is missing.
23
- You can generate one by calling: mix phx.gen.secret
24
- """
25
-
26
- config :my_app, MyAppWeb.Endpoint,
27
- http: [
28
- port: String.to_integer(System.get_env("PORT") || "4000"),
29
- transport_options: [socket_opts: [:inet6]]
30
- ],
31
- secret_key_base: secret_key_base
32
-
33
- # ## Using releases (Elixir v1.9+)
34
- #
35
- # If you are doing OTP releases, you need to instruct Phoenix
36
- # to start each relevant endpoint:
37
- #
38
- # config :my_app, MyAppWeb.Endpoint, server: true
39
- #
40
- # Then you can assemble a release by calling `mix release`.
41
- # See `mix help release` for more information.
added config/runtime.exs
 
@@ -0,0 +1,68 @@
1
+ import Config
2
+
3
+ # config/runtime.exs is executed for all environments, including
4
+ # during releases. It is executed after compilation and before the
5
+ # system starts, so it is typically used to load production configuration
6
+ # and secrets from environment variables or elsewhere. Do not define
7
+ # any compile-time configuration in here, as it won't be applied.
8
+ # The block below contains prod specific runtime configuration.
9
+ if config_env() == :prod do
10
+ database_url =
11
+ System.get_env("DATABASE_URL") ||
12
+ raise """
13
+ environment variable DATABASE_URL is missing.
14
+ For example: ecto://USER:PASS@HOST/DATABASE
15
+ """
16
+
17
+ config :my_app, MyApp.Repo,
18
+ # ssl: true,
19
+ # socket_options: [:inet6],
20
+ url: database_url,
21
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
22
+
23
+ secret_key_base =
24
+ System.get_env("SECRET_KEY_BASE") ||
25
+ raise """
26
+ environment variable SECRET_KEY_BASE is missing.
27
+ You can generate one by calling: mix phx.gen.secret
28
+ """
29
+
30
+ config :my_app, MyAppWeb.Endpoint,
31
+ http: [
32
+ # Enable IPv6 and bind on all interfaces.
33
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
34
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
35
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
36
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
37
+ port: String.to_integer(System.get_env("PORT") || "4000")
38
+ ],
39
+ secret_key_base: secret_key_base
40
+
41
+ # ## Using releases
42
+ #
43
+ # If you are doing OTP releases, you need to instruct Phoenix
44
+ # to start each relevant endpoint:
45
+ #
46
+ # config :my_app, MyAppWeb.Endpoint, server: true
47
+ #
48
+ # Then you can assemble a release by calling `mix release`.
49
+ # See `mix help release` for more information.
50
+
51
+ # ## Configuring the mailer
52
+ #
53
+ # In production you need to configure the mailer to use a different adapter.
54
+ # Also, you may need to configure the Swoosh API client of your choice if you
55
+ # are not using SMTP. Here is an example of the configuration:
56
+ #
57
+ # config :my_app, MyApp.Mailer,
58
+ # adapter: Swoosh.Adapters.Mailgun,
59
+ # api_key: System.get_env("MAILGUN_API_KEY"),
60
+ # domain: System.get_env("MAILGUN_DOMAIN")
61
+ #
62
+ # For this example you need include a HTTP client required by Swoosh API client.
63
+ # Swoosh supports Hackney and Finch out of the box:
64
+ #
65
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
66
+ #
67
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
68
+ end
changed config/test.exs
 
@@ -1,4 +1,4 @@
1
- use Mix.Config
1
+ import Config
2
2
3
3
# Configure your database
4
4
#
 
@@ -10,13 +10,20 @@ config :my_app, MyApp.Repo,
10
10
password: "postgres",
11
11
database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}",
12
12
hostname: "localhost",
13
- pool: Ecto.Adapters.SQL.Sandbox
13
+ pool: Ecto.Adapters.SQL.Sandbox,
14
+ pool_size: 10
14
15
15
16
# We don't run a server during test. If one is required,
16
17
# you can enable the server option below.
17
18
config :my_app, MyAppWeb.Endpoint,
18
- http: [port: 4002],
19
+ http: [ip: {127, 0, 0, 1}, port: 4002],
19
20
server: false
20
21
22
+ # In test we don't send emails.
23
+ config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Test
24
+
21
25
# Print only warnings and errors during test
22
26
config :logger, level: :warn
27
+
28
+ # Initialize plugs at runtime for faster test compilation
29
+ config :phoenix, :plug_init_mode, :runtime
changed lib/my_app/application.ex
 
@@ -5,6 +5,7 @@ defmodule MyApp.Application do
5
5
6
6
use Application
7
7
8
+ @impl true
8
9
def start(_type, _args) do
9
10
children = [
10
11
# Start the Ecto repository
 
@@ -27,6 +28,7 @@ defmodule MyApp.Application do
27
28
28
29
# Tell Phoenix to update the endpoint configuration
29
30
# whenever the application is updated.
31
+ @impl true
30
32
def config_change(changed, _new, removed) do
31
33
MyAppWeb.Endpoint.config_change(changed, removed)
32
34
:ok
added lib/my_app/mailer.ex
 
@@ -0,0 +1,3 @@
1
+ defmodule MyApp.Mailer do
2
+ use Swoosh.Mailer, otp_app: :my_app
3
+ end
removed lib/my_app_web/channels/user_socket.ex
 
@@ -1,35 +0,0 @@
1
- defmodule MyAppWeb.UserSocket do
2
- use Phoenix.Socket
3
-
4
- ## Channels
5
- # channel "room:*", MyAppWeb.RoomChannel
6
-
7
- # Socket params are passed from the client and can
8
- # be used to verify and authenticate a user. After
9
- # verification, you can put default assigns into
10
- # the socket that will be set for all channels, ie
11
- #
12
- # {:ok, assign(socket, :user_id, verified_user_id)}
13
- #
14
- # To deny connection, return `:error`.
15
- #
16
- # See `Phoenix.Token` documentation for examples in
17
- # performing token verification on connect.
18
- @impl true
19
- def connect(_params, socket, _connect_info) do
20
- {:ok, socket}
21
- end
22
-
23
- # Socket id's are topics that allow you to identify all sockets for a given user:
24
- #
25
- # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
26
- #
27
- # Would allow you to broadcast a "disconnect" event and terminate
28
- # all active sockets and channels for a given user:
29
- #
30
- # MyAppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
31
- #
32
- # Returning `nil` makes this socket anonymous.
33
- @impl true
34
- def id(_socket), do: nil
35
- end
changed lib/my_app_web/endpoint.ex
 
@@ -10,10 +10,6 @@ defmodule MyAppWeb.Endpoint do
10
10
signing_salt: "foo"
11
11
]
12
12
13
- socket "/socket", MyAppWeb.UserSocket,
14
- websocket: true,
15
- longpoll: false
16
-
17
13
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
18
14
19
15
# Serve at "/" the static files from "priv/static" directory.
 
@@ -24,7 +20,7 @@ defmodule MyAppWeb.Endpoint do
24
20
at: "/",
25
21
from: :my_app,
26
22
gzip: false,
27
- only: ~w(css fonts images js favicon.ico robots.txt)
23
+ only: ~w(assets fonts images favicon.ico robots.txt)
28
24
29
25
# Code reloading can be explicitly enabled under the
30
26
# :code_reloader configuration of your endpoint.
changed lib/my_app_web/router.ex
 
@@ -4,7 +4,8 @@ defmodule MyAppWeb.Router do
4
4
pipeline :browser do
5
5
plug :accepts, ["html"]
6
6
plug :fetch_session
7
- plug :fetch_flash
7
+ plug :fetch_live_flash
8
+ plug :put_root_layout, {MyAppWeb.LayoutView, :root}
8
9
plug :protect_from_forgery
9
10
plug :put_secure_browser_headers
10
11
end
 
@@ -39,4 +40,16 @@ defmodule MyAppWeb.Router do
39
40
live_dashboard "/dashboard", metrics: MyAppWeb.Telemetry
40
41
end
41
42
end
43
+
44
+ # Enables the Swoosh mailbox preview in development.
45
+ #
46
+ # Note that preview only shows emails that were sent by the same
47
+ # node running the Phoenix server.
48
+ if Mix.env() == :dev do
49
+ scope "/dev" do
50
+ pipe_through :browser
51
+
52
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
53
+ end
54
+ end
42
55
end
changed lib/my_app_web/telemetry.ex
 
@@ -31,11 +31,27 @@ defmodule MyAppWeb.Telemetry do
31
31
),
32
32
33
33
# Database Metrics
34
- summary("my_app.repo.query.total_time", unit: {:native, :millisecond}),
35
- summary("my_app.repo.query.decode_time", unit: {:native, :millisecond}),
36
- summary("my_app.repo.query.query_time", unit: {:native, :millisecond}),
37
- summary("my_app.repo.query.queue_time", unit: {:native, :millisecond}),
38
- summary("my_app.repo.query.idle_time", unit: {:native, :millisecond}),
34
+ summary("my_app.repo.query.total_time",
35
+ unit: {:native, :millisecond},
36
+ description: "The sum of the other measurements"
37
+ ),
38
+ summary("my_app.repo.query.decode_time",
39
+ unit: {:native, :millisecond},
40
+ description: "The time spent decoding the data received from the database"
41
+ ),
42
+ summary("my_app.repo.query.query_time",
43
+ unit: {:native, :millisecond},
44
+ description: "The time spent executing the query"
45
+ ),
46
+ summary("my_app.repo.query.queue_time",
47
+ unit: {:native, :millisecond},
48
+ description: "The time spent waiting for a database connection"
49
+ ),
50
+ summary("my_app.repo.query.idle_time",
51
+ unit: {:native, :millisecond},
52
+ description:
53
+ "The time the connection spent waiting before being checked out for the query"
54
+ ),
39
55
40
56
# VM Metrics
41
57
summary("vm.memory.total", unit: {:byte, :kilobyte}),
added lib/my_app_web/templates/layout/app.html.heex
 
@@ -0,0 +1,5 @@
1
+ <main class="container">
2
+ <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
3
+ <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
4
+ <%= @inner_content %>
5
+ </main>
added lib/my_app_web/templates/layout/live.html.heex
 
@@ -0,0 +1,11 @@
1
+ <main class="container">
2
+ <p class="alert alert-info" role="alert"
3
+ phx-click="lv:clear-flash"
4
+ phx-value-key="info"><%= live_flash(@flash, :info) %></p>
5
+
6
+ <p class="alert alert-danger" role="alert"
7
+ phx-click="lv:clear-flash"
8
+ phx-value-key="error"><%= live_flash(@flash, :error) %></p>
9
+
10
+ <%= @inner_content %>
11
+ </main>
renamed lib/my_app_web/templates/layout/app.html.eex -> lib/my_app_web/templates/layout/root.html.heex
 
@@ -4,14 +4,15 @@
4
4
<meta charset="utf-8"/>
5
5
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
6
6
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
- <title>MyApp · Phoenix Framework</title>
8
- <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
9
- <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
7
+ <%= csrf_meta_tag() %>
8
+ <%= live_title_tag assigns[:page_title] || "MyApp", suffix: " · Phoenix Framework" %>
9
+ <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
10
+ <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
10
11
</head>
11
12
<body>
12
13
<header>
13
14
<section class="container">
14
- <nav role="navigation">
15
+ <nav>
15
16
<ul>
16
17
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
17
18
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
 
@@ -20,14 +21,10 @@
20
21
</ul>
21
22
</nav>
22
23
<a href="https://phoenixframework.org/" class="phx-logo">
23
- <img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
24
+ <img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
24
25
</a>
25
26
</section>
26
27
</header>
27
- <main role="main" class="container">
28
- <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
29
- <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
30
- <%= @inner_content %>
31
- </main>
28
+ <%= @inner_content %>
32
29
</body>
33
30
</html>
renamed lib/my_app_web/templates/page/index.html.eex -> lib/my_app_web/templates/page/index.html.heex
 
@@ -14,7 +14,7 @@
14
14
<a href="https://github.com/phoenixframework/phoenix">Source</a>
15
15
</li>
16
16
<li>
17
- <a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
17
+ <a href="https://github.com/phoenixframework/phoenix/blob/v1.6/CHANGELOG.md">v1.6 Changelog</a>
18
18
</li>
19
19
</ul>
20
20
</article>
 
@@ -25,7 +25,7 @@
25
25
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
26
26
</li>
27
27
<li>
28
- <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
28
+ <a href="https://web.libera.chat/#elixir">#elixir on Libera Chat (IRC)</a>
29
29
</li>
30
30
<li>
31
31
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
 
@@ -33,6 +33,9 @@
33
33
<li>
34
34
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
35
35
</li>
36
+ <li>
37
+ <a href="https://discord.gg/elixir">Elixir on Discord</a>
38
+ </li>
36
39
</ul>
37
40
</article>
38
41
</section>
changed lib/my_app_web/views/layout_view.ex
 
@@ -1,3 +1,7 @@
1
1
defmodule MyAppWeb.LayoutView do
2
2
use MyAppWeb, :view
3
+
4
+ # Phoenix LiveDashboard is available only in development by default,
5
+ # so we instruct Elixir to not warn if the dashboard route is missing.
6
+ @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
3
7
end
changed lib/my_app_web.ex
 
@@ -42,12 +42,30 @@ defmodule MyAppWeb do
42
42
end
43
43
end
44
44
45
+ def live_view do
46
+ quote do
47
+ use Phoenix.LiveView,
48
+ layout: {MyAppWeb.LayoutView, "live.html"}
49
+
50
+ unquote(view_helpers())
51
+ end
52
+ end
53
+
54
+ def live_component do
55
+ quote do
56
+ use Phoenix.LiveComponent
57
+
58
+ unquote(view_helpers())
59
+ end
60
+ end
61
+
45
62
def router do
46
63
quote do
47
64
use Phoenix.Router
48
65
49
66
import Plug.Conn
50
67
import Phoenix.Controller
68
+ import Phoenix.LiveView.Router
51
69
end
52
70
end
53
71
 
@@ -63,6 +81,9 @@ defmodule MyAppWeb do
63
81
# Use all HTML functionality (forms, tags, etc)
64
82
use Phoenix.HTML
65
83
84
+ # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
85
+ import Phoenix.LiveView.Helpers
86
+
66
87
# Import basic rendering functionality (render, render_layout, etc)
67
88
import Phoenix.View
68
89
changed mix.exs
 
@@ -5,9 +5,9 @@ defmodule MyApp.MixProject do
5
5
[
6
6
app: :my_app,
7
7
version: "0.1.0",
8
- elixir: "~> 1.7",
8
+ elixir: "~> 1.12",
9
9
elixirc_paths: elixirc_paths(Mix.env()),
10
- compilers: [:phoenix, :gettext] ++ Mix.compilers(),
10
+ compilers: [:gettext] ++ Mix.compilers(),
11
11
start_permanent: Mix.env() == :prod,
12
12
aliases: aliases(),
13
13
deps: deps()
 
@@ -33,18 +33,22 @@ defmodule MyApp.MixProject do
33
33
# Type `mix help deps` for examples and options.
34
34
defp deps do
35
35
[
36
- {:phoenix, "~> 1.5.12"},
36
+ {:phoenix, "~> 1.6.0-rc.0", override: true},
37
37
{:phoenix_ecto, "~> 4.4"},
38
- {:ecto_sql, "~> 3.4"},
38
+ {:ecto_sql, "~> 3.6"},
39
39
{:postgrex, ">= 0.0.0"},
40
- {:phoenix_html, "~> 2.11"},
40
+ {:phoenix_html, "~> 3.0"},
41
41
{:phoenix_live_reload, "~> 1.2", only: :dev},
42
- {:phoenix_live_dashboard, "~> 0.4"},
43
- {:telemetry_metrics, "~> 0.4"},
44
- {:telemetry_poller, "~> 0.4"},
45
- {:gettext, "~> 0.11"},
46
- {:jason, "~> 1.0"},
47
- {:plug_cowboy, "~> 2.0"}
42
+ {:phoenix_live_view, "~> 0.16.0"},
43
+ {:floki, ">= 0.30.0", only: :test},
44
+ {:phoenix_live_dashboard, "~> 0.5"},
45
+ {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
46
+ {:swoosh, "~> 1.3"},
47
+ {:telemetry_metrics, "~> 0.6"},
48
+ {:telemetry_poller, "~> 1.0"},
49
+ {:gettext, "~> 0.18"},
50
+ {:jason, "~> 1.2"},
51
+ {:plug_cowboy, "~> 2.5"}
48
52
]
49
53
end
50
54
 
@@ -56,10 +60,11 @@ defmodule MyApp.MixProject do
56
60
# See the documentation for `Mix` for more info on aliases.
57
61
defp aliases do
58
62
[
59
- setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
63
+ setup: ["deps.get", "ecto.setup"],
60
64
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
61
65
"ecto.reset": ["ecto.drop", "ecto.setup"],
62
- test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
66
+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
67
+ "assets.deploy": ["esbuild default --minify", "phx.digest"]
63
68
]
64
69
end
65
70
end
renamed assets/static/favicon.ico -> priv/static/favicon.ico
renamed assets/static/images/phoenix.png -> priv/static/images/phoenix.png
renamed assets/static/robots.txt -> priv/static/robots.txt
changed test/support/channel_case.ex
 
@@ -29,12 +29,8 @@ defmodule MyAppWeb.ChannelCase do
29
29
end
30
30
31
31
setup tags do
32
- :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
33
-
34
- unless tags[:async] do
35
- Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
36
- end
37
-
32
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
33
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
38
34
:ok
39
35
end
40
36
end
changed test/support/conn_case.ex
 
@@ -32,12 +32,8 @@ defmodule MyAppWeb.ConnCase do
32
32
end
33
33
34
34
setup tags do
35
- :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
36
-
37
- unless tags[:async] do
38
- Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
39
- end
40
-
35
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
36
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
41
37
{:ok, conn: Phoenix.ConnTest.build_conn()}
42
38
end
43
39
end
changed test/support/data_case.ex
 
@@ -28,12 +28,8 @@ defmodule MyApp.DataCase do
28
28
end
29
29
30
30
setup tags do
31
- :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
32
-
33
- unless tags[:async] do
34
- Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
35
- end
36
-
31
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
32
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
37
33
:ok
38
34
end
39
35