VERSION=1.6.16 phx.new my_app
VERSION=1.7.0 phx.new my_app
|
@@ -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
|
]
|
|
@@ -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
|
|
|
@@ -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.
|
|
@@ -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
|
- }
|
|
@@ -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
|
- }
|
|
@@ -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()
|
|
@@ -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
|
+ }
|
|
@@ -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) {
|
|
@@ -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",
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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">→</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>
|
|
@@ -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>
|
|
@@ -0,0 +1,5 @@
|
1
|
+ defmodule MyAppWeb.Layouts do
|
2
|
+ use MyAppWeb, :html
|
3
|
+
|
4
|
+ embed_templates "layouts/*"
|
5
|
+ end
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 & 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>
|
|
@@ -0,0 +1,5 @@
|
1
|
+ defmodule MyAppWeb.PageHTML do
|
2
|
+ use MyAppWeb, :html
|
3
|
+
|
4
|
+ embed_templates "page_html/*"
|
5
|
+ end
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -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",
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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 & 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>
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1,3 +0,0 @@
|
1
|
- defmodule MyAppWeb.PageView do
|
2
|
- use MyAppWeb, :view
|
3
|
- end
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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 ""
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1,3 +0,0 @@
|
1
|
- defmodule MyAppWeb.PageViewTest do
|
2
|
- use MyAppWeb.ConnCase, async: true
|
3
|
- end
|
|
@@ -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
|
|