blonk is a radar for your web, where you follow vibes for cool blips on the radar

add skeleton elixir project to clone in sequence while building the typescript app

+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
··· 1 + @import "tailwindcss/base"; 2 + @import "tailwindcss/components"; 3 + @import "tailwindcss/utilities"; 4 + 5 + /* This file is for your main application CSS */
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + defmodule ElixirBlonk do 2 + @moduledoc """ 3 + ElixirBlonk keeps the contexts that define your domain 4 + and business logic. 5 + 6 + Contexts are also responsible for managing your data, regardless 7 + if it comes from the database, an external API or others. 8 + """ 9 + end
+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
··· 1 + defmodule ElixirBlonk.Mailer do 2 + use Swoosh.Mailer, otp_app: :elixir_blonk 3 + end
+5
elixir_blonk/lib/elixir_blonk/repo.ex
··· 1 + defmodule ElixirBlonk.Repo do 2 + use Ecto.Repo, 3 + otp_app: :elixir_blonk, 4 + adapter: Ecto.Adapters.Postgres 5 + end
+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
··· 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
··· 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
··· 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">&rarr;</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
··· 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
··· 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
··· 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
··· 1 + defmodule ElixirBlonkWeb.PageController do 2 + use ElixirBlonkWeb, :controller 3 + 4 + def home(conn, _params) do 5 + # The home page is often custom made, 6 + # so skip the default app layout. 7 + render(conn, :home, layout: false) 8 + end 9 + end
+10
elixir_blonk/lib/elixir_blonk_web/controllers/page_html.ex
··· 1 + defmodule ElixirBlonkWeb.PageHTML do 2 + @moduledoc """ 3 + This module contains pages rendered by PageController. 4 + 5 + See the `page_html` directory for all templates available. 6 + """ 7 + use ElixirBlonkWeb, :html 8 + 9 + embed_templates "page_html/*" 10 + end
+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 &amp; Docs 82 + </span> 83 + </a> 84 + <a 85 + href="https://github.com/phoenixframework/phoenix" 86 + class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" 87 + > 88 + <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105"> 89 + </span> 90 + <span class="relative flex items-center gap-4 sm:flex-col"> 91 + <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6"> 92 + <path 93 + fill-rule="evenodd" 94 + clip-rule="evenodd" 95 + d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z" 96 + fill="#18181B" 97 + /> 98 + </svg> 99 + Source Code 100 + </span> 101 + </a> 102 + <a 103 + href={"https://github.com/phoenixframework/phoenix/blob/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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + [ 2 + import_deps: [:ecto_sql], 3 + inputs: ["*.exs"] 4 + ]
+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

This is a binary file and will not be displayed.

+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
··· 1 + # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 + # 3 + # To ban all spiders from the entire site uncomment the next two lines: 4 + # User-agent: * 5 + # Disallow: /
+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
··· 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
··· 1 + defmodule ElixirBlonkWeb.PageControllerTest do 2 + use ElixirBlonkWeb.ConnCase 3 + 4 + test "GET /", %{conn: conn} do 5 + conn = get(conn, ~p"/") 6 + assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 + end 8 + end
+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
··· 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
+2
elixir_blonk/test/test_helper.exs
··· 1 + ExUnit.start() 2 + Ecto.Adapters.SQL.Sandbox.mode(ElixirBlonk.Repo, :manual)