Twitter GitHub
changed .formatter.exs
 
@@ -1,5 +1,6 @@
1
1
[
2
- import_deps: [:ecto, :phoenix],
3
- inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4
- subdirectories: ["priv/*/migrations"]
2
+ import_deps: [:ecto, :ecto_sql, :phoenix],
3
+ subdirectories: ["priv/*/migrations"],
4
+ plugins: [Phoenix.LiveView.HTMLFormatter],
5
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
5
6
]
changed .gitignore
 
@@ -19,6 +19,9 @@ erl_crash.dump
19
19
# Also ignore archive artifacts (built via "mix archive.build").
20
20
*.ez
21
21
22
+ # Temporary files, for example, from tests.
23
+ /tmp/
24
+
22
25
# Ignore package tarball (built via "mix hex.build").
23
26
my_app-*.tar
24
27
changed README.md
 
@@ -2,8 +2,7 @@
2
2
3
3
To start your Phoenix server:
4
4
5
- * Install dependencies with `mix deps.get`
6
- * Create and migrate your database with `mix ecto.setup`
5
+ * Run `mix setup` to install and setup dependencies
7
6
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
8
7
9
8
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
changed assets/css/app.css
 
@@ -1,120 +1,5 @@
1
+ @import "tailwindcss/base";
2
+ @import "tailwindcss/components";
3
+ @import "tailwindcss/utilities";
4
+
1
5
/* This file is for your main application CSS */
2
- @import "./phoenix.css";
3
-
4
- /* Alerts and form errors used by phx.new */
5
- .alert {
6
- padding: 15px;
7
- margin-bottom: 20px;
8
- border: 1px solid transparent;
9
- border-radius: 4px;
10
- }
11
- .alert-info {
12
- color: #31708f;
13
- background-color: #d9edf7;
14
- border-color: #bce8f1;
15
- }
16
- .alert-warning {
17
- color: #8a6d3b;
18
- background-color: #fcf8e3;
19
- border-color: #faebcc;
20
- }
21
- .alert-danger {
22
- color: #a94442;
23
- background-color: #f2dede;
24
- border-color: #ebccd1;
25
- }
26
- .alert p {
27
- margin-bottom: 0;
28
- }
29
- .alert:empty {
30
- display: none;
31
- }
32
- .invalid-feedback {
33
- color: #a94442;
34
- display: block;
35
- margin: -1rem 0 2rem;
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-loading{
50
- cursor: wait;
51
- }
52
-
53
- .phx-modal {
54
- opacity: 1!important;
55
- position: fixed;
56
- z-index: 1;
57
- left: 0;
58
- top: 0;
59
- width: 100%;
60
- height: 100%;
61
- overflow: auto;
62
- background-color: rgba(0,0,0,0.4);
63
- }
64
-
65
- .phx-modal-content {
66
- background-color: #fefefe;
67
- margin: 15vh auto;
68
- padding: 20px;
69
- border: 1px solid #888;
70
- width: 80%;
71
- }
72
-
73
- .phx-modal-close {
74
- color: #aaa;
75
- float: right;
76
- font-size: 28px;
77
- font-weight: bold;
78
- }
79
-
80
- .phx-modal-close:hover,
81
- .phx-modal-close:focus {
82
- color: black;
83
- text-decoration: none;
84
- cursor: pointer;
85
- }
86
-
87
- .fade-in-scale {
88
- animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
89
- }
90
-
91
- .fade-out-scale {
92
- animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
93
- }
94
-
95
- .fade-in {
96
- animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
97
- }
98
- .fade-out {
99
- animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
100
- }
101
-
102
- @keyframes fade-in-scale-keys{
103
- 0% { scale: 0.95; opacity: 0; }
104
- 100% { scale: 1.0; opacity: 1; }
105
- }
106
-
107
- @keyframes fade-out-scale-keys{
108
- 0% { scale: 1.0; opacity: 1; }
109
- 100% { scale: 0.95; opacity: 0; }
110
- }
111
-
112
- @keyframes fade-in-keys{
113
- 0% { opacity: 0; }
114
- 100% { opacity: 1; }
115
- }
116
-
117
- @keyframes fade-out-keys{
118
- 0% { opacity: 1; }
119
- 100% { opacity: 0; }
120
- }
removed assets/css/phoenix.css
 
@@ -1,101 +0,0 @@
1
- /* Includes some default style for the starter application.
2
- * This can be safely deleted to start fresh.
3
- */
4
-
5
- /* Milligram v1.4.1 https://milligram.github.io
6
- * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license
7
- */
8
-
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
-
11
- /* General style */
12
- h1{font-size: 3.6rem; line-height: 1.25}
13
- h2{font-size: 2.8rem; line-height: 1.3}
14
- h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35}
15
- h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5}
16
- h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4}
17
- h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2}
18
- pre{padding: 1em;}
19
-
20
- .container{
21
- margin: 0 auto;
22
- max-width: 80.0rem;
23
- padding: 0 2.0rem;
24
- position: relative;
25
- width: 100%
26
- }
27
- select {
28
- width: auto;
29
- }
30
-
31
- /* Phoenix promo and logo */
32
- .phx-hero {
33
- text-align: center;
34
- border-bottom: 1px solid #e3e3e3;
35
- background: #eee;
36
- border-radius: 6px;
37
- padding: 3em 3em 1em;
38
- margin-bottom: 3rem;
39
- font-weight: 200;
40
- font-size: 120%;
41
- }
42
- .phx-hero input {
43
- background: #ffffff;
44
- }
45
- .phx-logo {
46
- min-width: 300px;
47
- margin: 1rem;
48
- display: block;
49
- }
50
- .phx-logo img {
51
- width: auto;
52
- display: block;
53
- }
54
-
55
- /* Headers */
56
- header {
57
- width: 100%;
58
- background: #fdfdfd;
59
- border-bottom: 1px solid #eaeaea;
60
- margin-bottom: 2rem;
61
- }
62
- header section {
63
- align-items: center;
64
- display: flex;
65
- flex-direction: column;
66
- justify-content: space-between;
67
- }
68
- header section :first-child {
69
- order: 2;
70
- }
71
- header section :last-child {
72
- order: 1;
73
- }
74
- header nav ul,
75
- header nav li {
76
- margin: 0;
77
- padding: 0;
78
- display: block;
79
- text-align: right;
80
- white-space: nowrap;
81
- }
82
- header nav ul {
83
- margin: 1rem;
84
- margin-top: 0;
85
- }
86
- header nav a {
87
- display: block;
88
- }
89
-
90
- @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */
91
- header section {
92
- flex-direction: row;
93
- }
94
- header nav ul {
95
- margin: 1rem;
96
- }
97
- .phx-logo {
98
- flex-basis: 527px;
99
- margin: 2rem 1rem;
100
- }
101
- }
changed assets/js/app.js
 
@@ -1,7 +1,3 @@
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).
3
- import "../css/app.css"
4
-
5
1
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
6
2
// to get started and then uncomment the line below.
7
3
// import "./user_socket.js"
 
@@ -31,8 +27,8 @@ let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToke
31
27
32
28
// Show progress bar on live navigation and form submits
33
29
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())
30
+ window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
31
+ window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
36
32
37
33
// connect if there are any LiveViews on the page
38
34
liveSocket.connect()
added assets/tailwind.config.js
 
@@ -0,0 +1,26 @@
1
+ // See the Tailwind configuration guide for advanced usage
2
+ // https://tailwindcss.com/docs/configuration
3
+
4
+ const plugin = require("tailwindcss/plugin")
5
+
6
+ module.exports = {
7
+ content: [
8
+ "./js/**/*.js",
9
+ "../lib/*_web.ex",
10
+ "../lib/*_web/**/*.*ex"
11
+ ],
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ brand: "#FD4F00",
16
+ }
17
+ },
18
+ },
19
+ plugins: [
20
+ require("@tailwindcss/forms"),
21
+ plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
22
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
23
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
24
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
25
+ ]
26
+ }
changed assets/vendor/topbar.js
 
@@ -1,6 +1,6 @@
1
1
/**
2
2
* @license MIT
3
- * topbar 1.0.0, 2021-01-06
3
+ * topbar 2.0.0, 2023-02-04
4
4
* https://buunguyen.github.io/topbar
5
5
* Copyright (c) 2021 Buu Nguyen
6
6
*/
 
