+18
elixir_blonk/README.md
+18
elixir_blonk/README.md
···
1
+
# ElixirBlonk
2
+
3
+
To start your Phoenix server:
4
+
5
+
* Run `mix setup` to install and setup dependencies
6
+
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
7
+
8
+
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
9
+
10
+
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
11
+
12
+
## Learn more
13
+
14
+
* Official website: https://www.phoenixframework.org/
15
+
* Guides: https://hexdocs.pm/phoenix/overview.html
16
+
* Docs: https://hexdocs.pm/phoenix
17
+
* Forum: https://elixirforum.com/c/phoenix-forum
18
+
* Source: https://github.com/phoenixframework/phoenix
+5
elixir_blonk/assets/css/app.css
+5
elixir_blonk/assets/css/app.css
+44
elixir_blonk/assets/js/app.js
+44
elixir_blonk/assets/js/app.js
···
1
+
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
2
+
// to get started and then uncomment the line below.
3
+
// import "./user_socket.js"
4
+
5
+
// You can include dependencies in two ways.
6
+
//
7
+
// The simplest option is to put them in assets/vendor and
8
+
// import them using relative paths:
9
+
//
10
+
// import "../vendor/some-package.js"
11
+
//
12
+
// Alternatively, you can `npm install some-package --prefix assets` and import
13
+
// them using a path starting with the package name:
14
+
//
15
+
// import "some-package"
16
+
//
17
+
18
+
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
19
+
import "phoenix_html"
20
+
// Establish Phoenix Socket and LiveView configuration.
21
+
import {Socket} from "phoenix"
22
+
import {LiveSocket} from "phoenix_live_view"
23
+
import topbar from "../vendor/topbar"
24
+
25
+
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
26
+
let liveSocket = new LiveSocket("/live", Socket, {
27
+
longPollFallbackMs: 2500,
28
+
params: {_csrf_token: csrfToken}
29
+
})
30
+
31
+
// Show progress bar on live navigation and form submits
32
+
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
33
+
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
34
+
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
35
+
36
+
// connect if there are any LiveViews on the page
37
+
liveSocket.connect()
38
+
39
+
// expose liveSocket on window for web console debug logs and latency simulation:
40
+
// >> liveSocket.enableDebug()
41
+
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
42
+
// >> liveSocket.disableLatencySim()
43
+
window.liveSocket = liveSocket
44
+
+74
elixir_blonk/assets/tailwind.config.js
+74
elixir_blonk/assets/tailwind.config.js
···
1
+
// See the Tailwind configuration guide for advanced usage
2
+
// https://tailwindcss.com/docs/configuration
3
+
4
+
const plugin = require("tailwindcss/plugin")
5
+
const fs = require("fs")
6
+
const path = require("path")
7
+
8
+
module.exports = {
9
+
content: [
10
+
"./js/**/*.js",
11
+
"../lib/elixir_blonk_web.ex",
12
+
"../lib/elixir_blonk_web/**/*.*ex"
13
+
],
14
+
theme: {
15
+
extend: {
16
+
colors: {
17
+
brand: "#FD4F00",
18
+
}
19
+
},
20
+
},
21
+
plugins: [
22
+
require("@tailwindcss/forms"),
23
+
// Allows prefixing tailwind classes with LiveView classes to add rules
24
+
// only when LiveView classes are applied, for example:
25
+
//
26
+
// <div class="phx-click-loading:animate-ping">
27
+
//
28
+
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
29
+
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
30
+
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
31
+
32
+
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
33
+
// See your `CoreComponents.icon/1` for more information.
34
+
//
35
+
plugin(function({matchComponents, theme}) {
36
+
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
37
+
let values = {}
38
+
let icons = [
39
+
["", "/24/outline"],
40
+
["-solid", "/24/solid"],
41
+
["-mini", "/20/solid"],
42
+
["-micro", "/16/solid"]
43
+
]
44
+
icons.forEach(([suffix, dir]) => {
45
+
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
46
+
let name = path.basename(file, ".svg") + suffix
47
+
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
48
+
})
49
+
})
50
+
matchComponents({
51
+
"hero": ({name, fullPath}) => {
52
+
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
53
+
let size = theme("spacing.6")
54
+
if (name.endsWith("-mini")) {
55
+
size = theme("spacing.5")
56
+
} else if (name.endsWith("-micro")) {
57
+
size = theme("spacing.4")
58
+
}
59
+
return {
60
+
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
61
+
"-webkit-mask": `var(--hero-${name})`,
62
+
"mask": `var(--hero-${name})`,
63
+
"mask-repeat": "no-repeat",
64
+
"background-color": "currentColor",
65
+
"vertical-align": "middle",
66
+
"display": "inline-block",
67
+
"width": size,
68
+
"height": size
69
+
}
70
+
}
71
+
}, {values})
72
+
})
73
+
]
74
+
}
+165
elixir_blonk/assets/vendor/topbar.js
+165
elixir_blonk/assets/vendor/topbar.js
···
1
+
/**
2
+
* @license MIT
3
+
* topbar 2.0.0, 2023-02-04
4
+
* https://buunguyen.github.io/topbar
5
+
* Copyright (c) 2021 Buu Nguyen
6
+
*/
7
+
(function (window, document) {
8
+
"use strict";
9
+
10
+
// https://gist.github.com/paulirish/1579671
11
+
(function () {
12
+
var lastTime = 0;
13
+
var vendors = ["ms", "moz", "webkit", "o"];
14
+
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15
+
window.requestAnimationFrame =
16
+
window[vendors[x] + "RequestAnimationFrame"];
17
+
window.cancelAnimationFrame =
18
+
window[vendors[x] + "CancelAnimationFrame"] ||
19
+
window[vendors[x] + "CancelRequestAnimationFrame"];
20
+
}
21
+
if (!window.requestAnimationFrame)
22
+
window.requestAnimationFrame = function (callback, element) {
23
+
var currTime = new Date().getTime();
24
+
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25
+
var id = window.setTimeout(function () {
26
+
callback(currTime + timeToCall);
27
+
}, timeToCall);
28
+
lastTime = currTime + timeToCall;
29
+
return id;
30
+
};
31
+
if (!window.cancelAnimationFrame)
32
+
window.cancelAnimationFrame = function (id) {
33
+
clearTimeout(id);
34
+
};
35
+
})();
36
+
37
+
var canvas,
38
+
currentProgress,
39
+
showing,
40
+
progressTimerId = null,
41
+
fadeTimerId = null,
42
+
delayTimerId = null,
43
+
addEvent = function (elem, type, handler) {
44
+
if (elem.addEventListener) elem.addEventListener(type, handler, false);
45
+
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
46
+
else elem["on" + type] = handler;
47
+
},
48
+
options = {
49
+
autoRun: true,
50
+
barThickness: 3,
51
+
barColors: {
52
+
0: "rgba(26, 188, 156, .9)",
53
+
".25": "rgba(52, 152, 219, .9)",
54
+
".50": "rgba(241, 196, 15, .9)",
55
+
".75": "rgba(230, 126, 34, .9)",
56
+
"1.0": "rgba(211, 84, 0, .9)",
57
+
},
58
+
shadowBlur: 10,
59
+
shadowColor: "rgba(0, 0, 0, .6)",
60
+
className: null,
61
+
},
62
+
repaint = function () {
63
+
canvas.width = window.innerWidth;
64
+
canvas.height = options.barThickness * 5; // need space for shadow
65
+
66
+
var ctx = canvas.getContext("2d");
67
+
ctx.shadowBlur = options.shadowBlur;
68
+
ctx.shadowColor = options.shadowColor;
69
+
70
+
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
71
+
for (var stop in options.barColors)
72
+
lineGradient.addColorStop(stop, options.barColors[stop]);
73
+
ctx.lineWidth = options.barThickness;
74
+
ctx.beginPath();
75
+
ctx.moveTo(0, options.barThickness / 2);
76
+
ctx.lineTo(
77
+
Math.ceil(currentProgress * canvas.width),
78
+
options.barThickness / 2
79
+
);
80
+
ctx.strokeStyle = lineGradient;
81
+
ctx.stroke();
82
+
},
83
+
createCanvas = function () {
84
+
canvas = document.createElement("canvas");
85
+
var style = canvas.style;
86
+
style.position = "fixed";
87
+
style.top = style.left = style.right = style.margin = style.padding = 0;
88
+
style.zIndex = 100001;
89
+
style.display = "none";
90
+
if (options.className) canvas.classList.add(options.className);
91
+
document.body.appendChild(canvas);
92
+
addEvent(window, "resize", repaint);
93
+
},
94
+
topbar = {
95
+
config: function (opts) {
96
+
for (var key in opts)
97
+
if (options.hasOwnProperty(key)) options[key] = opts[key];
98
+
},
99
+
show: function (delay) {
100
+
if (showing) return;
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
+
}
119
+
}
120
+
},
121
+
progress: function (to) {
122
+
if (typeof to === "undefined") return currentProgress;
123
+
if (typeof to === "string") {
124
+
to =
125
+
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
126
+
? currentProgress
127
+
: 0) + parseFloat(to);
128
+
}
129
+
currentProgress = to > 1 ? 1 : to;
130
+
repaint();
131
+
return currentProgress;
132
+
},
133
+
hide: function () {
134
+
clearTimeout(delayTimerId);
135
+
delayTimerId = null;
136
+
if (!showing) return;
137
+
showing = false;
138
+
if (progressTimerId != null) {
139
+
window.cancelAnimationFrame(progressTimerId);
140
+
progressTimerId = null;
141
+
}
142
+
(function loop() {
143
+
if (topbar.progress("+.1") >= 1) {
144
+
canvas.style.opacity -= 0.05;
145
+
if (canvas.style.opacity <= 0.05) {
146
+
canvas.style.display = "none";
147
+
fadeTimerId = null;
148
+
return;
149
+
}
150
+
}
151
+
fadeTimerId = window.requestAnimationFrame(loop);
152
+
})();
153
+
},
154
+
};
155
+
156
+
if (typeof module === "object" && typeof module.exports === "object") {
157
+
module.exports = topbar;
158
+
} else if (typeof define === "function" && define.amd) {
159
+
define(function () {
160
+
return topbar;
161
+
});
162
+
} else {
163
+
this.topbar = topbar;
164
+
}
165
+
}.call(this, window, document));
+66
elixir_blonk/config/config.exs
+66
elixir_blonk/config/config.exs
···
1
+
# This file is responsible for configuring your application
2
+
# and its dependencies with the aid of the Config module.
3
+
#
4
+
# This configuration file is loaded before any dependency and
5
+
# is restricted to this project.
6
+
7
+
# General application configuration
8
+
import Config
9
+
10
+
config :elixir_blonk,
11
+
ecto_repos: [ElixirBlonk.Repo],
12
+
generators: [timestamp_type: :utc_datetime]
13
+
14
+
# Configures the endpoint
15
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
16
+
url: [host: "localhost"],
17
+
adapter: Bandit.PhoenixAdapter,
18
+
render_errors: [
19
+
formats: [html: ElixirBlonkWeb.ErrorHTML, json: ElixirBlonkWeb.ErrorJSON],
20
+
layout: false
21
+
],
22
+
pubsub_server: ElixirBlonk.PubSub,
23
+
live_view: [signing_salt: "KHMNfM6j"]
24
+
25
+
# Configures the mailer
26
+
#
27
+
# By default it uses the "Local" adapter which stores the emails
28
+
# locally. You can see the emails in your browser, at "/dev/mailbox".
29
+
#
30
+
# For production it's recommended to configure a different adapter
31
+
# at the `config/runtime.exs`.
32
+
config :elixir_blonk, ElixirBlonk.Mailer, adapter: Swoosh.Adapters.Local
33
+
34
+
# Configure esbuild (the version is required)
35
+
config :esbuild,
36
+
version: "0.17.11",
37
+
elixir_blonk: [
38
+
args:
39
+
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
40
+
cd: Path.expand("../assets", __DIR__),
41
+
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
42
+
]
43
+
44
+
# Configure tailwind (the version is required)
45
+
config :tailwind,
46
+
version: "3.4.3",
47
+
elixir_blonk: [
48
+
args: ~w(
49
+
--config=tailwind.config.js
50
+
--input=css/app.css
51
+
--output=../priv/static/assets/app.css
52
+
),
53
+
cd: Path.expand("../assets", __DIR__)
54
+
]
55
+
56
+
# Configures Elixir's Logger
57
+
config :logger, :console,
58
+
format: "$time $metadata[$level] $message\n",
59
+
metadata: [:request_id]
60
+
61
+
# Use Jason for JSON parsing in Phoenix
62
+
config :phoenix, :json_library, Jason
63
+
64
+
# Import environment specific config. This must remain at the bottom
65
+
# of this file so it overrides the configuration defined above.
66
+
import_config "#{config_env()}.exs"
+85
elixir_blonk/config/dev.exs
+85
elixir_blonk/config/dev.exs
···
1
+
import Config
2
+
3
+
# Configure your database
4
+
config :elixir_blonk, ElixirBlonk.Repo,
5
+
username: "postgres",
6
+
password: "postgres",
7
+
hostname: "localhost",
8
+
database: "elixir_blonk_dev",
9
+
stacktrace: true,
10
+
show_sensitive_data_on_connection_error: true,
11
+
pool_size: 10
12
+
13
+
# For development, we disable any cache and enable
14
+
# debugging and code reloading.
15
+
#
16
+
# The watchers configuration can be used to run external
17
+
# watchers to your application. For example, we can use it
18
+
# to bundle .js and .css sources.
19
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
20
+
# Binding to loopback ipv4 address prevents access from other machines.
21
+
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
22
+
http: [ip: {127, 0, 0, 1}, port: 4000],
23
+
check_origin: false,
24
+
code_reloader: true,
25
+
debug_errors: true,
26
+
secret_key_base: "krPtNY7c3FaPlxzuqimUcQHGnEwkCpDNZpHQSWE979liUXlFMJzqO+FFpmeevPo9",
27
+
watchers: [
28
+
esbuild: {Esbuild, :install_and_run, [:elixir_blonk, ~w(--sourcemap=inline --watch)]},
29
+
tailwind: {Tailwind, :install_and_run, [:elixir_blonk, ~w(--watch)]}
30
+
]
31
+
32
+
# ## SSL Support
33
+
#
34
+
# In order to use HTTPS in development, a self-signed
35
+
# certificate can be generated by running the following
36
+
# Mix task:
37
+
#
38
+
# mix phx.gen.cert
39
+
#
40
+
# Run `mix help phx.gen.cert` for more information.
41
+
#
42
+
# The `http:` config above can be replaced with:
43
+
#
44
+
# https: [
45
+
# port: 4001,
46
+
# cipher_suite: :strong,
47
+
# keyfile: "priv/cert/selfsigned_key.pem",
48
+
# certfile: "priv/cert/selfsigned.pem"
49
+
# ],
50
+
#
51
+
# If desired, both `http:` and `https:` keys can be
52
+
# configured to run both http and https servers on
53
+
# different ports.
54
+
55
+
# Watch static and templates for browser reloading.
56
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
57
+
live_reload: [
58
+
patterns: [
59
+
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
60
+
~r"priv/gettext/.*(po)$",
61
+
~r"lib/elixir_blonk_web/(controllers|live|components)/.*(ex|heex)$"
62
+
]
63
+
]
64
+
65
+
# Enable dev routes for dashboard and mailbox
66
+
config :elixir_blonk, dev_routes: true
67
+
68
+
# Do not include metadata nor timestamps in development logs
69
+
config :logger, :console, format: "[$level] $message\n"
70
+
71
+
# Set a higher stacktrace during development. Avoid configuring such
72
+
# in production as building large stacktraces may be expensive.
73
+
config :phoenix, :stacktrace_depth, 20
74
+
75
+
# Initialize plugs at runtime for faster development compilation
76
+
config :phoenix, :plug_init_mode, :runtime
77
+
78
+
config :phoenix_live_view,
79
+
# Include HEEx debug annotations as HTML comments in rendered markup
80
+
debug_heex_annotations: true,
81
+
# Enable helpful, but potentially expensive runtime checks
82
+
enable_expensive_runtime_checks: true
83
+
84
+
# Disable swoosh api client as it is only required for production adapters.
85
+
config :swoosh, :api_client, false
+21
elixir_blonk/config/prod.exs
+21
elixir_blonk/config/prod.exs
···
1
+
import Config
2
+
3
+
# Note we also include the path to a cache manifest
4
+
# containing the digested version of static files. This
5
+
# manifest is generated by the `mix assets.deploy` task,
6
+
# which you should run after static files are built and
7
+
# before starting your production server.
8
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
9
+
cache_static_manifest: "priv/static/cache_manifest.json"
10
+
11
+
# Configures Swoosh API Client
12
+
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: ElixirBlonk.Finch
13
+
14
+
# Disable Swoosh Local Memory Storage
15
+
config :swoosh, local: false
16
+
17
+
# Do not print debug messages in production
18
+
config :logger, level: :info
19
+
20
+
# Runtime production configuration, including reading
21
+
# of environment variables, is done on config/runtime.exs.
+117
elixir_blonk/config/runtime.exs
+117
elixir_blonk/config/runtime.exs
···
1
+
import Config
2
+
3
+
# config/runtime.exs is executed for all environments, including
4
+
# during releases. It is executed after compilation and before the
5
+
# system starts, so it is typically used to load production configuration
6
+
# and secrets from environment variables or elsewhere. Do not define
7
+
# any compile-time configuration in here, as it won't be applied.
8
+
# The block below contains prod specific runtime configuration.
9
+
10
+
# ## Using releases
11
+
#
12
+
# If you use `mix release`, you need to explicitly enable the server
13
+
# by passing the PHX_SERVER=true when you start it:
14
+
#
15
+
# PHX_SERVER=true bin/elixir_blonk start
16
+
#
17
+
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18
+
# script that automatically sets the env var above.
19
+
if System.get_env("PHX_SERVER") do
20
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint, server: true
21
+
end
22
+
23
+
if config_env() == :prod do
24
+
database_url =
25
+
System.get_env("DATABASE_URL") ||
26
+
raise """
27
+
environment variable DATABASE_URL is missing.
28
+
For example: ecto://USER:PASS@HOST/DATABASE
29
+
"""
30
+
31
+
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
32
+
33
+
config :elixir_blonk, ElixirBlonk.Repo,
34
+
# ssl: true,
35
+
url: database_url,
36
+
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
37
+
socket_options: maybe_ipv6
38
+
39
+
# The secret key base is used to sign/encrypt cookies and other secrets.
40
+
# A default value is used in config/dev.exs and config/test.exs but you
41
+
# want to use a different value for prod and you most likely don't want
42
+
# to check this value into version control, so we use an environment
43
+
# variable instead.
44
+
secret_key_base =
45
+
System.get_env("SECRET_KEY_BASE") ||
46
+
raise """
47
+
environment variable SECRET_KEY_BASE is missing.
48
+
You can generate one by calling: mix phx.gen.secret
49
+
"""
50
+
51
+
host = System.get_env("PHX_HOST") || "example.com"
52
+
port = String.to_integer(System.get_env("PORT") || "4000")
53
+
54
+
config :elixir_blonk, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
55
+
56
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
57
+
url: [host: host, port: 443, scheme: "https"],
58
+
http: [
59
+
# Enable IPv6 and bind on all interfaces.
60
+
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
61
+
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
62
+
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
63
+
ip: {0, 0, 0, 0, 0, 0, 0, 0},
64
+
port: port
65
+
],
66
+
secret_key_base: secret_key_base
67
+
68
+
# ## SSL Support
69
+
#
70
+
# To get SSL working, you will need to add the `https` key
71
+
# to your endpoint configuration:
72
+
#
73
+
# config :elixir_blonk, ElixirBlonkWeb.Endpoint,
74
+
# https: [
75
+
# ...,
76
+
# port: 443,
77
+
# cipher_suite: :strong,
78
+
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
79
+
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
80
+
# ]
81
+
#
82
+
# The `cipher_suite` is set to `:strong` to support only the
83
+
# latest and more secure SSL ciphers. This means old browsers
84
+
# and clients may not be supported. You can set it to
85
+
# `:compatible` for wider support.
86
+
#
87
+
# `:keyfile` and `:certfile` expect an absolute path to the key
88
+
# and cert in disk or a relative path inside priv, for example
89
+
# "priv/ssl/server.key". For all supported SSL configuration
90
+
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
91
+
#
92
+
# We also recommend setting `force_ssl` in your config/prod.exs,
93
+
# ensuring no data is ever sent via http, always redirecting to https:
94
+
#
95
+
# config :elixir_blonk, ElixirBlonkWeb.Endpoint,
96
+
# force_ssl: [hsts: true]
97
+
#
98
+
# Check `Plug.SSL` for all available options in `force_ssl`.
99
+
100
+
# ## Configuring the mailer
101
+
#
102
+
# In production you need to configure the mailer to use a different adapter.
103
+
# Also, you may need to configure the Swoosh API client of your choice if you
104
+
# are not using SMTP. Here is an example of the configuration:
105
+
#
106
+
# config :elixir_blonk, ElixirBlonk.Mailer,
107
+
# adapter: Swoosh.Adapters.Mailgun,
108
+
# api_key: System.get_env("MAILGUN_API_KEY"),
109
+
# domain: System.get_env("MAILGUN_DOMAIN")
110
+
#
111
+
# For this example you need include a HTTP client required by Swoosh API client.
112
+
# Swoosh supports Hackney and Finch out of the box:
113
+
#
114
+
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
115
+
#
116
+
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
117
+
end
+37
elixir_blonk/config/test.exs
+37
elixir_blonk/config/test.exs
···
1
+
import Config
2
+
3
+
# Configure your database
4
+
#
5
+
# The MIX_TEST_PARTITION environment variable can be used
6
+
# to provide built-in test partitioning in CI environment.
7
+
# Run `mix help test` for more information.
8
+
config :elixir_blonk, ElixirBlonk.Repo,
9
+
username: "postgres",
10
+
password: "postgres",
11
+
hostname: "localhost",
12
+
database: "elixir_blonk_test#{System.get_env("MIX_TEST_PARTITION")}",
13
+
pool: Ecto.Adapters.SQL.Sandbox,
14
+
pool_size: System.schedulers_online() * 2
15
+
16
+
# We don't run a server during test. If one is required,
17
+
# you can enable the server option below.
18
+
config :elixir_blonk, ElixirBlonkWeb.Endpoint,
19
+
http: [ip: {127, 0, 0, 1}, port: 4002],
20
+
secret_key_base: "A+ekSTzgGgh0jggwdWRuHQuQV8SORhh7fjvodptzHBKYf6c+Dg7NG/dV8i930P3A",
21
+
server: false
22
+
23
+
# In test we don't send emails
24
+
config :elixir_blonk, ElixirBlonk.Mailer, adapter: Swoosh.Adapters.Test
25
+
26
+
# Disable swoosh api client as it is only required for production adapters
27
+
config :swoosh, :api_client, false
28
+
29
+
# Print only warnings and errors during test
30
+
config :logger, level: :warning
31
+
32
+
# Initialize plugs at runtime for faster test compilation
33
+
config :phoenix, :plug_init_mode, :runtime
34
+
35
+
# Enable helpful, but potentially expensive runtime checks
36
+
config :phoenix_live_view,
37
+
enable_expensive_runtime_checks: true
+9
elixir_blonk/lib/elixir_blonk.ex
+9
elixir_blonk/lib/elixir_blonk.ex
+36
elixir_blonk/lib/elixir_blonk/application.ex
+36
elixir_blonk/lib/elixir_blonk/application.ex
···
1
+
defmodule ElixirBlonk.Application do
2
+
# See https://hexdocs.pm/elixir/Application.html
3
+
# for more information on OTP Applications
4
+
@moduledoc false
5
+
6
+
use Application
7
+
8
+
@impl true
9
+
def start(_type, _args) do
10
+
children = [
11
+
ElixirBlonkWeb.Telemetry,
12
+
ElixirBlonk.Repo,
13
+
{DNSCluster, query: Application.get_env(:elixir_blonk, :dns_cluster_query) || :ignore},
14
+
{Phoenix.PubSub, name: ElixirBlonk.PubSub},
15
+
# Start the Finch HTTP client for sending emails
16
+
{Finch, name: ElixirBlonk.Finch},
17
+
# Start a worker by calling: ElixirBlonk.Worker.start_link(arg)
18
+
# {ElixirBlonk.Worker, arg},
19
+
# Start to serve requests, typically the last entry
20
+
ElixirBlonkWeb.Endpoint
21
+
]
22
+
23
+
# See https://hexdocs.pm/elixir/Supervisor.html
24
+
# for other strategies and supported options
25
+
opts = [strategy: :one_for_one, name: ElixirBlonk.Supervisor]
26
+
Supervisor.start_link(children, opts)
27
+
end
28
+
29
+
# Tell Phoenix to update the endpoint configuration
30
+
# whenever the application is updated.
31
+
@impl true
32
+
def config_change(changed, _new, removed) do
33
+
ElixirBlonkWeb.Endpoint.config_change(changed, removed)
34
+
:ok
35
+
end
36
+
end
+3
elixir_blonk/lib/elixir_blonk/mailer.ex
+3
elixir_blonk/lib/elixir_blonk/mailer.ex
+5
elixir_blonk/lib/elixir_blonk/repo.ex
+5
elixir_blonk/lib/elixir_blonk/repo.ex
+116
elixir_blonk/lib/elixir_blonk_web.ex
+116
elixir_blonk/lib/elixir_blonk_web.ex
···
1
+
defmodule ElixirBlonkWeb do
2
+
@moduledoc """
3
+
The entrypoint for defining your web interface, such
4
+
as controllers, components, channels, and so on.
5
+
6
+
This can be used in your application as:
7
+
8
+
use ElixirBlonkWeb, :controller
9
+
use ElixirBlonkWeb, :html
10
+
11
+
The definitions below will be executed for every controller,
12
+
component, etc, so keep them short and clean, focused
13
+
on imports, uses and aliases.
14
+
15
+
Do NOT define functions inside the quoted expressions
16
+
below. Instead, define additional modules and import
17
+
those modules here.
18
+
"""
19
+
20
+
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
21
+
22
+
def router do
23
+
quote do
24
+
use Phoenix.Router, helpers: false
25
+
26
+
# Import common connection and controller functions to use in pipelines
27
+
import Plug.Conn
28
+
import Phoenix.Controller
29
+
import Phoenix.LiveView.Router
30
+
end
31
+
end
32
+
33
+
def channel do
34
+
quote do
35
+
use Phoenix.Channel
36
+
end
37
+
end
38
+
39
+
def controller do
40
+
quote do
41
+
use Phoenix.Controller,
42
+
formats: [:html, :json],
43
+
layouts: [html: ElixirBlonkWeb.Layouts]
44
+
45
+
use Gettext, backend: ElixirBlonkWeb.Gettext
46
+
47
+
import Plug.Conn
48
+
49
+
unquote(verified_routes())
50
+
end
51
+
end
52
+
53
+
def live_view do
54
+
quote do
55
+
use Phoenix.LiveView,
56
+
layout: {ElixirBlonkWeb.Layouts, :app}
57
+
58
+
unquote(html_helpers())
59
+
end
60
+
end
61
+
62
+
def live_component do
63
+
quote do
64
+
use Phoenix.LiveComponent
65
+
66
+
unquote(html_helpers())
67
+
end
68
+
end
69
+
70
+
def html do
71
+
quote do
72
+
use Phoenix.Component
73
+
74
+
# Import convenience functions from controllers
75
+
import Phoenix.Controller,
76
+
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
77
+
78
+
# Include general helpers for rendering HTML
79
+
unquote(html_helpers())
80
+
end
81
+
end
82
+
83
+
defp html_helpers do
84
+
quote do
85
+
# Translation
86
+
use Gettext, backend: ElixirBlonkWeb.Gettext
87
+
88
+
# HTML escaping functionality
89
+
import Phoenix.HTML
90
+
# Core UI components
91
+
import ElixirBlonkWeb.CoreComponents
92
+
93
+
# Shortcut for generating JS commands
94
+
alias Phoenix.LiveView.JS
95
+
96
+
# Routes generation with the ~p sigil
97
+
unquote(verified_routes())
98
+
end
99
+
end
100
+
101
+
def verified_routes do
102
+
quote do
103
+
use Phoenix.VerifiedRoutes,
104
+
endpoint: ElixirBlonkWeb.Endpoint,
105
+
router: ElixirBlonkWeb.Router,
106
+
statics: ElixirBlonkWeb.static_paths()
107
+
end
108
+
end
109
+
110
+
@doc """
111
+
When used, dispatch to the appropriate controller/live_view/etc.
112
+
"""
113
+
defmacro __using__(which) when is_atom(which) do
114
+
apply(__MODULE__, which, [])
115
+
end
116
+
end
+676
elixir_blonk/lib/elixir_blonk_web/components/core_components.ex
+676
elixir_blonk/lib/elixir_blonk_web/components/core_components.ex
···
1
+
defmodule ElixirBlonkWeb.CoreComponents do
2
+
@moduledoc """
3
+
Provides core UI components.
4
+
5
+
At first glance, this module may seem daunting, but its goal is to provide
6
+
core building blocks for your application, such as modals, tables, and
7
+
forms. The components consist mostly of markup and are well-documented
8
+
with doc strings and declarative assigns. You may customize and style
9
+
them in any way you want, based on your application growth and needs.
10
+
11
+
The default components use Tailwind CSS, a utility-first CSS framework.
12
+
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
13
+
how to customize them or feel free to swap in another framework altogether.
14
+
15
+
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
16
+
"""
17
+
use Phoenix.Component
18
+
use Gettext, backend: ElixirBlonkWeb.Gettext
19
+
20
+
alias Phoenix.LiveView.JS
21
+
22
+
@doc """
23
+
Renders a modal.
24
+
25
+
## Examples
26
+
27
+
<.modal id="confirm-modal">
28
+
This is a modal.
29
+
</.modal>
30
+
31
+
JS commands may be passed to the `:on_cancel` to configure
32
+
the closing/cancel event, for example:
33
+
34
+
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
35
+
This is another modal.
36
+
</.modal>
37
+
38
+
"""
39
+
attr :id, :string, required: true
40
+
attr :show, :boolean, default: false
41
+
attr :on_cancel, JS, default: %JS{}
42
+
slot :inner_block, required: true
43
+
44
+
def modal(assigns) do
45
+
~H"""
46
+
<div
47
+
id={@id}
48
+
phx-mounted={@show && show_modal(@id)}
49
+
phx-remove={hide_modal(@id)}
50
+
data-cancel={JS.exec(@on_cancel, "phx-remove")}
51
+
class="relative z-50 hidden"
52
+
>
53
+
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
54
+
<div
55
+
class="fixed inset-0 overflow-y-auto"
56
+
aria-labelledby={"#{@id}-title"}
57
+
aria-describedby={"#{@id}-description"}
58
+
role="dialog"
59
+
aria-modal="true"
60
+
tabindex="0"
61
+
>
62
+
<div class="flex min-h-full items-center justify-center">
63
+
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
64
+
<.focus_wrap
65
+
id={"#{@id}-container"}
66
+
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
67
+
phx-key="escape"
68
+
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
69
+
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
70
+
>
71
+
<div class="absolute top-6 right-5">
72
+
<button
73
+
phx-click={JS.exec("data-cancel", to: "##{@id}")}
74
+
type="button"
75
+
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
76
+
aria-label={gettext("close")}
77
+
>
78
+
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
79
+
</button>
80
+
</div>
81
+
<div id={"#{@id}-content"}>
82
+
{render_slot(@inner_block)}
83
+
</div>
84
+
</.focus_wrap>
85
+
</div>
86
+
</div>
87
+
</div>
88
+
</div>
89
+
"""
90
+
end
91
+
92
+
@doc """
93
+
Renders flash notices.
94
+
95
+
## Examples
96
+
97
+
<.flash kind={:info} flash={@flash} />
98
+
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
99
+
"""
100
+
attr :id, :string, doc: "the optional id of flash container"
101
+
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
102
+
attr :title, :string, default: nil
103
+
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
104
+
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
105
+
106
+
slot :inner_block, doc: "the optional inner block that renders the flash message"
107
+
108
+
def flash(assigns) do
109
+
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
110
+
111
+
~H"""
112
+
<div
113
+
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
114
+
id={@id}
115
+
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
116
+
role="alert"
117
+
class={[
118
+
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
119
+
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
120
+
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
121
+
]}
122
+
{@rest}
123
+
>
124
+
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
125
+
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
126
+
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
127
+
{@title}
128
+
</p>
129
+
<p class="mt-2 text-sm leading-5">{msg}</p>
130
+
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
131
+
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
132
+
</button>
133
+
</div>
134
+
"""
135
+
end
136
+
137
+
@doc """
138
+
Shows the flash group with standard titles and content.
139
+
140
+
## Examples
141
+
142
+
<.flash_group flash={@flash} />
143
+
"""
144
+
attr :flash, :map, required: true, doc: "the map of flash messages"
145
+
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
146
+
147
+
def flash_group(assigns) do
148
+
~H"""
149
+
<div id={@id}>
150
+
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
151
+
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
152
+
<.flash
153
+
id="client-error"
154
+
kind={:error}
155
+
title={gettext("We can't find the internet")}
156
+
phx-disconnected={show(".phx-client-error #client-error")}
157
+
phx-connected={hide("#client-error")}
158
+
hidden
159
+
>
160
+
{gettext("Attempting to reconnect")}
161
+
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
162
+
</.flash>
163
+
164
+
<.flash
165
+
id="server-error"
166
+
kind={:error}
167
+
title={gettext("Something went wrong!")}
168
+
phx-disconnected={show(".phx-server-error #server-error")}
169
+
phx-connected={hide("#server-error")}
170
+
hidden
171
+
>
172
+
{gettext("Hang in there while we get back on track")}
173
+
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
174
+
</.flash>
175
+
</div>
176
+
"""
177
+
end
178
+
179
+
@doc """
180
+
Renders a simple form.
181
+
182
+
## Examples
183
+
184
+
<.simple_form for={@form} phx-change="validate" phx-submit="save">
185
+
<.input field={@form[:email]} label="Email"/>
186
+
<.input field={@form[:username]} label="Username" />
187
+
<:actions>
188
+
<.button>Save</.button>
189
+
</:actions>
190
+
</.simple_form>
191
+
"""
192
+
attr :for, :any, required: true, doc: "the data structure for the form"
193
+
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
194
+
195
+
attr :rest, :global,
196
+
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
197
+
doc: "the arbitrary HTML attributes to apply to the form tag"
198
+
199
+
slot :inner_block, required: true
200
+
slot :actions, doc: "the slot for form actions, such as a submit button"
201
+
202
+
def simple_form(assigns) do
203
+
~H"""
204
+
<.form :let={f} for={@for} as={@as} {@rest}>
205
+
<div class="mt-10 space-y-8 bg-white">
206
+
{render_slot(@inner_block, f)}
207
+
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
208
+
{render_slot(action, f)}
209
+
</div>
210
+
</div>
211
+
</.form>
212
+
"""
213
+
end
214
+
215
+
@doc """
216
+
Renders a button.
217
+
218
+
## Examples
219
+
220
+
<.button>Send!</.button>
221
+
<.button phx-click="go" class="ml-2">Send!</.button>
222
+
"""
223
+
attr :type, :string, default: nil
224
+
attr :class, :string, default: nil
225
+
attr :rest, :global, include: ~w(disabled form name value)
226
+
227
+
slot :inner_block, required: true
228
+
229
+
def button(assigns) do
230
+
~H"""
231
+
<button
232
+
type={@type}
233
+
class={[
234
+
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
235
+
"text-sm font-semibold leading-6 text-white active:text-white/80",
236
+
@class
237
+
]}
238
+
{@rest}
239
+
>
240
+
{render_slot(@inner_block)}
241
+
</button>
242
+
"""
243
+
end
244
+
245
+
@doc """
246
+
Renders an input with label and error messages.
247
+
248
+
A `Phoenix.HTML.FormField` may be passed as argument,
249
+
which is used to retrieve the input name, id, and values.
250
+
Otherwise all attributes may be passed explicitly.
251
+
252
+
## Types
253
+
254
+
This function accepts all HTML input types, considering that:
255
+
256
+
* You may also set `type="select"` to render a `<select>` tag
257
+
258
+
* `type="checkbox"` is used exclusively to render boolean values
259
+
260
+
* For live file uploads, see `Phoenix.Component.live_file_input/1`
261
+
262
+
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
263
+
for more information. Unsupported types, such as hidden and radio,
264
+
are best written directly in your templates.
265
+
266
+
## Examples
267
+
268
+
<.input field={@form[:email]} type="email" />
269
+
<.input name="my-input" errors={["oh no!"]} />
270
+
"""
271
+
attr :id, :any, default: nil
272
+
attr :name, :any
273
+
attr :label, :string, default: nil
274
+
attr :value, :any
275
+
276
+
attr :type, :string,
277
+
default: "text",
278
+
values: ~w(checkbox color date datetime-local email file month number password
279
+
range search select tel text textarea time url week)
280
+
281
+
attr :field, Phoenix.HTML.FormField,
282
+
doc: "a form field struct retrieved from the form, for example: @form[:email]"
283
+
284
+
attr :errors, :list, default: []
285
+
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
286
+
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
287
+
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
288
+
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
289
+
290
+
attr :rest, :global,
291
+
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
292
+
multiple pattern placeholder readonly required rows size step)
293
+
294
+
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
295
+
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
296
+
297
+
assigns
298
+
|> assign(field: nil, id: assigns.id || field.id)
299
+
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
300
+
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
301
+
|> assign_new(:value, fn -> field.value end)
302
+
|> input()
303
+
end
304
+
305
+
def input(%{type: "checkbox"} = assigns) do
306
+
assigns =
307
+
assign_new(assigns, :checked, fn ->
308
+
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
309
+
end)
310
+
311
+
~H"""
312
+
<div>
313
+
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
314
+
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
315
+
<input
316
+
type="checkbox"
317
+
id={@id}
318
+
name={@name}
319
+
value="true"
320
+
checked={@checked}
321
+
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
322
+
{@rest}
323
+
/>
324
+
{@label}
325
+
</label>
326
+
<.error :for={msg <- @errors}>{msg}</.error>
327
+
</div>
328
+
"""
329
+
end
330
+
331
+
def input(%{type: "select"} = assigns) do
332
+
~H"""
333
+
<div>
334
+
<.label for={@id}>{@label}</.label>
335
+
<select
336
+
id={@id}
337
+
name={@name}
338
+
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
339
+
multiple={@multiple}
340
+
{@rest}
341
+
>
342
+
<option :if={@prompt} value="">{@prompt}</option>
343
+
{Phoenix.HTML.Form.options_for_select(@options, @value)}
344
+
</select>
345
+
<.error :for={msg <- @errors}>{msg}</.error>
346
+
</div>
347
+
"""
348
+
end
349
+
350
+
def input(%{type: "textarea"} = assigns) do
351
+
~H"""
352
+
<div>
353
+
<.label for={@id}>{@label}</.label>
354
+
<textarea
355
+
id={@id}
356
+
name={@name}
357
+
class={[
358
+
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
359
+
@errors == [] && "border-zinc-300 focus:border-zinc-400",
360
+
@errors != [] && "border-rose-400 focus:border-rose-400"
361
+
]}
362
+
{@rest}
363
+
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
364
+
<.error :for={msg <- @errors}>{msg}</.error>
365
+
</div>
366
+
"""
367
+
end
368
+
369
+
# All other inputs text, datetime-local, url, password, etc. are handled here...
370
+
def input(assigns) do
371
+
~H"""
372
+
<div>
373
+
<.label for={@id}>{@label}</.label>
374
+
<input
375
+
type={@type}
376
+
name={@name}
377
+
id={@id}
378
+
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
379
+
class={[
380
+
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
381
+
@errors == [] && "border-zinc-300 focus:border-zinc-400",
382
+
@errors != [] && "border-rose-400 focus:border-rose-400"
383
+
]}
384
+
{@rest}
385
+
/>
386
+
<.error :for={msg <- @errors}>{msg}</.error>
387
+
</div>
388
+
"""
389
+
end
390
+
391
+
@doc """
392
+
Renders a label.
393
+
"""
394
+
attr :for, :string, default: nil
395
+
slot :inner_block, required: true
396
+
397
+
def label(assigns) do
398
+
~H"""
399
+
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
400
+
{render_slot(@inner_block)}
401
+
</label>
402
+
"""
403
+
end
404
+
405
+
@doc """
406
+
Generates a generic error message.
407
+
"""
408
+
slot :inner_block, required: true
409
+
410
+
def error(assigns) do
411
+
~H"""
412
+
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
413
+
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
414
+
{render_slot(@inner_block)}
415
+
</p>
416
+
"""
417
+
end
418
+
419
+
@doc """
420
+
Renders a header with title.
421
+
"""
422
+
attr :class, :string, default: nil
423
+
424
+
slot :inner_block, required: true
425
+
slot :subtitle
426
+
slot :actions
427
+
428
+
def header(assigns) do
429
+
~H"""
430
+
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
431
+
<div>
432
+
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
433
+
{render_slot(@inner_block)}
434
+
</h1>
435
+
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
436
+
{render_slot(@subtitle)}
437
+
</p>
438
+
</div>
439
+
<div class="flex-none">{render_slot(@actions)}</div>
440
+
</header>
441
+
"""
442
+
end
443
+
444
+
@doc ~S"""
445
+
Renders a table with generic styling.
446
+
447
+
## Examples
448
+
449
+
<.table id="users" rows={@users}>
450
+
<:col :let={user} label="id">{user.id}</:col>
451
+
<:col :let={user} label="username">{user.username}</:col>
452
+
</.table>
453
+
"""
454
+
attr :id, :string, required: true
455
+
attr :rows, :list, required: true
456
+
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
457
+
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
458
+
459
+
attr :row_item, :any,
460
+
default: &Function.identity/1,
461
+
doc: "the function for mapping each row before calling the :col and :action slots"
462
+
463
+
slot :col, required: true do
464
+
attr :label, :string
465
+
end
466
+
467
+
slot :action, doc: "the slot for showing user actions in the last table column"
468
+
469
+
def table(assigns) do
470
+
assigns =
471
+
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
472
+
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
473
+
end
474
+
475
+
~H"""
476
+
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
477
+
<table class="w-[40rem] mt-11 sm:w-full">
478
+
<thead class="text-sm text-left leading-6 text-zinc-500">
479
+
<tr>
480
+
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
481
+
<th :if={@action != []} class="relative p-0 pb-4">
482
+
<span class="sr-only">{gettext("Actions")}</span>
483
+
</th>
484
+
</tr>
485
+
</thead>
486
+
<tbody
487
+
id={@id}
488
+
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
489
+
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
490
+
>
491
+
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
492
+
<td
493
+
:for={{col, i} <- Enum.with_index(@col)}
494
+
phx-click={@row_click && @row_click.(row)}
495
+
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
496
+
>
497
+
<div class="block py-4 pr-6">
498
+
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
499
+
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
500
+
{render_slot(col, @row_item.(row))}
501
+
</span>
502
+
</div>
503
+
</td>
504
+
<td :if={@action != []} class="relative w-14 p-0">
505
+
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
506
+
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
507
+
<span
508
+
:for={action <- @action}
509
+
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
510
+
>
511
+
{render_slot(action, @row_item.(row))}
512
+
</span>
513
+
</div>
514
+
</td>
515
+
</tr>
516
+
</tbody>
517
+
</table>
518
+
</div>
519
+
"""
520
+
end
521
+
522
+
@doc """
523
+
Renders a data list.
524
+
525
+
## Examples
526
+
527
+
<.list>
528
+
<:item title="Title">{@post.title}</:item>
529
+
<:item title="Views">{@post.views}</:item>
530
+
</.list>
531
+
"""
532
+
slot :item, required: true do
533
+
attr :title, :string, required: true
534
+
end
535
+
536
+
def list(assigns) do
537
+
~H"""
538
+
<div class="mt-14">
539
+
<dl class="-my-4 divide-y divide-zinc-100">
540
+
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
541
+
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
542
+
<dd class="text-zinc-700">{render_slot(item)}</dd>
543
+
</div>
544
+
</dl>
545
+
</div>
546
+
"""
547
+
end
548
+
549
+
@doc """
550
+
Renders a back navigation link.
551
+
552
+
## Examples
553
+
554
+
<.back navigate={~p"/posts"}>Back to posts</.back>
555
+
"""
556
+
attr :navigate, :any, required: true
557
+
slot :inner_block, required: true
558
+
559
+
def back(assigns) do
560
+
~H"""
561
+
<div class="mt-16">
562
+
<.link
563
+
navigate={@navigate}
564
+
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
565
+
>
566
+
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
567
+
{render_slot(@inner_block)}
568
+
</.link>
569
+
</div>
570
+
"""
571
+
end
572
+
573
+
@doc """
574
+
Renders a [Heroicon](https://heroicons.com).
575
+
576
+
Heroicons come in three styles – outline, solid, and mini.
577
+
By default, the outline style is used, but solid and mini may
578
+
be applied by using the `-solid` and `-mini` suffix.
579
+
580
+
You can customize the size and colors of the icons by setting
581
+
width, height, and background color classes.
582
+
583
+
Icons are extracted from the `deps/heroicons` directory and bundled within
584
+
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
585
+
586
+
## Examples
587
+
588
+
<.icon name="hero-x-mark-solid" />
589
+
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
590
+
"""
591
+
attr :name, :string, required: true
592
+
attr :class, :string, default: nil
593
+
594
+
def icon(%{name: "hero-" <> _} = assigns) do
595
+
~H"""
596
+
<span class={[@name, @class]} />
597
+
"""
598
+
end
599
+
600
+
## JS Commands
601
+
602
+
def show(js \\ %JS{}, selector) do
603
+
JS.show(js,
604
+
to: selector,
605
+
time: 300,
606
+
transition:
607
+
{"transition-all transform ease-out duration-300",
608
+
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
609
+
"opacity-100 translate-y-0 sm:scale-100"}
610
+
)
611
+
end
612
+
613
+
def hide(js \\ %JS{}, selector) do
614
+
JS.hide(js,
615
+
to: selector,
616
+
time: 200,
617
+
transition:
618
+
{"transition-all transform ease-in duration-200",
619
+
"opacity-100 translate-y-0 sm:scale-100",
620
+
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
621
+
)
622
+
end
623
+
624
+
def show_modal(js \\ %JS{}, id) when is_binary(id) do
625
+
js
626
+
|> JS.show(to: "##{id}")
627
+
|> JS.show(
628
+
to: "##{id}-bg",
629
+
time: 300,
630
+
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
631
+
)
632
+
|> show("##{id}-container")
633
+
|> JS.add_class("overflow-hidden", to: "body")
634
+
|> JS.focus_first(to: "##{id}-content")
635
+
end
636
+
637
+
def hide_modal(js \\ %JS{}, id) do
638
+
js
639
+
|> JS.hide(
640
+
to: "##{id}-bg",
641
+
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
642
+
)
643
+
|> hide("##{id}-container")
644
+
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
645
+
|> JS.remove_class("overflow-hidden", to: "body")
646
+
|> JS.pop_focus()
647
+
end
648
+
649
+
@doc """
650
+
Translates an error message using gettext.
651
+
"""
652
+
def translate_error({msg, opts}) do
653
+
# When using gettext, we typically pass the strings we want
654
+
# to translate as a static argument:
655
+
#
656
+
# # Translate the number of files with plural rules
657
+
# dngettext("errors", "1 file", "%{count} files", count)
658
+
#
659
+
# However the error messages in our forms and APIs are generated
660
+
# dynamically, so we need to translate them by calling Gettext
661
+
# with our gettext backend as first argument. Translations are
662
+
# available in the errors.po file (as we use the "errors" domain).
663
+
if count = opts[:count] do
664
+
Gettext.dngettext(ElixirBlonkWeb.Gettext, "errors", msg, msg, count, opts)
665
+
else
666
+
Gettext.dgettext(ElixirBlonkWeb.Gettext, "errors", msg, opts)
667
+
end
668
+
end
669
+
670
+
@doc """
671
+
Translates the errors for a field from a keyword list of errors.
672
+
"""
673
+
def translate_errors(errors, field) when is_list(errors) do
674
+
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
675
+
end
676
+
end
+14
elixir_blonk/lib/elixir_blonk_web/components/layouts.ex
+14
elixir_blonk/lib/elixir_blonk_web/components/layouts.ex
···
1
+
defmodule ElixirBlonkWeb.Layouts do
2
+
@moduledoc """
3
+
This module holds different layouts used by your application.
4
+
5
+
See the `layouts` directory for all templates available.
6
+
The "root" layout is a skeleton rendered as part of the
7
+
application router. The "app" layout is set as the default
8
+
layout on both `use ElixirBlonkWeb, :controller` and
9
+
`use ElixirBlonkWeb, :live_view`.
10
+
"""
11
+
use ElixirBlonkWeb, :html
12
+
13
+
embed_templates "layouts/*"
14
+
end
+32
elixir_blonk/lib/elixir_blonk_web/components/layouts/app.html.heex
+32
elixir_blonk/lib/elixir_blonk_web/components/layouts/app.html.heex
···
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 text-sm">
3
+
<div class="flex items-center gap-4">
4
+
<a href="/">
5
+
<img src={~p"/images/logo.svg"} width="36" />
6
+
</a>
7
+
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
8
+
v{Application.spec(:phoenix, :vsn)}
9
+
</p>
10
+
</div>
11
+
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
12
+
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
13
+
@elixirphoenix
14
+
</a>
15
+
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
16
+
GitHub
17
+
</a>
18
+
<a
19
+
href="https://hexdocs.pm/phoenix/overview.html"
20
+
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
21
+
>
22
+
Get Started <span aria-hidden="true">→</span>
23
+
</a>
24
+
</div>
25
+
</div>
26
+
</header>
27
+
<main class="px-4 py-20 sm:px-6 lg:px-8">
28
+
<div class="mx-auto max-w-2xl">
29
+
<.flash_group flash={@flash} />
30
+
{@inner_content}
31
+
</div>
32
+
</main>
+17
elixir_blonk/lib/elixir_blonk_web/components/layouts/root.html.heex
+17
elixir_blonk/lib/elixir_blonk_web/components/layouts/root.html.heex
···
1
+
<!DOCTYPE html>
2
+
<html lang="en" class="[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 default="ElixirBlonk" suffix=" · Phoenix Framework">
8
+
{assigns[:page_title]}
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">
15
+
{@inner_content}
16
+
</body>
17
+
</html>
+24
elixir_blonk/lib/elixir_blonk_web/controllers/error_html.ex
+24
elixir_blonk/lib/elixir_blonk_web/controllers/error_html.ex
···
1
+
defmodule ElixirBlonkWeb.ErrorHTML do
2
+
@moduledoc """
3
+
This module is invoked by your endpoint in case of errors on HTML requests.
4
+
5
+
See config/config.exs.
6
+
"""
7
+
use ElixirBlonkWeb, :html
8
+
9
+
# If you want to customize your error pages,
10
+
# uncomment the embed_templates/1 call below
11
+
# and add pages to the error directory:
12
+
#
13
+
# * lib/elixir_blonk_web/controllers/error_html/404.html.heex
14
+
# * lib/elixir_blonk_web/controllers/error_html/500.html.heex
15
+
#
16
+
# embed_templates "error_html/*"
17
+
18
+
# The default is to render a plain text page based on
19
+
# the template name. For example, "404.html" becomes
20
+
# "Not Found".
21
+
def render(template, _assigns) do
22
+
Phoenix.Controller.status_message_from_template(template)
23
+
end
24
+
end
+21
elixir_blonk/lib/elixir_blonk_web/controllers/error_json.ex
+21
elixir_blonk/lib/elixir_blonk_web/controllers/error_json.ex
···
1
+
defmodule ElixirBlonkWeb.ErrorJSON do
2
+
@moduledoc """
3
+
This module is invoked by your endpoint in case of errors on JSON requests.
4
+
5
+
See config/config.exs.
6
+
"""
7
+
8
+
# If you want to customize a particular status code,
9
+
# you may add your own clauses, such as:
10
+
#
11
+
# def render("500.json", _assigns) do
12
+
# %{errors: %{detail: "Internal Server Error"}}
13
+
# end
14
+
15
+
# By default, Phoenix returns the status message from
16
+
# the template name. For example, "404.json" becomes
17
+
# "Not Found".
18
+
def render(template, _assigns) do
19
+
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
20
+
end
21
+
end
+9
elixir_blonk/lib/elixir_blonk_web/controllers/page_controller.ex
+9
elixir_blonk/lib/elixir_blonk_web/controllers/page_controller.ex
+10
elixir_blonk/lib/elixir_blonk_web/controllers/page_html.ex
+10
elixir_blonk/lib/elixir_blonk_web/controllers/page_html.ex
+222
elixir_blonk/lib/elixir_blonk_web/controllers/page_html/home.html.heex
+222
elixir_blonk/lib/elixir_blonk_web/controllers/page_html/home.html.heex
···
1
+
<.flash_group flash={@flash} />
2
+
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
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:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
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="text-brand mt-10 flex items-center text-sm font-semibold leading-6">
50
+
Phoenix Framework
51
+
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6">
52
+
v{Application.spec(:phoenix, :vsn)}
53
+
</small>
54
+
</h1>
55
+
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance">
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/v#{Application.spec(:phoenix, :vsn)}/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://web.libera.chat/#elixir"
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
175
+
fill-rule="evenodd"
176
+
clip-rule="evenodd"
177
+
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"
178
+
/>
179
+
<path
180
+
fill-rule="evenodd"
181
+
clip-rule="evenodd"
182
+
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"
183
+
/>
184
+
</svg>
185
+
Chat on Libera IRC
186
+
</a>
187
+
</div>
188
+
<div>
189
+
<a
190
+
href="https://discord.gg/elixir"
191
+
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"
192
+
>
193
+
<svg
194
+
viewBox="0 0 16 16"
195
+
aria-hidden="true"
196
+
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
197
+
>
198
+
<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" />
199
+
</svg>
200
+
Join our Discord server
201
+
</a>
202
+
</div>
203
+
<div>
204
+
<a
205
+
href="https://fly.io/docs/elixir/getting-started/"
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 20 20"
210
+
aria-hidden="true"
211
+
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
212
+
>
213
+
<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" />
214
+
</svg>
215
+
Deploy your application
216
+
</a>
217
+
</div>
218
+
</div>
219
+
</div>
220
+
</div>
221
+
</div>
222
+
</div>
+53
elixir_blonk/lib/elixir_blonk_web/endpoint.ex
+53
elixir_blonk/lib/elixir_blonk_web/endpoint.ex
···
1
+
defmodule ElixirBlonkWeb.Endpoint do
2
+
use Phoenix.Endpoint, otp_app: :elixir_blonk
3
+
4
+
# The session will be stored in the cookie and signed,
5
+
# this means its contents can be read but not tampered with.
6
+
# Set :encryption_salt if you would also like to encrypt it.
7
+
@session_options [
8
+
store: :cookie,
9
+
key: "_elixir_blonk_key",
10
+
signing_salt: "UzVVIaJL",
11
+
same_site: "Lax"
12
+
]
13
+
14
+
socket "/live", Phoenix.LiveView.Socket,
15
+
websocket: [connect_info: [session: @session_options]],
16
+
longpoll: [connect_info: [session: @session_options]]
17
+
18
+
# Serve at "/" the static files from "priv/static" directory.
19
+
#
20
+
# You should set gzip to true if you are running phx.digest
21
+
# when deploying your static files in production.
22
+
plug Plug.Static,
23
+
at: "/",
24
+
from: :elixir_blonk,
25
+
gzip: false,
26
+
only: ElixirBlonkWeb.static_paths()
27
+
28
+
# Code reloading can be explicitly enabled under the
29
+
# :code_reloader configuration of your endpoint.
30
+
if code_reloading? do
31
+
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
32
+
plug Phoenix.LiveReloader
33
+
plug Phoenix.CodeReloader
34
+
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :elixir_blonk
35
+
end
36
+
37
+
plug Phoenix.LiveDashboard.RequestLogger,
38
+
param_key: "request_logger",
39
+
cookie_key: "request_logger"
40
+
41
+
plug Plug.RequestId
42
+
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
43
+
44
+
plug Plug.Parsers,
45
+
parsers: [:urlencoded, :multipart, :json],
46
+
pass: ["*/*"],
47
+
json_decoder: Phoenix.json_library()
48
+
49
+
plug Plug.MethodOverride
50
+
plug Plug.Head
51
+
plug Plug.Session, @session_options
52
+
plug ElixirBlonkWeb.Router
53
+
end
+25
elixir_blonk/lib/elixir_blonk_web/gettext.ex
+25
elixir_blonk/lib/elixir_blonk_web/gettext.ex
···
1
+
defmodule ElixirBlonkWeb.Gettext do
2
+
@moduledoc """
3
+
A module providing Internationalization with a gettext-based API.
4
+
5
+
By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations
6
+
that you can use in your application. To use this Gettext backend module,
7
+
call `use Gettext` and pass it as an option:
8
+
9
+
use Gettext, backend: ElixirBlonkWeb.Gettext
10
+
11
+
# Simple translation
12
+
gettext("Here is the string to translate")
13
+
14
+
# Plural translation
15
+
ngettext("Here is the string to translate",
16
+
"Here are the strings to translate",
17
+
3)
18
+
19
+
# Domain-based translation
20
+
dgettext("errors", "Here is the error message to translate")
21
+
22
+
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
23
+
"""
24
+
use Gettext.Backend, otp_app: :elixir_blonk
25
+
end
+44
elixir_blonk/lib/elixir_blonk_web/router.ex
+44
elixir_blonk/lib/elixir_blonk_web/router.ex
···
1
+
defmodule ElixirBlonkWeb.Router do
2
+
use ElixirBlonkWeb, :router
3
+
4
+
pipeline :browser do
5
+
plug :accepts, ["html"]
6
+
plug :fetch_session
7
+
plug :fetch_live_flash
8
+
plug :put_root_layout, html: {ElixirBlonkWeb.Layouts, :root}
9
+
plug :protect_from_forgery
10
+
plug :put_secure_browser_headers
11
+
end
12
+
13
+
pipeline :api do
14
+
plug :accepts, ["json"]
15
+
end
16
+
17
+
scope "/", ElixirBlonkWeb do
18
+
pipe_through :browser
19
+
20
+
get "/", PageController, :home
21
+
end
22
+
23
+
# Other scopes may use custom stacks.
24
+
# scope "/api", ElixirBlonkWeb do
25
+
# pipe_through :api
26
+
# end
27
+
28
+
# Enable LiveDashboard and Swoosh mailbox preview in development
29
+
if Application.compile_env(:elixir_blonk, :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).
35
+
import Phoenix.LiveDashboard.Router
36
+
37
+
scope "/dev" do
38
+
pipe_through :browser
39
+
40
+
live_dashboard "/dashboard", metrics: ElixirBlonkWeb.Telemetry
41
+
forward "/mailbox", Plug.Swoosh.MailboxPreview
42
+
end
43
+
end
44
+
end
+93
elixir_blonk/lib/elixir_blonk_web/telemetry.ex
+93
elixir_blonk/lib/elixir_blonk_web/telemetry.ex
···
1
+
defmodule ElixirBlonkWeb.Telemetry do
2
+
use Supervisor
3
+
import Telemetry.Metrics
4
+
5
+
def start_link(arg) do
6
+
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
7
+
end
8
+
9
+
@impl true
10
+
def init(_arg) do
11
+
children = [
12
+
# Telemetry poller will execute the given period measurements
13
+
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
14
+
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
15
+
# Add reporters as children of your supervision tree.
16
+
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
17
+
]
18
+
19
+
Supervisor.init(children, strategy: :one_for_one)
20
+
end
21
+
22
+
def metrics do
23
+
[
24
+
# Phoenix Metrics
25
+
summary("phoenix.endpoint.start.system_time",
26
+
unit: {:native, :millisecond}
27
+
),
28
+
summary("phoenix.endpoint.stop.duration",
29
+
unit: {:native, :millisecond}
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
+
),
39
+
summary("phoenix.router_dispatch.stop.duration",
40
+
tags: [:route],
41
+
unit: {:native, :millisecond}
42
+
),
43
+
summary("phoenix.socket_connected.duration",
44
+
unit: {:native, :millisecond}
45
+
),
46
+
sum("phoenix.socket_drain.count"),
47
+
summary("phoenix.channel_joined.duration",
48
+
unit: {:native, :millisecond}
49
+
),
50
+
summary("phoenix.channel_handled_in.duration",
51
+
tags: [:event],
52
+
unit: {:native, :millisecond}
53
+
),
54
+
55
+
# Database Metrics
56
+
summary("elixir_blonk.repo.query.total_time",
57
+
unit: {:native, :millisecond},
58
+
description: "The sum of the other measurements"
59
+
),
60
+
summary("elixir_blonk.repo.query.decode_time",
61
+
unit: {:native, :millisecond},
62
+
description: "The time spent decoding the data received from the database"
63
+
),
64
+
summary("elixir_blonk.repo.query.query_time",
65
+
unit: {:native, :millisecond},
66
+
description: "The time spent executing the query"
67
+
),
68
+
summary("elixir_blonk.repo.query.queue_time",
69
+
unit: {:native, :millisecond},
70
+
description: "The time spent waiting for a database connection"
71
+
),
72
+
summary("elixir_blonk.repo.query.idle_time",
73
+
unit: {:native, :millisecond},
74
+
description:
75
+
"The time the connection spent waiting before being checked out for the query"
76
+
),
77
+
78
+
# VM Metrics
79
+
summary("vm.memory.total", unit: {:byte, :kilobyte}),
80
+
summary("vm.total_run_queue_lengths.total"),
81
+
summary("vm.total_run_queue_lengths.cpu"),
82
+
summary("vm.total_run_queue_lengths.io")
83
+
]
84
+
end
85
+
86
+
defp periodic_measurements do
87
+
[
88
+
# A module, function and arguments to be invoked periodically.
89
+
# This function must call :telemetry.execute/3 and a metric must be added above.
90
+
# {ElixirBlonkWeb, :count_users, []}
91
+
]
92
+
end
93
+
end
+85
elixir_blonk/mix.exs
+85
elixir_blonk/mix.exs
···
1
+
defmodule ElixirBlonk.MixProject do
2
+
use Mix.Project
3
+
4
+
def project do
5
+
[
6
+
app: :elixir_blonk,
7
+
version: "0.1.0",
8
+
elixir: "~> 1.14",
9
+
elixirc_paths: elixirc_paths(Mix.env()),
10
+
start_permanent: Mix.env() == :prod,
11
+
aliases: aliases(),
12
+
deps: deps()
13
+
]
14
+
end
15
+
16
+
# Configuration for the OTP application.
17
+
#
18
+
# Type `mix help compile.app` for more information.
19
+
def application do
20
+
[
21
+
mod: {ElixirBlonk.Application, []},
22
+
extra_applications: [:logger, :runtime_tools]
23
+
]
24
+
end
25
+
26
+
# Specifies which paths to compile per environment.
27
+
defp elixirc_paths(:test), do: ["lib", "test/support"]
28
+
defp elixirc_paths(_), do: ["lib"]
29
+
30
+
# Specifies your project dependencies.
31
+
#
32
+
# Type `mix help deps` for examples and options.
33
+
defp deps do
34
+
[
35
+
{:phoenix, "~> 1.7.21"},
36
+
{:phoenix_ecto, "~> 4.5"},
37
+
{:ecto_sql, "~> 3.10"},
38
+
{:postgrex, ">= 0.0.0"},
39
+
{:phoenix_html, "~> 4.1"},
40
+
{:phoenix_live_reload, "~> 1.2", only: :dev},
41
+
{:phoenix_live_view, "~> 1.0"},
42
+
{:floki, ">= 0.30.0", only: :test},
43
+
{:phoenix_live_dashboard, "~> 0.8.3"},
44
+
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
45
+
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
46
+
{:heroicons,
47
+
github: "tailwindlabs/heroicons",
48
+
tag: "v2.1.1",
49
+
sparse: "optimized",
50
+
app: false,
51
+
compile: false,
52
+
depth: 1},
53
+
{:swoosh, "~> 1.5"},
54
+
{:finch, "~> 0.13"},
55
+
{:telemetry_metrics, "~> 1.0"},
56
+
{:telemetry_poller, "~> 1.0"},
57
+
{:gettext, "~> 0.26"},
58
+
{:jason, "~> 1.2"},
59
+
{:dns_cluster, "~> 0.1.1"},
60
+
{:bandit, "~> 1.5"}
61
+
]
62
+
end
63
+
64
+
# Aliases are shortcuts or tasks specific to the current project.
65
+
# For example, to install project dependencies and perform other setup tasks, run:
66
+
#
67
+
# $ mix setup
68
+
#
69
+
# See the documentation for `Mix` for more info on aliases.
70
+
defp aliases do
71
+
[
72
+
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
73
+
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
74
+
"ecto.reset": ["ecto.drop", "ecto.setup"],
75
+
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
76
+
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
77
+
"assets.build": ["tailwind elixir_blonk", "esbuild elixir_blonk"],
78
+
"assets.deploy": [
79
+
"tailwind elixir_blonk --minify",
80
+
"esbuild elixir_blonk --minify",
81
+
"phx.digest"
82
+
]
83
+
]
84
+
end
85
+
end
+41
elixir_blonk/mix.lock
+41
elixir_blonk/mix.lock
···
1
+
%{
2
+
"bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
3
+
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
4
+
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
5
+
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
6
+
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
7
+
"ecto": {:hex, :ecto, "3.13.1", "ebb11c2f0307ff62e8aaba57def59ad920a3cbd89d002b1118944cbf598c13c7", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d9ea5075a6f3af9cd2cdbabe8a0759eb73b485e981fd7c03014f79479ac85340"},
8
+
"ecto_sql": {:hex, :ecto_sql, "3.13.0", "a732428f38ce86612a2c34a1ea5d0a9642a5a71f044052007fd2f2e815707990", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ce13085122a0871d93ea9ba1a886447d89c07f3b563e19e0b3dcdf201ed9fe9"},
9
+
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
10
+
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
11
+
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
12
+
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
13
+
"floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
14
+
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
15
+
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
16
+
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
17
+
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
18
+
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
19
+
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
20
+
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
21
+
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
22
+
"phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
23
+
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"},
24
+
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
25
+
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
26
+
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
27
+
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"},
28
+
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
29
+
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
30
+
"plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
31
+
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
32
+
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
33
+
"swoosh": {:hex, :swoosh, "1.19.3", "02ad4455939f502386e4e1443d4de94c514995fd0e51b3cafffd6bd270ffe81c", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "04a10f8496786b744b84130e3510eb53ca51e769c39511b65023bdf4136b732f"},
34
+
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
35
+
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
36
+
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
37
+
"telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
38
+
"thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
39
+
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
40
+
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
41
+
}
+112
elixir_blonk/priv/gettext/en/LC_MESSAGES/errors.po
+112
elixir_blonk/priv/gettext/en/LC_MESSAGES/errors.po
···
1
+
## `msgid`s in this file come from POT (.pot) files.
2
+
##
3
+
## Do not add, change, or remove `msgid`s manually here as
4
+
## they're tied to the ones in the corresponding POT file
5
+
## (with the same domain).
6
+
##
7
+
## Use `mix gettext.extract --merge` or `mix gettext.merge`
8
+
## to merge POT files into PO files.
9
+
msgid ""
10
+
msgstr ""
11
+
"Language: en\n"
12
+
13
+
## From Ecto.Changeset.cast/4
14
+
msgid "can't be blank"
15
+
msgstr ""
16
+
17
+
## From Ecto.Changeset.unique_constraint/3
18
+
msgid "has already been taken"
19
+
msgstr ""
20
+
21
+
## From Ecto.Changeset.put_change/3
22
+
msgid "is invalid"
23
+
msgstr ""
24
+
25
+
## From Ecto.Changeset.validate_acceptance/3
26
+
msgid "must be accepted"
27
+
msgstr ""
28
+
29
+
## From Ecto.Changeset.validate_format/3
30
+
msgid "has invalid format"
31
+
msgstr ""
32
+
33
+
## From Ecto.Changeset.validate_subset/3
34
+
msgid "has an invalid entry"
35
+
msgstr ""
36
+
37
+
## From Ecto.Changeset.validate_exclusion/3
38
+
msgid "is reserved"
39
+
msgstr ""
40
+
41
+
## From Ecto.Changeset.validate_confirmation/3
42
+
msgid "does not match confirmation"
43
+
msgstr ""
44
+
45
+
## From Ecto.Changeset.no_assoc_constraint/3
46
+
msgid "is still associated with this entry"
47
+
msgstr ""
48
+
49
+
msgid "are still associated with this entry"
50
+
msgstr ""
51
+
52
+
## From Ecto.Changeset.validate_length/3
53
+
msgid "should have %{count} item(s)"
54
+
msgid_plural "should have %{count} item(s)"
55
+
msgstr[0] ""
56
+
msgstr[1] ""
57
+
58
+
msgid "should be %{count} character(s)"
59
+
msgid_plural "should be %{count} character(s)"
60
+
msgstr[0] ""
61
+
msgstr[1] ""
62
+
63
+
msgid "should be %{count} byte(s)"
64
+
msgid_plural "should be %{count} byte(s)"
65
+
msgstr[0] ""
66
+
msgstr[1] ""
67
+
68
+
msgid "should have at least %{count} item(s)"
69
+
msgid_plural "should have at least %{count} item(s)"
70
+
msgstr[0] ""
71
+
msgstr[1] ""
72
+
73
+
msgid "should be at least %{count} character(s)"
74
+
msgid_plural "should be at least %{count} character(s)"
75
+
msgstr[0] ""
76
+
msgstr[1] ""
77
+
78
+
msgid "should be at least %{count} byte(s)"
79
+
msgid_plural "should be at least %{count} byte(s)"
80
+
msgstr[0] ""
81
+
msgstr[1] ""
82
+
83
+
msgid "should have at most %{count} item(s)"
84
+
msgid_plural "should have at most %{count} item(s)"
85
+
msgstr[0] ""
86
+
msgstr[1] ""
87
+
88
+
msgid "should be at most %{count} character(s)"
89
+
msgid_plural "should be at most %{count} character(s)"
90
+
msgstr[0] ""
91
+
msgstr[1] ""
92
+
93
+
msgid "should be at most %{count} byte(s)"
94
+
msgid_plural "should be at most %{count} byte(s)"
95
+
msgstr[0] ""
96
+
msgstr[1] ""
97
+
98
+
## From Ecto.Changeset.validate_number/3
99
+
msgid "must be less than %{number}"
100
+
msgstr ""
101
+
102
+
msgid "must be greater than %{number}"
103
+
msgstr ""
104
+
105
+
msgid "must be less than or equal to %{number}"
106
+
msgstr ""
107
+
108
+
msgid "must be greater than or equal to %{number}"
109
+
msgstr ""
110
+
111
+
msgid "must be equal to %{number}"
112
+
msgstr ""
+109
elixir_blonk/priv/gettext/errors.pot
+109
elixir_blonk/priv/gettext/errors.pot
···
1
+
## This is a PO Template file.
2
+
##
3
+
## `msgid`s here are often extracted from source code.
4
+
## Add new translations manually only if they're dynamic
5
+
## translations that can't be statically extracted.
6
+
##
7
+
## Run `mix gettext.extract` to bring this file up to
8
+
## date. Leave `msgstr`s empty as changing them here has no
9
+
## effect: edit them in PO (`.po`) files instead.
10
+
## From Ecto.Changeset.cast/4
11
+
msgid "can't be blank"
12
+
msgstr ""
13
+
14
+
## From Ecto.Changeset.unique_constraint/3
15
+
msgid "has already been taken"
16
+
msgstr ""
17
+
18
+
## From Ecto.Changeset.put_change/3
19
+
msgid "is invalid"
20
+
msgstr ""
21
+
22
+
## From Ecto.Changeset.validate_acceptance/3
23
+
msgid "must be accepted"
24
+
msgstr ""
25
+
26
+
## From Ecto.Changeset.validate_format/3
27
+
msgid "has invalid format"
28
+
msgstr ""
29
+
30
+
## From Ecto.Changeset.validate_subset/3
31
+
msgid "has an invalid entry"
32
+
msgstr ""
33
+
34
+
## From Ecto.Changeset.validate_exclusion/3
35
+
msgid "is reserved"
36
+
msgstr ""
37
+
38
+
## From Ecto.Changeset.validate_confirmation/3
39
+
msgid "does not match confirmation"
40
+
msgstr ""
41
+
42
+
## From Ecto.Changeset.no_assoc_constraint/3
43
+
msgid "is still associated with this entry"
44
+
msgstr ""
45
+
46
+
msgid "are still associated with this entry"
47
+
msgstr ""
48
+
49
+
## From Ecto.Changeset.validate_length/3
50
+
msgid "should have %{count} item(s)"
51
+
msgid_plural "should have %{count} item(s)"
52
+
msgstr[0] ""
53
+
msgstr[1] ""
54
+
55
+
msgid "should be %{count} character(s)"
56
+
msgid_plural "should be %{count} character(s)"
57
+
msgstr[0] ""
58
+
msgstr[1] ""
59
+
60
+
msgid "should be %{count} byte(s)"
61
+
msgid_plural "should be %{count} byte(s)"
62
+
msgstr[0] ""
63
+
msgstr[1] ""
64
+
65
+
msgid "should have at least %{count} item(s)"
66
+
msgid_plural "should have at least %{count} item(s)"
67
+
msgstr[0] ""
68
+
msgstr[1] ""
69
+
70
+
msgid "should be at least %{count} character(s)"
71
+
msgid_plural "should be at least %{count} character(s)"
72
+
msgstr[0] ""
73
+
msgstr[1] ""
74
+
75
+
msgid "should be at least %{count} byte(s)"
76
+
msgid_plural "should be at least %{count} byte(s)"
77
+
msgstr[0] ""
78
+
msgstr[1] ""
79
+
80
+
msgid "should have at most %{count} item(s)"
81
+
msgid_plural "should have at most %{count} item(s)"
82
+
msgstr[0] ""
83
+
msgstr[1] ""
84
+
85
+
msgid "should be at most %{count} character(s)"
86
+
msgid_plural "should be at most %{count} character(s)"
87
+
msgstr[0] ""
88
+
msgstr[1] ""
89
+
90
+
msgid "should be at most %{count} byte(s)"
91
+
msgid_plural "should be at most %{count} byte(s)"
92
+
msgstr[0] ""
93
+
msgstr[1] ""
94
+
95
+
## From Ecto.Changeset.validate_number/3
96
+
msgid "must be less than %{number}"
97
+
msgstr ""
98
+
99
+
msgid "must be greater than %{number}"
100
+
msgstr ""
101
+
102
+
msgid "must be less than or equal to %{number}"
103
+
msgstr ""
104
+
105
+
msgid "must be greater than or equal to %{number}"
106
+
msgstr ""
107
+
108
+
msgid "must be equal to %{number}"
109
+
msgstr ""
+4
elixir_blonk/priv/repo/migrations/.formatter.exs
+4
elixir_blonk/priv/repo/migrations/.formatter.exs
+11
elixir_blonk/priv/repo/seeds.exs
+11
elixir_blonk/priv/repo/seeds.exs
···
1
+
# Script for populating the database. You can run it as:
2
+
#
3
+
# mix run priv/repo/seeds.exs
4
+
#
5
+
# Inside the script, you can read and write to any of your
6
+
# repositories directly:
7
+
#
8
+
# ElixirBlonk.Repo.insert!(%ElixirBlonk.SomeSchema{})
9
+
#
10
+
# We recommend using the bang functions (`insert!`, `update!`
11
+
# and so on) as they will fail if something goes wrong.
elixir_blonk/priv/static/favicon.ico
elixir_blonk/priv/static/favicon.ico
This is a binary file and will not be displayed.
+6
elixir_blonk/priv/static/images/logo.svg
+6
elixir_blonk/priv/static/images/logo.svg
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
2
+
<path
3
+
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-.077.057c1.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.728a13 13 0 0 0 1.182 1.106c1.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.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
4
+
fill="#FD4F00"
5
+
/>
6
+
</svg>
+5
elixir_blonk/priv/static/robots.txt
+5
elixir_blonk/priv/static/robots.txt
+14
elixir_blonk/test/elixir_blonk_web/controllers/error_html_test.exs
+14
elixir_blonk/test/elixir_blonk_web/controllers/error_html_test.exs
···
1
+
defmodule ElixirBlonkWeb.ErrorHTMLTest do
2
+
use ElixirBlonkWeb.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(ElixirBlonkWeb.ErrorHTML, "404", "html", []) == "Not Found"
9
+
end
10
+
11
+
test "renders 500.html" do
12
+
assert render_to_string(ElixirBlonkWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
13
+
end
14
+
end
+12
elixir_blonk/test/elixir_blonk_web/controllers/error_json_test.exs
+12
elixir_blonk/test/elixir_blonk_web/controllers/error_json_test.exs
···
1
+
defmodule ElixirBlonkWeb.ErrorJSONTest do
2
+
use ElixirBlonkWeb.ConnCase, async: true
3
+
4
+
test "renders 404" do
5
+
assert ElixirBlonkWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6
+
end
7
+
8
+
test "renders 500" do
9
+
assert ElixirBlonkWeb.ErrorJSON.render("500.json", %{}) ==
10
+
%{errors: %{detail: "Internal Server Error"}}
11
+
end
12
+
end
+8
elixir_blonk/test/elixir_blonk_web/controllers/page_controller_test.exs
+8
elixir_blonk/test/elixir_blonk_web/controllers/page_controller_test.exs
+38
elixir_blonk/test/support/conn_case.ex
+38
elixir_blonk/test/support/conn_case.ex
···
1
+
defmodule ElixirBlonkWeb.ConnCase do
2
+
@moduledoc """
3
+
This module defines the test case to be used by
4
+
tests that require setting up a connection.
5
+
6
+
Such tests rely on `Phoenix.ConnTest` and also
7
+
import other functionality to make it easier
8
+
to build common data structures and query the data layer.
9
+
10
+
Finally, if the test case interacts with the database,
11
+
we enable the SQL sandbox, so changes done to the database
12
+
are reverted at the end of every test. If you are using
13
+
PostgreSQL, you can even run database tests asynchronously
14
+
by setting `use ElixirBlonkWeb.ConnCase, async: true`, although
15
+
this option is not recommended for other databases.
16
+
"""
17
+
18
+
use ExUnit.CaseTemplate
19
+
20
+
using do
21
+
quote do
22
+
# The default endpoint for testing
23
+
@endpoint ElixirBlonkWeb.Endpoint
24
+
25
+
use ElixirBlonkWeb, :verified_routes
26
+
27
+
# Import conveniences for testing with connections
28
+
import Plug.Conn
29
+
import Phoenix.ConnTest
30
+
import ElixirBlonkWeb.ConnCase
31
+
end
32
+
end
33
+
34
+
setup tags do
35
+
ElixirBlonk.DataCase.setup_sandbox(tags)
36
+
{:ok, conn: Phoenix.ConnTest.build_conn()}
37
+
end
38
+
end
+58
elixir_blonk/test/support/data_case.ex
+58
elixir_blonk/test/support/data_case.ex
···
1
+
defmodule ElixirBlonk.DataCase do
2
+
@moduledoc """
3
+
This module defines the setup for tests requiring
4
+
access to the application's data layer.
5
+
6
+
You may define functions here to be used as helpers in
7
+
your tests.
8
+
9
+
Finally, if the test case interacts with the database,
10
+
we enable the SQL sandbox, so changes done to the database
11
+
are reverted at the end of every test. If you are using
12
+
PostgreSQL, you can even run database tests asynchronously
13
+
by setting `use ElixirBlonk.DataCase, async: true`, although
14
+
this option is not recommended for other databases.
15
+
"""
16
+
17
+
use ExUnit.CaseTemplate
18
+
19
+
using do
20
+
quote do
21
+
alias ElixirBlonk.Repo
22
+
23
+
import Ecto
24
+
import Ecto.Changeset
25
+
import Ecto.Query
26
+
import ElixirBlonk.DataCase
27
+
end
28
+
end
29
+
30
+
setup tags do
31
+
ElixirBlonk.DataCase.setup_sandbox(tags)
32
+
:ok
33
+
end
34
+
35
+
@doc """
36
+
Sets up the sandbox based on the test tags.
37
+
"""
38
+
def setup_sandbox(tags) do
39
+
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(ElixirBlonk.Repo, shared: not tags[:async])
40
+
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
41
+
end
42
+
43
+
@doc """
44
+
A helper that transforms changeset errors into a map of messages.
45
+
46
+
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
47
+
assert "password is too short" in errors_on(changeset).password
48
+
assert %{password: ["password is too short"]} = errors_on(changeset)
49
+
50
+
"""
51
+
def errors_on(changeset) do
52
+
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
53
+
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
54
+
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
55
+
end)
56
+
end)
57
+
end
58
+
end