@@ -35,10 +35,11 @@
35
35
})();
36
36
37
37
var canvas,
38
- progressTimerId,
39
- fadeTimerId,
40
38
currentProgress,
41
39
showing,
40
+ progressTimerId = null,
41
+ fadeTimerId = null,
42
+ delayTimerId = null,
42
43
addEvent = function (elem, type, handler) {
43
44
if (elem.addEventListener) elem.addEventListener(type, handler, false);
44
45
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
 
@@ -95,21 +96,26 @@
95
96
for (var key in opts)
96
97
if (options.hasOwnProperty(key)) options[key] = opts[key];
97
98
},
98
- show: function () {
99
+ show: function (delay) {
99
100
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
- })();
101
+ if (delay) {
102
+ if (delayTimerId) return;
103
+ delayTimerId = setTimeout(() => topbar.show(), delay);
104
+ } else {
105
+ showing = true;
106
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
107
+ if (!canvas) createCanvas();
108
+ canvas.style.opacity = 1;
109
+ canvas.style.display = "block";
110
+ topbar.progress(0);
111
+ if (options.autoRun) {
112
+ (function loop() {
113
+ progressTimerId = window.requestAnimationFrame(loop);
114
+ topbar.progress(
115
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
116
+ );
117
+ })();
118
+ }
113
119
}
114
120
},
115
121
progress: function (to) {
 
@@ -125,6 +131,8 @@
125
131
return currentProgress;
126
132
},
127
133
hide: function () {
134
+ clearTimeout(delayTimerId);
135
+ delayTimerId = null;
128
136
if (!showing) return;
129
137
showing = false;
130
138
if (progressTimerId != null) {
changed config/config.exs
 
@@ -13,7 +13,10 @@ config :my_app,
13
13
# Configures the endpoint
14
14
config :my_app, MyAppWeb.Endpoint,
15
15
url: [host: "localhost"],
16
- render_errors: [view: MyAppWeb.ErrorView, accepts: ~w(html json), layout: false],
16
+ render_errors: [
17
+ formats: [html: MyAppWeb.ErrorHTML, json: MyAppWeb.ErrorJSON],
18
+ layout: false
19
+ ],
17
20
pubsub_server: MyApp.PubSub,
18
21
live_view: [signing_salt: "foo"]
19
22
 
@@ -26,12 +29,9 @@ config :my_app, MyAppWeb.Endpoint,
26
29
# at the `config/runtime.exs`.
27
30
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Local
28
31
29
- # Swoosh API client is needed for adapters other than SMTP.
30
- config :swoosh, :api_client, false
31
-
32
32
# Configure esbuild (the version is required)
33
33
config :esbuild,
34
- version: "0.14.29",
34
+ version: "0.14.41",
35
35
default: [
36
36
args:
37
37
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
 
@@ -39,6 +39,18 @@ config :esbuild,
39
39
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
40
40
]
41
41
42
+ # Configure tailwind (the version is required)
43
+ config :tailwind,
44
+ version: "3.2.4",
45
+ default: [
46
+ args: ~w(
47
+ --config=tailwind.config.js
48
+ --input=css/app.css
49
+ --output=../priv/static/assets/app.css
50
+ ),
51
+ cd: Path.expand("../assets", __DIR__)
52
+ ]
53
+
42
54
# Configures Elixir's Logger
43
55
config :logger, :console,
44
56
format: "$time $metadata[$level] $message\n",
changed config/dev.exs
 
@@ -25,8 +25,8 @@ config :my_app, MyAppWeb.Endpoint,
25
25
debug_errors: true,
26
26
secret_key_base: "foo",
27
27
watchers: [
28
- # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
29
- esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
28
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
29
+ tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
30
30
]
31
31
32
32
# ## SSL Support
 
@@ -37,7 +37,6 @@ config :my_app, MyAppWeb.Endpoint,
37
37
#
38
38
# mix phx.gen.cert
39
39
#
40
- # Note that this task requires Erlang/OTP 20 or later.
41
40
# Run `mix help phx.gen.cert` for more information.
42
41
#
43
42
# The `http:` config above can be replaced with:
 
@@ -59,11 +58,13 @@ config :my_app, MyAppWeb.Endpoint,
59
58
patterns: [
60
59
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
61
60
~r"priv/gettext/.*(po)$",
62
- ~r"lib/my_app_web/(live|views)/.*(ex)$",
63
- ~r"lib/my_app_web/templates/.*(eex)$"
61
+ ~r"lib/my_app_web/(controllers|live|components)/.*(ex|heex)$"
64
62
]
65
63
]
66
64
65
+ # Enable dev routes for dashboard and mailbox
66
+ config :my_app, dev_routes: true
67
+
67
68
# Do not include metadata nor timestamps in development logs
68
69
config :logger, :console, format: "[$level] $message\n"
69
70
 
@@ -73,3 +74,6 @@ config :phoenix, :stacktrace_depth, 20
73
74
74
75
# Initialize plugs at runtime for faster development compilation
75
76
config :phoenix, :plug_init_mode, :runtime
77
+
78
+ # Disable swoosh api client as it is only required for production adapters.
79
+ config :swoosh, :api_client, false
changed config/prod.exs
 
@@ -3,7 +3,7 @@ import Config
3
3
# For production, don't forget to configure the url host
4
4
# to something meaningful, Phoenix uses this information
5
5
# when generating URLs.
6
- #
6
+
7
7
# Note we also include the path to a cache manifest
8
8
# containing the digested version of static files. This
9
9
# manifest is generated by the `mix phx.digest` task,
 
@@ -11,39 +11,11 @@ import Config
11
11
# before starting your production server.
12
12
config :my_app, MyAppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
13
13
14
+ # Configures Swoosh API Client
15
+ config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: MyApp.Finch
16
+
14
17
# Do not print debug messages in production
15
18
config :logger, level: :info
16
19
17
- # ## SSL Support
18
- #
19
- # To get SSL working, you will need to add the `https` key
20
- # to the previous section and set your `:url` port to 443:
21
- #
22
- # config :my_app, MyAppWeb.Endpoint,
23
- # ...,
24
- # url: [host: "example.com", port: 443],
25
- # https: [
26
- # ...,
27
- # port: 443,
28
- # cipher_suite: :strong,
29
- # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
30
- # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
31
- # ]
32
- #
33
- # The `cipher_suite` is set to `:strong` to support only the
34
- # latest and more secure SSL ciphers. This means old browsers
35
- # and clients may not be supported. You can set it to
36
- # `:compatible` for wider support.
37
- #
38
- # `:keyfile` and `:certfile` expect an absolute path to the key
39
- # and cert in disk or a relative path inside priv, for example
40
- # "priv/ssl/server.key". For all supported SSL configuration
41
- # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
42
- #
43
- # We also recommend setting `force_ssl` in your endpoint, ensuring
44
- # no data is ever sent via http, always redirecting to https:
45
- #
46
- # config :my_app, MyAppWeb.Endpoint,
47
- # force_ssl: [hsts: true]
48
- #
49
- # Check `Plug.SSL` for all available options in `force_ssl`.
20
+ # Runtime production configuration, including reading
21
+ # of environment variables, is done on config/runtime.exs.
changed config/runtime.exs
 
@@ -28,7 +28,7 @@ if config_env() == :prod do
28
28
For example: ecto://USER:PASS@HOST/DATABASE
29
29
"""
30
30
31
- maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
31
+ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
32
32
33
33
config :my_app, MyApp.Repo,
34
34
# ssl: true,
 
@@ -63,6 +63,38 @@ if config_env() == :prod do
63
63
],
64
64
secret_key_base: secret_key_base
65
65
66
+ # ## SSL Support
67
+ #
68
+ # To get SSL working, you will need to add the `https` key
69
+ # to your endpoint configuration:
70
+ #
71
+ # config :my_app, MyAppWeb.Endpoint,
72
+ # https: [
73
+ # ...,
74
+ # port: 443,
75
+ # cipher_suite: :strong,
76
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
77
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
78
+ # ]
79
+ #
80
+ # The `cipher_suite` is set to `:strong` to support only the
81
+ # latest and more secure SSL ciphers. This means old browsers
82
+ # and clients may not be supported. You can set it to
83
+ # `:compatible` for wider support.
84
+ #
85
+ # `:keyfile` and `:certfile` expect an absolute path to the key
86
+ # and cert in disk or a relative path inside priv, for example
87
+ # "priv/ssl/server.key". For all supported SSL configuration
88
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
89
+ #
90
+ # We also recommend setting `force_ssl` in your endpoint, ensuring
91
+ # no data is ever sent via http, always redirecting to https:
92
+ #
93
+ # config :my_app, MyAppWeb.Endpoint,
94
+ # force_ssl: [hsts: true]
95
+ #
96
+ # Check `Plug.SSL` for all available options in `force_ssl`.
97
+
66
98
# ## Configuring the mailer
67
99
#
68
100
# In production you need to configure the mailer to use a different adapter.
changed config/test.exs
 
@@ -23,8 +23,11 @@ config :my_app, MyAppWeb.Endpoint,
23
23
# In test we don't send emails.
24
24
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Test
25
25
26
+ # Disable swoosh api client as it is only required for production adapters.
27
+ config :swoosh, :api_client, false
28
+
26
29
# Print only warnings and errors during test
27
- config :logger, level: :warn
30
+ config :logger, level: :warning
28
31
29
32
# Initialize plugs at runtime for faster test compilation
30
33
config :phoenix, :plug_init_mode, :runtime
changed lib/my_app/application.ex
 
@@ -8,12 +8,14 @@ defmodule MyApp.Application do
8
8
@impl true
9
9
def start(_type, _args) do
10
10
children = [
11
- # Start the Ecto repository
12
- MyApp.Repo,
13
11
# Start the Telemetry supervisor
14
12
MyAppWeb.Telemetry,
13
+ # Start the Ecto repository
14
+ MyApp.Repo,
15
15
# Start the PubSub system
16
16
{Phoenix.PubSub, name: MyApp.PubSub},
17
+ # Start Finch
18
+ {Finch, name: MyApp.Finch},
17
19
# Start the Endpoint (http/https)
18
20
MyAppWeb.Endpoint
19
21
# Start a worker by calling: MyApp.Worker.start_link(arg)
added lib/my_app_web/components/core_components.ex
 
@@ -0,0 +1,661 @@
1
+ defmodule MyAppWeb.CoreComponents do
2
+ @moduledoc """
3
+ Provides core UI components.
4
+
5
+ The components in this module use Tailwind CSS, a utility-first CSS framework.
6
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
7
+ customize the generated components in this module.
8
+
9
+ Icons are provided by [heroicons](https://heroicons.com), using the
10
+ [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
11
+ """
12
+ use Phoenix.Component
13
+
14
+ alias Phoenix.LiveView.JS
15
+ import MyAppWeb.Gettext
16
+
17
+ @doc """
18
+ Renders a modal.
19
+
20
+ ## Examples
21
+
22
+ <.modal id="confirm-modal">
23
+ Are you sure?
24
+ <:confirm>OK</:confirm>
25
+ <:cancel>Cancel</:cancel>
26
+ </.modal>
27
+
28
+ JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
29
+ for the caller to react to each button press, for example:
30
+
31
+ <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
32
+ Are you sure you?
33
+ <:confirm>OK</:confirm>
34
+ <:cancel>Cancel</:cancel>
35
+ </.modal>
36
+ """
37
+ attr :id, :string, required: true
38
+ attr :show, :boolean, default: false
39
+ attr :on_cancel, JS, default: %JS{}
40
+ attr :on_confirm, JS, default: %JS{}
41
+
42
+ slot :inner_block, required: true
43
+ slot :title
44
+ slot :subtitle
45
+ slot :confirm
46
+ slot :cancel
47
+
48
+ def modal(assigns) do
49
+ ~H"""
50
+ <div
51
+ id={@id}
52
+ phx-mounted={@show && show_modal(@id)}
53
+ phx-remove={hide_modal(@id)}
54
+ class="relative z-50 hidden"
55
+ >
56
+ <div id={"#{@id}-bg"} class="fixed inset-0 bg-zinc-50/90 transition-opacity" aria-hidden="true" />
57
+ <div
58
+ class="fixed inset-0 overflow-y-auto"
59
+ aria-labelledby={"#{@id}-title"}
60
+ aria-describedby={"#{@id}-description"}
61
+ role="dialog"
62
+ aria-modal="true"
63
+ tabindex="0"
64
+ >
65
+ <div class="flex min-h-full items-center justify-center">
66
+ <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
67
+ <.focus_wrap
68
+ id={"#{@id}-container"}
69
+ phx-mounted={@show && show_modal(@id)}
70
+ phx-window-keydown={hide_modal(@on_cancel, @id)}
71
+ phx-key="escape"
72
+ phx-click-away={hide_modal(@on_cancel, @id)}
73
+ class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition"
74
+ >
75
+ <div class="absolute top-6 right-5">
76
+ <button
77
+ phx-click={hide_modal(@on_cancel, @id)}
78
+ type="button"
79
+ class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
80
+ aria-label={gettext("close")}
81
+ >
82
+ <Heroicons.x_mark solid class="h-5 w-5 stroke-current" />
83
+ </button>
84
+ </div>
85
+ <div id={"#{@id}-content"}>
86
+ <header :if={@title != []}>
87
+ <h1 id={"#{@id}-title"} class="text-lg font-semibold leading-8 text-zinc-800">
88
+ <%= render_slot(@title) %>
89
+ </h1>
90
+ <p
91
+ :if={@subtitle != []}
92
+ id={"#{@id}-description"}
93
+ class="mt-2 text-sm leading-6 text-zinc-600"
94
+ >
95
+ <%= render_slot(@subtitle) %>
96
+ </p>
97
+ </header>
98
+ <%= render_slot(@inner_block) %>
99
+ <div :if={@confirm != [] or @cancel != []} class="ml-6 mb-4 flex items-center gap-5">
100
+ <.button
101
+ :for={confirm <- @confirm}
102
+ id={"#{@id}-confirm"}
103
+ phx-click={@on_confirm}
104
+ phx-disable-with
105
+ class="py-2 px-3"
106
+ >
107
+ <%= render_slot(confirm) %>
108
+ </.button>
109
+ <.link
110
+ :for={cancel <- @cancel}
111
+ phx-click={hide_modal(@on_cancel, @id)}
112
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
113
+ >
114
+ <%= render_slot(cancel) %>
115
+ </.link>
116
+ </div>
117
+ </div>
118
+ </.focus_wrap>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ """
124
+ end
125
+
126
+ @doc """
127
+ Renders flash notices.
128
+
129
+ ## Examples
130
+
131
+ <.flash kind={:info} flash={@flash} />
132
+ <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
133
+ """
134
+ attr :id, :string, default: "flash", doc: "the optional id of flash container"
135
+ attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
136
+ attr :title, :string, default: nil
137
+ attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
138
+ attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
139
+ attr :close, :boolean, default: true, doc: "whether the flash can be closed"
140
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
141
+
142
+ slot :inner_block, doc: "the optional inner block that renders the flash message"
143
+
144
+ def flash(assigns) do
145
+ ~H"""
146
+ <div
147
+ :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
148
+ id={@id}
149
+ phx-mounted={@autoshow && show("##{@id}")}
150
+ phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
151
+ role="alert"
152
+ class={[
153
+ "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
154
+ @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
155
+ @kind == :error && "bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
156
+ ]}
157
+ {@rest}
158
+ >
159
+ <p :if={@title} class="flex items-center gap-1.5 text-[0.8125rem] font-semibold leading-6">
160
+ <Heroicons.information_circle :if={@kind == :info} mini class="h-4 w-4" />
161
+ <Heroicons.exclamation_circle :if={@kind == :error} mini class="h-4 w-4" />
162
+ <%= @title %>
163
+ </p>
164
+ <p class="mt-2 text-[0.8125rem] leading-5"><%= msg %></p>
165
+ <button
166
+ :if={@close}
167
+ type="button"
168
+ class="group absolute top-2 right-1 p-2"
169
+ aria-label={gettext("close")}
170
+ >
171
+ <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
172
+ </button>
173
+ </div>
174
+ """
175
+ end
176
+
177
+ @doc """
178
+ Shows the flash group with standard titles and content.
179
+
180
+ ## Examples
181
+
182
+ <.flash_group flash={@flash} />
183
+ """
184
+ attr :flash, :map, required: true, doc: "the map of flash messages"
185
+
186
+ def flash_group(assigns) do
187
+ ~H"""
188
+ <.flash kind={:info} title="Success!" flash={@flash} />
189
+ <.flash kind={:error} title="Error!" flash={@flash} />
190
+ <.flash
191
+ id="disconnected"
192
+ kind={:error}
193
+ title="We can't find the internet"
194
+ close={false}
195
+ autoshow={false}
196
+ phx-disconnected={show("#disconnected")}
197
+ phx-connected={hide("#disconnected")}
198
+ >
199
+ Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
200
+ </.flash>
201
+ """
202
+ end
203
+
204
+ @doc """
205
+ Renders a simple form.
206
+
207
+ ## Examples
208
+
209
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
210
+ <.input field={@form[:email]} label="Email"/>
211
+ <.input field={@form[:username]} label="Username" />
212
+ <:actions>
213
+ <.button>Save</.button>
214
+ </:actions>
215
+ </.simple_form>
216
+ """
217
+ attr :for, :any, required: true, doc: "the datastructure for the form"
218
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
219
+
220
+ attr :rest, :global,
221
+ include: ~w(autocomplete name rel action enctype method novalidate target),
222
+ doc: "the arbitrary HTML attributes to apply to the form tag"
223
+
224
+ slot :inner_block, required: true
225
+ slot :actions, doc: "the slot for form actions, such as a submit button"
226
+
227
+ def simple_form(assigns) do
228
+ ~H"""
229
+ <.form :let={f} for={@for} as={@as} {@rest}>
230
+ <div class="space-y-8 bg-white mt-10">
231
+ <%= render_slot(@inner_block, f) %>
232
+ <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
233
+ <%= render_slot(action, f) %>
234
+ </div>
235
+ </div>
236
+ </.form>
237
+ """
238
+ end
239
+
240
+ @doc """
241
+ Renders a button.
242
+
243
+ ## Examples
244
+
245
+ <.button>Send!</.button>
246
+ <.button phx-click="go" class="ml-2">Send!</.button>
247
+ """
248
+ attr :type, :string, default: nil
249
+ attr :class, :string, default: nil
250
+ attr :rest, :global, include: ~w(disabled form name value)
251
+
252
+ slot :inner_block, required: true
253
+
254
+ def button(assigns) do
255
+ ~H"""
256
+ <button
257
+ type={@type}
258
+ class={[
259
+ "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
260
+ "text-sm font-semibold leading-6 text-white active:text-white/80",
261
+ @class
262
+ ]}
263
+ {@rest}
264
+ >
265
+ <%= render_slot(@inner_block) %>
266
+ </button>
267
+ """
268
+ end
269
+
270
+ @doc """
271
+ Renders an input with label and error messages.
272
+
273
+ A `%Phoenix.HTML.Form{}` and field name may be passed to the input
274
+ to build input names and error messages, or all the attributes and
275
+ errors may be passed explicitly.
276
+
277
+ ## Examples
278
+
279
+ <.input field={@form[:email]} type="email" />
280
+ <.input name="my-input" errors={["oh no!"]} />
281
+ """
282
+ attr :id, :any, default: nil
283
+ attr :name, :any
284
+ attr :label, :string, default: nil
285
+ attr :value, :any
286
+
287
+ attr :type, :string,
288
+ default: "text",
289
+ values: ~w(checkbox color date datetime-local email file hidden month number password
290
+ range radio search select tel text textarea time url week)
291
+
292
+ attr :field, Phoenix.HTML.FormField,
293
+ doc: "a form field struct retrieved from the form, for example: @form[:email]"
294
+
295
+ attr :errors, :list, default: []
296
+ attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
297
+ attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
298
+ attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
299
+ attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
300
+ attr :rest, :global, include: ~w(autocomplete cols disabled form max maxlength min minlength
301
+ pattern placeholder readonly required rows size step)
302
+ slot :inner_block
303
+
304
+ def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
305
+ assigns
306
+ |> assign(field: nil, id: assigns.id || field.id)
307
+ |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
308
+ |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
309
+ |> assign_new(:value, fn -> field.value end)
310
+ |> input()
311
+ end
312
+
313
+ def input(%{type: "checkbox", value: value} = assigns) do
314
+ assigns =
315
+ assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
316
+
317
+ ~H"""
318
+ <div phx-feedback-for={@name}>
319
+ <label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
320
+ <input type="hidden" name={@name} value="false" />
321
+ <input
322
+ type="checkbox"
323
+ id={@id || @name}
324
+ name={@name}
325
+ value="true"
326
+ checked={@checked}
327
+ class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
328
+ {@rest}
329
+ />
330
+ <%= @label %>
331
+ </label>
332
+ <.error :for={msg <- @errors}><%= msg %></.error>
333
+ </div>
334
+ """
335
+ end
336
+
337
+ def input(%{type: "select"} = assigns) do
338
+ ~H"""
339
+ <div phx-feedback-for={@name}>
340
+ <.label for={@id}><%= @label %></.label>
341
+ <select
342
+ id={@id}
343
+ name={@name}
344
+ class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-zinc-500 focus:border-zinc-500 sm:text-sm"
345
+ multiple={@multiple}
346
+ {@rest}
347
+ >
348
+ <option :if={@prompt} value=""><%= @prompt %></option>
349
+ <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
350
+ </select>
351
+ <.error :for={msg <- @errors}><%= msg %></.error>
352
+ </div>
353
+ """
354
+ end
355
+
356
+ def input(%{type: "textarea"} = assigns) do
357
+ ~H"""
358
+ <div phx-feedback-for={@name}>
359
+ <.label for={@id}><%= @label %></.label>
360
+ <textarea
361
+ id={@id || @name}
362
+ name={@name}
363
+ class={[
364
+ "mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
365
+ "text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6",
366
+ "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
367
+ "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
368
+ @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
369
+ ]}
370
+ {@rest}
371
+ ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
372
+ <.error :for={msg <- @errors}><%= msg %></.error>
373
+ </div>
374
+ """
375
+ end
376
+
377
+ def input(assigns) do
378
+ ~H"""
379
+ <div phx-feedback-for={@name}>
380
+ <.label for={@id}><%= @label %></.label>
381
+ <input
382
+ type={@type}
383
+ name={@name}
384
+ id={@id || @name}
385
+ value={Phoenix.HTML.Form.normalize_value(@type, @value)}
386
+ class={[
387
+ "mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
388
+ "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
389
+ "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
390
+ "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
391
+ @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
392
+ ]}
393
+ {@rest}
394
+ />
395
+ <.error :for={msg <- @errors}><%= msg %></.error>
396
+ </div>
397
+ """
398
+ end
399
+
400
+ @doc """
401
+ Renders a label.
402
+ """
403
+ attr :for, :string, default: nil
404
+ slot :inner_block, required: true
405
+
406
+ def label(assigns) do
407
+ ~H"""
408
+ <label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
409
+ <%= render_slot(@inner_block) %>
410
+ </label>
411
+ """
412
+ end
413
+
414
+ @doc """
415
+ Generates a generic error message.
416
+ """
417
+ slot :inner_block, required: true
418
+
419
+ def error(assigns) do
420
+ ~H"""
421
+ <p class="phx-no-feedback:hidden mt-3 flex gap-3 text-sm leading-6 text-rose-600">
422
+ <Heroicons.exclamation_circle mini class="mt-0.5 h-5 w-5 flex-none fill-rose-500" />
423
+ <%= render_slot(@inner_block) %>
424
+ </p>
425
+ """
426
+ end
427
+
428
+ @doc """
429
+ Renders a header with title.
430
+ """
431
+ attr :class, :string, default: nil
432
+
433
+ slot :inner_block, required: true
434
+ slot :subtitle
435
+ slot :actions
436
+
437
+ def header(assigns) do
438
+ ~H"""
439
+ <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
440
+ <div>
441
+ <h1 class="text-lg font-semibold leading-8 text-zinc-800">
442
+ <%= render_slot(@inner_block) %>
443
+ </h1>
444
+ <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
445
+ <%= render_slot(@subtitle) %>
446
+ </p>
447
+ </div>
448
+ <div class="flex-none"><%= render_slot(@actions) %></div>
449
+ </header>
450
+ """
451
+ end
452
+
453
+ @doc ~S"""
454
+ Renders a table with generic styling.
455
+
456
+ ## Examples
457
+
458
+ <.table id="users" rows={@users}>
459
+ <:col :let={user} label="id"><%= user.id %></:col>
460
+ <:col :let={user} label="username"><%= user.username %></:col>
461
+ </.table>
462
+ """
463
+ attr :id, :string, required: true
464
+ attr :rows, :list, required: true
465
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
466
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
467
+
468
+ attr :row_item, :any,
469
+ default: &Function.identity/1,
470
+ doc: "the function for mapping each row before calling the :col and :action slots"
471
+
472
+ slot :col, required: true do
473
+ attr :label, :string
474
+ end
475
+
476
+ slot :action, doc: "the slot for showing user actions in the last table column"
477
+
478
+ def table(assigns) do
479
+ assigns =
480
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
481
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
482
+ end
483
+
484
+ ~H"""
485
+ <div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
486
+ <table class="mt-11 w-[40rem] sm:w-full">
487
+ <thead class="text-left text-[0.8125rem] leading-6 text-zinc-500">
488
+ <tr>
489
+ <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
490
+ <th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
491
+ </tr>
492
+ </thead>
493
+ <tbody
494
+ id={@id}
495
+ phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
496
+ class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
497
+ >
498
+ <tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
499
+ <td
500
+ :for={{col, i} <- Enum.with_index(@col)}
501
+ phx-click={@row_click && @row_click.(row)}
502
+ class={["relative p-0", @row_click && "hover:cursor-pointer"]}
503
+ >
504
+ <div class="block py-4 pr-6">
505
+ <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
506
+ <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
507
+ <%= render_slot(col, @row_item.(row)) %>
508
+ </span>
509
+ </div>
510
+ </td>
511
+ <td :if={@action != []} class="relative p-0 w-14">
512
+ <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
513
+ <span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
514
+ <span
515
+ :for={action <- @action}
516
+ class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
517
+ >
518
+ <%= render_slot(action, @row_item.(row)) %>
519
+ </span>
520
+ </div>
521
+ </td>
522
+ </tr>
523
+ </tbody>
524
+ </table>
525
+ </div>
526
+ """
527
+ end
528
+
529
+ @doc """
530
+ Renders a data list.
531
+
532
+ ## Examples
533
+
534
+ <.list>
535
+ <:item title="Title"><%= @post.title %></:item>
536
+ <:item title="Views"><%= @post.views %></:item>
537
+ </.list>
538
+ """
539
+ slot :item, required: true do
540
+ attr :title, :string, required: true
541
+ end
542
+
543
+ def list(assigns) do
544
+ ~H"""
545
+ <div class="mt-14">
546
+ <dl class="-my-4 divide-y divide-zinc-100">
547
+ <div :for={item <- @item} class="flex gap-4 py-4 sm:gap-8">
548
+ <dt class="w-1/4 flex-none text-[0.8125rem] leading-6 text-zinc-500"><%= item.title %></dt>
549
+ <dd class="text-sm leading-6 text-zinc-700"><%= render_slot(item) %></dd>
550
+ </div>
551
+ </dl>
552
+ </div>
553
+ """
554
+ end
555
+
556
+ @doc """
557
+ Renders a back navigation link.
558
+
559
+ ## Examples
560
+
561
+ <.back navigate={~p"/posts"}>Back to posts</.back>
562
+ """
563
+ attr :navigate, :any, required: true
564
+ slot :inner_block, required: true
565
+
566
+ def back(assigns) do
567
+ ~H"""
568
+ <div class="mt-16">
569
+ <.link
570
+ navigate={@navigate}
571
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
572
+ >
573
+ <Heroicons.arrow_left solid class="w-3 h-3 stroke-current inline" />
574
+ <%= render_slot(@inner_block) %>
575
+ </.link>
576
+ </div>
577
+ """
578
+ end
579
+
580
+ ## JS Commands
581
+
582
+ def show(js \\ %JS{}, selector) do
583
+ JS.show(js,
584
+ to: selector,
585
+ transition:
586
+ {"transition-all transform ease-out duration-300",
587
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
588
+ "opacity-100 translate-y-0 sm:scale-100"}
589
+ )
590
+ end
591
+
592
+ def hide(js \\ %JS{}, selector) do
593
+ JS.hide(js,
594
+ to: selector,
595
+ time: 200,
596
+ transition:
597
+ {"transition-all transform ease-in duration-200",
598
+ "opacity-100 translate-y-0 sm:scale-100",
599
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
600
+ )
601
+ end
602
+
603
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
604
+ js
605
+ |> JS.show(to: "##{id}")
606
+ |> JS.show(
607
+ to: "##{id}-bg",
608
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
609
+ )
610
+ |> show("##{id}-container")
611
+ |> JS.add_class("overflow-hidden", to: "body")
612
+ |> JS.focus_first(to: "##{id}-content")
613
+ end
614
+
615
+ def hide_modal(js \\ %JS{}, id) do
616
+ js
617
+ |> JS.hide(
618
+ to: "##{id}-bg",
619
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
620
+ )
621
+ |> hide("##{id}-container")
622
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
623
+ |> JS.remove_class("overflow-hidden", to: "body")
624
+ |> JS.pop_focus()
625
+ end
626
+
627
+ @doc """
628
+ Translates an error message using gettext.
629
+ """
630
+ def translate_error({msg, opts}) do
631
+ # When using gettext, we typically pass the strings we want
632
+ # to translate as a static argument:
633
+ #
634
+ # # Translate "is invalid" in the "errors" domain
635
+ # dgettext("errors", "is invalid")
636
+ #
637
+ # # Translate the number of files with plural rules
638
+ # dngettext("errors", "1 file", "%{count} files", count)
639
+ #
640
+ # Because the error messages we show in our forms and APIs
641
+ # are defined inside Ecto, we need to translate them dynamically.
642
+ # This requires us to call the Gettext module passing our gettext
643
+ # backend as first argument.
644
+ #
645
+ # Note we use the "errors" domain, which means translations
646
+ # should be written to the errors.po file. The :count option is
647
+ # set by Ecto and indicates we should also apply plural rules.
648
+ if count = opts[:count] do
649
+ Gettext.dngettext(MyAppWeb.Gettext, "errors", msg, msg, count, opts)
650
+ else
651
+ Gettext.dgettext(MyAppWeb.Gettext, "errors", msg, opts)
652
+ end
653
+ end
654
+
655
+ @doc """
656
+ Translates the errors for a field from a keyword list of errors.
657
+ """
658
+ def translate_errors(errors, field) when is_list(errors) do
659
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
660
+ end
661
+ end
added lib/my_app_web/components/layouts/app.html.heex
 
@@ -0,0 +1,43 @@
1
+ <header class="px-4 sm:px-6 lg:px-8">
2
+ <div class="flex items-center justify-between border-b border-zinc-100 py-3">
3
+ <div class="flex items-center gap-4">
4
+ <a href="/">
5
+ <svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
6
+ <path
7
+ d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
8
+ fill="#FD4F00"
9
+ />
10
+ </svg>
11
+ </a>
12
+ <p class="rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
13
+ v1.7
14
+ </p>
15
+ </div>
16
+ <div class="flex items-center gap-4">
17
+ <a
18
+ href="https://twitter.com/elixirphoenix"
19
+ class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
20
+ >
21
+ @elixirphoenix
22
+ </a>
23
+ <a
24
+ href="https://github.com/phoenixframework/phoenix"
25
+ class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
26
+ >
27
+ GitHub
28
+ </a>
29
+ <a
30
+ href="https://hexdocs.pm/phoenix/overview.html"
31
+ class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
32
+ >
33
+ Get Started <span aria-hidden="true">&rarr;</span>
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </header>
38
+ <main class="px-4 py-20 sm:px-6 lg:px-8">
39
+ <div class="mx-auto max-w-2xl">
40
+ <.flash_group flash={@flash} />
41
+ <%= @inner_content %>
42
+ </div>
43
+ </main>
added lib/my_app_web/components/layouts/root.html.heex
 
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" style="scrollbar-gutter: stable;">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="csrf-token" content={get_csrf_token()} />
7
+ <.live_title suffix=" · Phoenix Framework">
8
+ <%= assigns[:page_title] || "MyApp" %>
9
+ </.live_title>
10
+ <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
11
+ <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
12
+ </script>
13
+ </head>
14
+ <body class="bg-white antialiased">
15
+ <%= @inner_content %>
16
+ </body>
17
+ </html>
added lib/my_app_web/components/layouts.ex
 
@@ -0,0 +1,5 @@
1
+ defmodule MyAppWeb.Layouts do
2
+ use MyAppWeb, :html
3
+
4
+ embed_templates "layouts/*"
5
+ end
added lib/my_app_web/controllers/error_html.ex
 
@@ -0,0 +1,19 @@
1
+ defmodule MyAppWeb.ErrorHTML do
2
+ use MyAppWeb, :html
3
+
4
+ # If you want to customize your error pages,
5
+ # uncomment the embed_templates/1 call below
6
+ # and add pages to the error directory:
7
+ #
8
+ # * lib/my_app_web/controllers/error_html/404.html.heex
9
+ # * lib/my_app_web/controllers/error_html/500.html.heex
10
+ #
11
+ # embed_templates "error_html/*"
12
+
13
+ # The default is to render a plain text page based on
14
+ # the template name. For example, "404.html" becomes
15
+ # "Not Found".
16
+ def render(template, _assigns) do
17
+ Phoenix.Controller.status_message_from_template(template)
18
+ end
19
+ end
added lib/my_app_web/controllers/error_json.ex
 
@@ -0,0 +1,15 @@
1
+ defmodule MyAppWeb.ErrorJSON do
2
+ # If you want to customize a particular status code,
3
+ # you may add your own clauses, such as:
4
+ #
5
+ # def render("500.json", _assigns) do
6
+ # %{errors: %{detail: "Internal Server Error"}}
7
+ # end
8
+
9
+ # By default, Phoenix returns the status message from
10
+ # the template name. For example, "404.json" becomes
11
+ # "Not Found".
12
+ def render(template, _assigns) do
13
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
14
+ end
15
+ end
changed lib/my_app_web/controllers/page_controller.ex
 
@@ -1,7 +1,9 @@
1
1
defmodule MyAppWeb.PageController do
2
2
use MyAppWeb, :controller
3
3
4
- def index(conn, _params) do
5
- render(conn, "index.html")
4
+ def home(conn, _params) do
5
+ # The home page is often custom made,
6
+ # so skip the default app layout.
7
+ render(conn, :home, layout: false)
6
8
end
7
9
end
added lib/my_app_web/controllers/page_html/home.html.heex
 
@@ -0,0 +1,237 @@
1
+ <.flash_group flash={@flash} />
2
+ <div class="fixed inset-y-0 right-0 left-[40rem] hidden lg:block xl:left-[50rem] z-0">
3
+ <svg
4
+ viewBox="0 0 1480 957"
5
+ fill="none"
6
+ aria-hidden="true"
7
+ class="absolute inset-0 h-full w-full"
8
+ preserveAspectRatio="xMinYMid slice"
9
+ >
10
+ <path fill="#EE7868" d="M0 0h1480v957H0z" />
11
+ <path
12
+ d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
13
+ fill="#FF9F92"
14
+ />
15
+ <path
16
+ d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
17
+ fill="#FA8372"
18
+ />
19
+ <path
20
+ d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
21
+ fill="#E96856"
22
+ fill-opacity=".6"
23
+ />
24
+ <path
25
+ d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
26
+ fill="#C42652"
27
+ fill-opacity=".2"
28
+ />
29
+ <path
30
+ d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
31
+ fill="#A41C42"
32
+ fill-opacity=".2"
33
+ />
34
+ <path
35
+ d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
36
+ fill="#A41C42"
37
+ fill-opacity=".2"
38
+ />
39
+ </svg>
40
+ </div>
41
+ <div class="px-4 py-10 sm:py-28 sm:px-6 lg:px-8 xl:py-32 xl:px-28">
42
+ <div class="mx-auto max-w-xl lg:mx-0">
43
+ <svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
44
+ <path
45
+ d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
46
+ fill="#FD4F00"
47
+ />
48
+ </svg>
49
+ <h1 class="mt-10 flex items-center text-sm font-semibold leading-6 text-brand">
50
+ Phoenix Framework
51
+ <small class="ml-3 rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6">
52
+ v1.7
53
+ </small>
54
+ </h1>
55
+ <p class="mt-4 text-[2rem] font-semibold leading-10 tracking-tighter text-zinc-900">
56
+ Peace of mind from prototype to production.
57
+ </p>
58
+ <p class="mt-4 text-base leading-7 text-zinc-600">
59
+ 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.
60
+ </p>
61
+ <div class="flex">
62
+ <div class="w-full sm:w-auto">
63
+ <div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
64
+ <a
65
+ href="https://hexdocs.pm/phoenix/overview.html"
66
+ class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
67
+ >
68
+ <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
69
+ </span>
70
+ <span class="relative flex items-center gap-4 sm:flex-col">
71
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
72
+ <path d="m12 4 10-2v18l-10 2V4Z" fill="#18181B" fill-opacity=".15" />
73
+ <path
74
+ d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
75
+ stroke="#18181B"
76
+ stroke-width="2"
77
+ stroke-linecap="round"
78
+ stroke-linejoin="round"
79
+ />
80
+ </svg>
81
+ Guides &amp; Docs
82
+ </span>
83
+ </a>
84
+ <a
85
+ href="https://github.com/phoenixframework/phoenix"
86
+ class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
87
+ >
88
+ <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
89
+ </span>
90
+ <span class="relative flex items-center gap-4 sm:flex-col">
91
+ <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
92
+ <path
93
+ fill-rule="evenodd"
94
+ clip-rule="evenodd"
95
+ d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
96
+ fill="#18181B"
97
+ />
98
+ </svg>
99
+ Source Code
100
+ </span>
101
+ </a>
102
+ <a
103
+ href="https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md"
104
+ class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
105
+ >
106
+ <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
107
+ </span>
108
+ <span class="relative flex items-center gap-4 sm:flex-col">
109
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
110
+ <path
111
+ d="M12 1v6M12 17v6"
112
+ stroke="#18181B"
113
+ stroke-width="2"
114
+ stroke-linecap="round"
115
+ stroke-linejoin="round"
116
+ />
117
+ <circle
118
+ cx="12"
119
+ cy="12"
120
+ r="4"
121
+ fill="#18181B"
122
+ fill-opacity=".15"
123
+ stroke="#18181B"
124
+ stroke-width="2"
125
+ stroke-linecap="round"
126
+ stroke-linejoin="round"
127
+ />
128
+ </svg>
129
+ Changelog
130
+ </span>
131
+ </a>
132
+ </div>
133
+ <div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-zinc-700 sm:grid-cols-2">
134
+ <div>
135
+ <a
136
+ href="https://twitter.com/elixirphoenix"
137
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
138
+ >
139
+ <svg
140
+ viewBox="0 0 16 16"
141
+ aria-hidden="true"
142
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
143
+ >
144
+ <path d="M5.403 14c5.283 0 8.172-4.617 8.172-8.62 0-.131 0-.262-.008-.391A6.033 6.033 0 0 0 15 3.419a5.503 5.503 0 0 1-1.65.477 3.018 3.018 0 0 0 1.263-1.676 5.579 5.579 0 0 1-1.824.736 2.832 2.832 0 0 0-1.63-.916 2.746 2.746 0 0 0-1.821.319A2.973 2.973 0 0 0 8.076 3.78a3.185 3.185 0 0 0-.182 1.938 7.826 7.826 0 0 1-3.279-.918 8.253 8.253 0 0 1-2.64-2.247 3.176 3.176 0 0 0-.315 2.208 3.037 3.037 0 0 0 1.203 1.836A2.739 2.739 0 0 1 1.56 6.22v.038c0 .7.23 1.377.65 1.919.42.54 1.004.912 1.654 1.05-.423.122-.866.14-1.297.052.184.602.541 1.129 1.022 1.506a2.78 2.78 0 0 0 1.662.598 5.656 5.656 0 0 1-2.007 1.074A5.475 5.475 0 0 1 1 12.64a7.827 7.827 0 0 0 4.403 1.358" />
145
+ </svg>
146
+ Follow on Twitter
147
+ </a>
148
+ </div>
149
+ <div>
150
+ <a
151
+ href="https://elixirforum.com"
152
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
153
+ >
154
+ <svg
155
+ viewBox="0 0 16 16"
156
+ aria-hidden="true"
157
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
158
+ >
159
+ <path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
160
+ </svg>
161
+ Discuss on the Elixir forum
162
+ </a>
163
+ </div>
164
+ <div>
165
+ <a
166
+ href="https://elixir-slackin.herokuapp.com"
167
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
168
+ >
169
+ <svg
170
+ viewBox="0 0 16 16"
171
+ aria-hidden="true"
172
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
173
+ >
174
+ <path d="M3.95 9.85a1.47 1.47 0 1 1-2.94 0 1.47 1.47 0 0 1 1.47-1.472h1.47v1.471Zm.735 0a1.47 1.47 0 1 1 2.94 0v3.678a1.47 1.47 0 1 1-2.94 0V9.85ZM6.156 3.942a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 1 1 2.94 0v1.472h-1.47Zm0 .747c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H2.47A1.47 1.47 0 0 1 1 6.16 1.47 1.47 0 0 1 2.47 4.69h3.686ZM12.048 6.16a1.47 1.47 0 1 1 2.94 0 1.47 1.47 0 0 1-1.47 1.472h-1.47V6.16Zm-.735 0a1.47 1.47 0 1 1-2.94 0V2.47a1.47 1.47 0 1 1 2.94 0v3.69ZM9.843 12.057c.813 0 1.47.657 1.47 1.471a1.47 1.47 0 1 1-2.94 0v-1.471h1.47Zm0-.736a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 0 1 1.47-1.471h3.686c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H9.843Z" />
175
+ </svg>
176
+ Join our Slack channel
177
+ </a>
178
+ </div>
179
+ <div>
180
+ <a
181
+ href="https://web.libera.chat/#elixir"
182
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
183
+ >
184
+ <svg
185
+ viewBox="0 0 16 16"
186
+ aria-hidden="true"
187
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
188
+ >
189
+ <path
190
+ fill-rule="evenodd"
191
+ clip-rule="evenodd"
192
+ d="M6.356 2.007a.75.75 0 0 1 .637.849l-1.5 10.5a.75.75 0 1 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.637ZM11.356 2.008a.75.75 0 0 1 .637.848l-1.5 10.5a.75.75 0 0 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.636Z"
193
+ />
194
+ <path
195
+ fill-rule="evenodd"
196
+ clip-rule="evenodd"
197
+ d="M14 5.25a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75ZM13 10.75a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75Z"
198
+ />
199
+ </svg>
200
+ Chat on Libera IRC
201
+ </a>
202
+ </div>
203
+ <div>
204
+ <a
205
+ href="https://discord.gg/elixir"
206
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
207
+ >
208
+ <svg
209
+ viewBox="0 0 16 16"
210
+ aria-hidden="true"
211
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
212
+ >
213
+ <path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
214
+ </svg>
215
+ Join our Discord server
216
+ </a>
217
+ </div>
218
+ <div>
219
+ <a
220
+ href="https://fly.io/docs/elixir/getting-started/"
221
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
222
+ >
223
+ <svg
224
+ viewBox="0 0 20 20"
225
+ aria-hidden="true"
226
+ class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
227
+ >
228
+ <path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
229
+ </svg>
230
+ Deploy your application
231
+ </a>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
added lib/my_app_web/controllers/page_html.ex
 
@@ -0,0 +1,5 @@
1
+ defmodule MyAppWeb.PageHTML do
2
+ use MyAppWeb, :html
3
+
4
+ embed_templates "page_html/*"
5
+ end
changed lib/my_app_web/endpoint.ex
 
@@ -7,7 +7,8 @@ defmodule MyAppWeb.Endpoint do
7
7
@session_options [
8
8
store: :cookie,
9
9
key: "_my_app_key",
10
- signing_salt: "A2WGho/5"
10
+ signing_salt: "6dP515pU",
11
+ same_site: "Lax"
11
12
]
12
13
13
14
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
 
@@ -20,7 +21,7 @@ defmodule MyAppWeb.Endpoint do
20
21
at: "/",
21
22
from: :my_app,
22
23
gzip: false,
23
- only: ~w(assets fonts images favicon.ico robots.txt)
24
+ only: MyAppWeb.static_paths()
24
25
25
26
# Code reloading can be explicitly enabled under the
26
27
# :code_reloader configuration of your endpoint.
changed lib/my_app_web/router.ex
 
@@ -5,7 +5,7 @@ defmodule MyAppWeb.Router do
5
5
plug :accepts, ["html"]
6
6
plug :fetch_session
7
7
plug :fetch_live_flash
8
- plug :put_root_layout, {MyAppWeb.LayoutView, :root}
8
+ plug :put_root_layout, {MyAppWeb.Layouts, :root}
9
9
plug :protect_from_forgery
10
10
plug :put_secure_browser_headers
11
11
end
 
@@ -17,7 +17,7 @@ defmodule MyAppWeb.Router do
17
17
scope "/", MyAppWeb do
18
18
pipe_through :browser
19
19
20
- get "/", PageController, :index
20
+ get "/", PageController, :home
21
21
end
22
22
23
23
# Other scopes may use custom stacks.
 
@@ -25,31 +25,19 @@ defmodule MyAppWeb.Router do
25
25
# pipe_through :api
26
26
# end
27
27
28
- # Enables LiveDashboard only for development
29
- #
30
- # If you want to use the LiveDashboard in production, you should put
31
- # it behind authentication and allow only admins to access it.
32
- # If your application does not have an admins-only section yet,
33
- # you can use Plug.BasicAuth to set up some basic authentication
34
- # as long as you are also using SSL (which you should anyway).
35
- if Mix.env() in [:dev, :test] do
28
+ # Enable LiveDashboard and Swoosh mailbox preview in development
29
+ if Application.compile_env(:my_app, :dev_routes) do
30
+ # If you want to use the LiveDashboard in production, you should put
31
+ # it behind authentication and allow only admins to access it.
32
+ # If your application does not have an admins-only section yet,
33
+ # you can use Plug.BasicAuth to set up some basic authentication
34
+ # as long as you are also using SSL (which you should anyway).
36
35
import Phoenix.LiveDashboard.Router
37
36
38
- scope "/" do
39
- pipe_through :browser
40
-
41
- live_dashboard "/dashboard", metrics: MyAppWeb.Telemetry
42
- end
43
- end
44
-
45
- # Enables the Swoosh mailbox preview in development.
46
- #
47
- # Note that preview only shows emails that were sent by the same
48
- # node running the Phoenix server.
49
- if Mix.env() == :dev do
50
37
scope "/dev" do
51
38
pipe_through :browser
52
39
40
+ live_dashboard "/dashboard", metrics: MyAppWeb.Telemetry
53
41
forward "/mailbox", Plug.Swoosh.MailboxPreview
54
42
end
55
43
end
changed lib/my_app_web/telemetry.ex
 
@@ -22,13 +22,34 @@ defmodule MyAppWeb.Telemetry do
22
22
def metrics do
23
23
[
24
24
# Phoenix Metrics
25
+ summary("phoenix.endpoint.start.system_time",
26
+ unit: {:native, :millisecond}
27
+ ),
25
28
summary("phoenix.endpoint.stop.duration",
26
29
unit: {:native, :millisecond}
27
30
),
31
+ summary("phoenix.router_dispatch.start.system_time",
32
+ tags: [:route],
33
+ unit: {:native, :millisecond}
34
+ ),
35
+ summary("phoenix.router_dispatch.exception.duration",
36
+ tags: [:route],
37
+ unit: {:native, :millisecond}
38
+ ),
28
39
summary("phoenix.router_dispatch.stop.duration",
29
40
tags: [:route],
30
41
unit: {:native, :millisecond}
31
42
),
43
+ summary("phoenix.socket_connected.duration",
44
+ unit: {:native, :millisecond}
45
+ ),
46
+ summary("phoenix.channel_join.duration",
47
+ unit: {:native, :millisecond}
48
+ ),
49
+ summary("phoenix.channel_handled_in.duration",
50
+ tags: [:event],
51
+ unit: {:native, :millisecond}
52
+ ),
32
53
33
54
# Database Metrics
34
55
summary("my_app.repo.query.total_time",
removed lib/my_app_web/templates/layout/app.html.heex
 
@@ -1,5 +0,0 @@
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>
removed lib/my_app_web/templates/layout/live.html.heex
 
@@ -1,11 +0,0 @@
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>
removed lib/my_app_web/templates/layout/root.html.heex
 
@@ -1,30 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8"/>
5
- <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
- <meta name="csrf-token" content={csrf_token_value()}>
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>
11
- </head>
12
- <body>
13
- <header>
14
- <section class="container">
15
- <nav>
16
- <ul>
17
- <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
18
- <%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
19
- <li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
20
- <% end %>
21
- </ul>
22
- </nav>
23
- <a href="https://phoenixframework.org/" class="phx-logo">
24
- <img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
25
- </a>
26
- </section>
27
- </header>
28
- <%= @inner_content %>
29
- </body>
30
- </html>
removed lib/my_app_web/templates/page/index.html.heex
 
@@ -1,41 +0,0 @@
1
- <section class="phx-hero">
2
- <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
3
- <p>Peace of mind from prototype to production</p>
4
- </section>
5
-
6
- <section class="row">
7
- <article class="column">
8
- <h2>Resources</h2>
9
- <ul>
10
- <li>
11
- <a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
12
- </li>
13
- <li>
14
- <a href="https://github.com/phoenixframework/phoenix">Source</a>
15
- </li>
16
- <li>
17
- <a href="https://github.com/phoenixframework/phoenix/blob/v1.6/CHANGELOG.md">v1.6 Changelog</a>
18
- </li>
19
- </ul>
20
- </article>
21
- <article class="column">
22
- <h2>Help</h2>
23
- <ul>
24
- <li>
25
- <a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
26
- </li>
27
- <li>
28
- <a href="https://web.libera.chat/#elixir">#elixir on Libera Chat (IRC)</a>
29
- </li>
30
- <li>
31
- <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
32
- </li>
33
- <li>
34
- <a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
35
- </li>
36
- <li>
37
- <a href="https://discord.gg/elixir">Elixir on Discord</a>
38
- </li>
39
- </ul>
40
- </article>
41
- </section>
removed lib/my_app_web/views/error_helpers.ex
 
@@ -1,47 +0,0 @@
1
- defmodule MyAppWeb.ErrorHelpers do
2
- @moduledoc """
3
- Conveniences for translating and building error messages.
4
- """
5
-
6
- use Phoenix.HTML
7
-
8
- @doc """
9
- Generates tag for inlined form input errors.
10
- """
11
- def error_tag(form, field) do
12
- Enum.map(Keyword.get_values(form.errors, field), fn error ->
13
- content_tag(:span, translate_error(error),
14
- class: "invalid-feedback",
15
- phx_feedback_for: input_name(form, field)
16
- )
17
- end)
18
- end
19
-
20
- @doc """
21
- Translates an error message using gettext.
22
- """
23
- def translate_error({msg, opts}) do
24
- # When using gettext, we typically pass the strings we want
25
- # to translate as a static argument:
26
- #
27
- # # Translate "is invalid" in the "errors" domain
28
- # dgettext("errors", "is invalid")
29
- #
30
- # # Translate the number of files with plural rules
31
- # dngettext("errors", "1 file", "%{count} files", count)
32
- #
33
- # Because the error messages we show in our forms and APIs
34
- # are defined inside Ecto, we need to translate them dynamically.
35
- # This requires us to call the Gettext module passing our gettext
36
- # backend as first argument.
37
- #
38
- # Note we use the "errors" domain, which means translations
39
- # should be written to the errors.po file. The :count option is
40
- # set by Ecto and indicates we should also apply plural rules.
41
- if count = opts[:count] do
42
- Gettext.dngettext(MyAppWeb.Gettext, "errors", msg, msg, count, opts)
43
- else
44
- Gettext.dgettext(MyAppWeb.Gettext, "errors", msg, opts)
45
- end
46
- end
47
- end
removed lib/my_app_web/views/error_view.ex
 
@@ -1,16 +0,0 @@
1
- defmodule MyAppWeb.ErrorView do
2
- use MyAppWeb, :view
3
-
4
- # If you want to customize a particular status code
5
- # for a certain format, you may uncomment below.
6
- # def render("500.html", _assigns) do
7
- # "Internal Server Error"
8
- # end
9
-
10
- # By default, Phoenix returns the status message from
11
- # the template name. For example, "404.html" becomes
12
- # "Not Found".
13
- def template_not_found(template, _assigns) do
14
- Phoenix.Controller.status_message_from_template(template)
15
- end
16
- end
removed lib/my_app_web/views/layout_view.ex
 
@@ -1,7 +0,0 @@
1
- defmodule MyAppWeb.LayoutView do
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}}
7
- end
removed lib/my_app_web/views/page_view.ex
 
@@ -1,3 +0,0 @@
1
- defmodule MyAppWeb.PageView do
2
- use MyAppWeb, :view
3
- end
changed lib/my_app_web.ex
 
@@ -1,76 +1,29 @@
1
1
defmodule MyAppWeb do
2
2
@moduledoc """
3
3
The entrypoint for defining your web interface, such
4
- as controllers, views, channels and so on.
4
+ as controllers, components, channels, and so on.
5
5
6
6
This can be used in your application as:
7
7
8
8
use MyAppWeb, :controller
9
- use MyAppWeb, :view
9
+ use MyAppWeb, :html
10
10
11
- The definitions below will be executed for every view,
12
- controller, etc, so keep them short and clean, focused
11
+ The definitions below will be executed for every controller,
12
+ component, etc, so keep them short and clean, focused
13
13
on imports, uses and aliases.
14
14
15
15
Do NOT define functions inside the quoted expressions
16
- below. Instead, define any helper function in modules
17
- and import those modules here.
16
+ below. Instead, define additional modules and import
17
+ those modules here.
18
18
"""
19
19
20
- def controller do
21
- quote do
22
- use Phoenix.Controller, namespace: MyAppWeb
23
-
24
- import Plug.Conn
25
- import MyAppWeb.Gettext
26
- alias MyAppWeb.Router.Helpers, as: Routes
27
- end
28
- end
29
-
30
- def view do
31
- quote do
32
- use Phoenix.View,
33
- root: "lib/my_app_web/templates",
34
- namespace: MyAppWeb
35
-
36
- # Import convenience functions from controllers
37
- import Phoenix.Controller,
38
- only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
39
-
40
- # Include shared imports and aliases for views
41
- unquote(view_helpers())
42
- end
43
- end
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
-
62
- def component do
63
- quote do
64
- use Phoenix.Component
65
-
66
- unquote(view_helpers())
67
- end
68
- end
20
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
69
21
70
22
def router do
71
23
quote do
72
- use Phoenix.Router
24
+ use Phoenix.Router, helpers: false
73
25
26
+ # Import common connection and controller functions to use in pipelines
74
27
import Plug.Conn
75
28
import Phoenix.Controller
76
29
import Phoenix.LiveView.Router
 
@@ -80,24 +33,74 @@ defmodule MyAppWeb do
80
33
def channel do
81
34
quote do
82
35
use Phoenix.Channel
83
- import MyAppWeb.Gettext
84
36
end
85
37
end
86
38
87
- defp view_helpers do
39
+ def controller do
88
40
quote do
89
- # Use all HTML functionality (forms, tags, etc)
90
- use Phoenix.HTML
41
+ use Phoenix.Controller,
42
+ formats: [:html, :json],
43
+ layouts: [html: MyAppWeb.Layouts]
91
44
92
- # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
93
- import Phoenix.LiveView.Helpers
94
-
95
- # Import basic rendering functionality (render, render_layout, etc)
96
- import Phoenix.View
97
-
98
- import MyAppWeb.ErrorHelpers
45
+ import Plug.Conn
99
46
import MyAppWeb.Gettext
100
- alias MyAppWeb.Router.Helpers, as: Routes
47
+
48
+ unquote(verified_routes())
49
+ end
50
+ end
51
+
52
+ def live_view do
53
+ quote do
54
+ use Phoenix.LiveView,
55
+ layout: {MyAppWeb.Layouts, :app}
56
+
57
+ unquote(html_helpers())
58
+ end
59
+ end
60
+
61
+ def live_component do
62
+ quote do
63
+ use Phoenix.LiveComponent
64
+
65
+ unquote(html_helpers())
66
+ end
67
+ end
68
+
69
+ def html do
70
+ quote do
71
+ use Phoenix.Component
72
+
73
+ # Import convenience functions from controllers
74
+ import Phoenix.Controller,
75
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
76
+
77
+ # Include general helpers for rendering HTML
78
+ unquote(html_helpers())
79
+ end
80
+ end
81
+
82
+ defp html_helpers do
83
+ quote do
84
+ # HTML escaping functionality
85
+ import Phoenix.HTML
86
+ # Core UI components and translation
87
+ import MyAppWeb.CoreComponents
88
+ import MyAppWeb.Gettext
89
+
90
+ # Shortcut for generating JS commands
91
+ alias Phoenix.LiveView.JS
92
+
93
+ # Routes generation with the ~p sigil
94
+ unquote(verified_routes())
95
+ end
96
+ end
97
+
98
+ def verified_routes do
99
+ quote do
100
+ use Phoenix.VerifiedRoutes,
101
+ endpoint: MyAppWeb.Endpoint,
102
+ router: MyAppWeb.Router,
103
+ statics: MyAppWeb.static_paths()
101
104
end
102
105
end
103
106
changed mix.exs
 
@@ -5,9 +5,8 @@ defmodule MyApp.MixProject do
5
5
[
6
6
app: :my_app,
7
7
version: "0.1.0",
8
- elixir: "~> 1.12",
8
+ elixir: "~> 1.14",
9
9
elixirc_paths: elixirc_paths(Mix.env()),
10
- compilers: [:gettext] ++ Mix.compilers(),
11
10
start_permanent: Mix.env() == :prod,
12
11
aliases: aliases(),
13
12
deps: deps()
 
@@ -33,20 +32,23 @@ defmodule MyApp.MixProject do
33
32
# Type `mix help deps` for examples and options.
34
33
defp deps do
35
34
[
36
- {:phoenix, "~> 1.6.16"},
35
+ {:phoenix, "~> 1.7.0"},
37
36
{:phoenix_ecto, "~> 4.4"},
38
37
{:ecto_sql, "~> 3.6"},
39
38
{:postgrex, ">= 0.0.0"},
40
- {:phoenix_html, "~> 3.0"},
39
+ {:phoenix_html, "~> 3.3"},
41
40
{:phoenix_live_reload, "~> 1.2", only: :dev},
42
- {:phoenix_live_view, "~> 0.17.5"},
41
+ {:phoenix_live_view, "~> 0.18.16"},
42
+ {:heroicons, "~> 0.5"},
43
43
{:floki, ">= 0.30.0", only: :test},
44
- {:phoenix_live_dashboard, "~> 0.6"},
45
- {:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
44
+ {:phoenix_live_dashboard, "~> 0.7.2"},
45
+ {:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
46
+ {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
46
47
{:swoosh, "~> 1.3"},
48
+ {:finch, "~> 0.13"},
47
49
{:telemetry_metrics, "~> 0.6"},
48
50
{:telemetry_poller, "~> 1.0"},
49
- {:gettext, "~> 0.18"},
51
+ {:gettext, "~> 0.20"},
50
52
{:jason, "~> 1.2"},
51
53
{:plug_cowboy, "~> 2.5"}
52
54
]
 
@@ -60,11 +62,13 @@ defmodule MyApp.MixProject do
60
62
# See the documentation for `Mix` for more info on aliases.
61
63
defp aliases do
62
64
[
63
- setup: ["deps.get", "ecto.setup"],
65
+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
64
66
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
65
67
"ecto.reset": ["ecto.drop", "ecto.setup"],
66
68
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
67
- "assets.deploy": ["esbuild default --minify", "phx.digest"]
69
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
70
+ "assets.build": ["tailwind default", "esbuild default"],
71
+ "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
68
72
]
69
73
end
70
74
end
changed priv/gettext/errors.pot
 
@@ -48,18 +48,18 @@ msgid "are still associated with this entry"
48
48
msgstr ""
49
49
50
50
## From Ecto.Changeset.validate_length/3
51
- msgid "should be %{count} character(s)"
52
- msgid_plural "should be %{count} character(s)"
53
- msgstr[0] ""
54
- msgstr[1] ""
55
-
56
51
msgid "should have %{count} item(s)"
57
52
msgid_plural "should have %{count} item(s)"
58
53
msgstr[0] ""
59
54
msgstr[1] ""
60
55
61
- msgid "should be at least %{count} character(s)"
62
- msgid_plural "should be at least %{count} character(s)"
56
+ msgid "should be %{count} character(s)"
57
+ msgid_plural "should be %{count} character(s)"
58
+ msgstr[0] ""
59
+ msgstr[1] ""
60
+
61
+ msgid "should be %{count} byte(s)"
62
+ msgid_plural "should be %{count} byte(s)"
63
63
msgstr[0] ""
64
64
msgstr[1] ""
65
65
 
@@ -68,8 +68,13 @@ msgid_plural "should have at least %{count} item(s)"
68
68
msgstr[0] ""
69
69
msgstr[1] ""
70
70
71
- msgid "should be at most %{count} character(s)"
72
- msgid_plural "should be at most %{count} character(s)"
71
+ msgid "should be at least %{count} character(s)"
72
+ msgid_plural "should be at least %{count} character(s)"
73
+ msgstr[0] ""
74
+ msgstr[1] ""
75
+
76
+ msgid "should be at least %{count} byte(s)"
77
+ msgid_plural "should be at least %{count} byte(s)"
73
78
msgstr[0] ""
74
79
msgstr[1] ""
75
80
 
@@ -78,6 +83,16 @@ msgid_plural "should have at most %{count} item(s)"
78
83
msgstr[0] ""
79
84
msgstr[1] ""
80
85
86
+ msgid "should be at most %{count} character(s)"
87
+ msgid_plural "should be at most %{count} character(s)"
88
+ msgstr[0] ""
89
+ msgstr[1] ""
90
+
91
+ msgid "should be at most %{count} byte(s)"
92
+ msgid_plural "should be at most %{count} byte(s)"
93
+ msgstr[0] ""
94
+ msgstr[1] ""
95
+
81
96
## From Ecto.Changeset.validate_number/3
82
97
msgid "must be less than %{number}"
83
98
msgstr ""
removed priv/static/images/phoenix.png
added test/my_app_web/controllers/error_html_test.exs
 
@@ -0,0 +1,14 @@
1
+ defmodule MyAppWeb.ErrorHTMLTest do
2
+ use MyAppWeb.ConnCase, async: true
3
+
4
+ # Bring render_to_string/4 for testing custom views
5
+ import Phoenix.Template
6
+
7
+ test "renders 404.html" do
8
+ assert render_to_string(MyAppWeb.ErrorHTML, "404", "html", []) == "Not Found"
9
+ end
10
+
11
+ test "renders 500.html" do
12
+ assert render_to_string(MyAppWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
13
+ end
14
+ end
added test/my_app_web/controllers/error_json_test.exs
 
@@ -0,0 +1,12 @@
1
+ defmodule MyAppWeb.ErrorJSONTest do
2
+ use MyAppWeb.ConnCase, async: true
3
+
4
+ test "renders 404" do
5
+ assert MyAppWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6
+ end
7
+
8
+ test "renders 500" do
9
+ assert MyAppWeb.ErrorJSON.render("500.json", %{}) ==
10
+ %{errors: %{detail: "Internal Server Error"}}
11
+ end
12
+ end
changed test/my_app_web/controllers/page_controller_test.exs
 
@@ -2,7 +2,7 @@ defmodule MyAppWeb.PageControllerTest do
2
2
use MyAppWeb.ConnCase
3
3
4
4
test "GET /", %{conn: conn} do
5
- conn = get(conn, "/")
6
- assert html_response(conn, 200) =~ "Welcome to Phoenix!"
5
+ conn = get(conn, ~p"/")
6
+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7
7
end
8
8
end
removed test/my_app_web/views/error_view_test.exs
 
@@ -1,14 +0,0 @@
1
- defmodule MyAppWeb.ErrorViewTest do
2
- use MyAppWeb.ConnCase, async: true
3
-
4
- # Bring render/3 and render_to_string/3 for testing custom views
5
- import Phoenix.View
6
-
7
- test "renders 404.html" do
8
- assert render_to_string(MyAppWeb.ErrorView, "404.html", []) == "Not Found"
9
- end
10
-
11
- test "renders 500.html" do
12
- assert render_to_string(MyAppWeb.ErrorView, "500.html", []) == "Internal Server Error"
13
- end
14
- end
removed test/my_app_web/views/layout_view_test.exs
 
@@ -1,8 +0,0 @@
1
- defmodule MyAppWeb.LayoutViewTest do
2
- use MyAppWeb.ConnCase, async: true
3
-
4
- # When testing helpers, you may want to import Phoenix.HTML and
5
- # use functions such as safe_to_string() to convert the helper
6
- # result into an HTML string.
7
- # import Phoenix.HTML
8
- end
removed test/my_app_web/views/page_view_test.exs
 
@@ -1,3 +0,0 @@
1
- defmodule MyAppWeb.PageViewTest do
2
- use MyAppWeb.ConnCase, async: true
3
- end
changed test/support/conn_case.ex
 
@@ -19,15 +19,15 @@ defmodule MyAppWeb.ConnCase do
19
19
20
20
using do
21
21
quote do
22
+ # The default endpoint for testing
23
+ @endpoint MyAppWeb.Endpoint
24
+
25
+ use MyAppWeb, :verified_routes
26
+
22
27
# Import conveniences for testing with connections
23
28
import Plug.Conn
24
29
import Phoenix.ConnTest
25
30
import MyAppWeb.ConnCase
26
-
27
- alias MyAppWeb.Router.Helpers, as: Routes
28
-
29
- # The default endpoint for testing
30
- @endpoint MyAppWeb.Endpoint
31
31
end
32
32
end
33
33