From e8c7a6f7c09c62454d689c1645fdb7d75404da4e Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Fri, 4 Aug 2023 14:15:15 -0400 Subject: [PATCH 1/8] Merge generated code from phx.new as the baseline for view and liveview based features --- .formatter.exs | 8 +- .gitignore | 4 +- Dockerfile | 3 + assets/css/app.css | 46 +-------- assets/js/app.js | 24 ++++- assets/package-lock.json | 34 ++----- assets/package.json | 5 +- assets/tailwind.config.js | 99 +++++++++++++++++++ config/config.exs | 15 ++- config/dev.exs | 12 ++- config/prod.exs | 3 + config/runtime.exs | 5 +- lib/elixir_boilerplate/application.ex | 1 + .../errors/templates/404.html.heex | 4 +- .../home/templates/header.html.heex | 9 +- .../home/templates/index.html.heex | 4 +- .../home/templates/index_live.html.heex | 4 +- .../layouts/templates/flash.html.heex | 7 +- .../layouts/templates/root.html.heex | 3 +- lib/elixir_boilerplate_web/session.ex | 2 +- lib/elixir_boilerplate_web/telemetry.ex | 91 +++++++++++++++++ mix.exs | 23 +++-- mix.lock | 3 + 23 files changed, 301 insertions(+), 108 deletions(-) create mode 100644 assets/tailwind.config.js create mode 100644 lib/elixir_boilerplate_web/telemetry.ex diff --git a/.formatter.exs b/.formatter.exs index 197a6c9d..6bde019f 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,7 @@ [ - inputs: ["*.exs", "{config,lib,priv,rel,test}/**/*.{ex,exs}"], - line_length: 180, - plugins: [Styler] + import_deps: [:ecto, :ecto_sql, :phoenix], + subdirectories: ["priv/*/migrations"], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"], + line_length: 180 ] diff --git a/.gitignore b/.gitignore index 89a9ec10..cf5eda12 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,10 @@ /db /deps /*.ez -/cover + +# Temporary files /tmp +/cover # Generated on crash by the VM erl_crash.dump diff --git a/Dockerfile b/Dockerfile index fc2d8834..ef453ca1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,9 @@ ENV MIX_ENV="prod" COPY mix.exs mix.lock ./ RUN mix deps.get --only $MIX_ENV +# Setup assets dependencies (Esbuild, Tailwind, etc…) so the are cached +RUN mix assets.setup + # Copy compile-time config files before we compile dependencies # to ensure any relevant config change will trigger the dependencies # to be re-compiled. diff --git a/assets/css/app.css b/assets/css/app.css index 989c539b..a31e4441 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,43 +1,3 @@ -.home { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 40px; - text-align: center; - font-family: - system-ui, - -apple-system, - 'Segoe UI', - Roboto, - 'Helvetica Neue', - Arial, - 'Noto Sans', - 'Liberation Sans', - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - 'Noto Color Emoji'; - line-height: 1.4; -} - -.home a { - display: block; - margin: 0 0 20px; -} - -.home p { - margin: 0 0 20px; -} - -.home p:last-child { - margin-bottom: 0; -} - -.flash-messages { - position: fixed; - top: 0; - right: 0; - padding: 10px; -} +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; diff --git a/assets/js/app.js b/assets/js/app.js index 7a224a63..d8f4b5ff 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,6 +1,18 @@ -import 'simple-css-reset/reset.css'; -import '../css/app.css'; +// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. +import 'phoenix_html'; +// Show progress bar on live navigation and form submits +import topbar from 'topbar'; + +const DELAY_IN_MILISECONDS = 200; + +topbar.config({barColors: {0: '#29d'}, shadowColor: 'rgba(0, 0, 0, .3)'}); +window.addEventListener('phx:page-loading-start', (_info) => + topbar.delayedShow(DELAY_IN_MILISECONDS) +); +window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide()); + +// Establish Phoenix Socket and LiveView configuration. import {Socket} from 'phoenix'; import {LiveSocket} from 'phoenix_live_view'; @@ -29,10 +41,16 @@ Hooks.Flash = { const csrfToken = document .querySelector("meta[name='csrf-token']") .getAttribute('content'); - const liveSocket = new LiveSocket('/live', Socket, { hooks: Hooks, params: {_csrf_token: csrfToken} // eslint-disable-line camelcase }); +// connect if there are any LiveViews on the page liveSocket.connect(); + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket; diff --git a/assets/package-lock.json b/assets/package-lock.json index bf207c1d..dcbad9df 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -8,10 +8,7 @@ "name": "elixir-boilerplate", "version": "0.0.1", "dependencies": { - "phoenix": "^1.7.7", - "phoenix_html": "^3.3.1", - "phoenix_live_view": "^0.20.5", - "simple-css-reset": "^3.0.0" + "topbar": "^2.0.1" }, "devDependencies": { "@babel/eslint-parser": "^7.23.10", @@ -29,11 +26,13 @@ } }, "../deps/phoenix": { - "version": "1.7.7", + "version": "1.7.11", + "extraneous": true, "license": "MIT" }, "../deps/phoenix_html": { - "version": "3.3.2" + "version": "3.3.3", + "extraneous": true }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", @@ -2432,19 +2431,6 @@ "node": ">=8" } }, - "node_modules/phoenix": { - "resolved": "../deps/phoenix", - "link": true - }, - "node_modules/phoenix_html": { - "resolved": "../deps/phoenix_html", - "link": true - }, - "node_modules/phoenix_live_view": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-0.20.5.tgz", - "integrity": "sha512-FgwuGVvanKLs8dZj7k2JBI2fBijcY5DJoFS7A1va4+PAF+R5lqLSwIhHQF1PTtcoV95Av9v9kmj/3yMgTiY9JQ==" - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2771,11 +2757,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-css-reset": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/simple-css-reset/-/simple-css-reset-3.0.0.tgz", - "integrity": "sha512-IN1NRbrCL9pLVBFzzyXmJfkgJAS4b5VwcWFXdpEGMx9asEUZ7AbSnqbLHnB5CvCPa7+uu41aLAsguiUQQvrBmw==" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3189,6 +3170,11 @@ "node": ">=8.0" } }, + "node_modules/topbar": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/topbar/-/topbar-2.0.2.tgz", + "integrity": "sha512-hCKoSaWxXqGIgjag8rIVajysE41as7ti5z9GDO5rcx2zmII1/rY5zvO9IgKwbf50HL82EzlimL6OmPYPUgbpEw==" + }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", diff --git a/assets/package.json b/assets/package.json index c9ef1acc..1cbee6d5 100644 --- a/assets/package.json +++ b/assets/package.json @@ -8,10 +8,7 @@ "npm": "^9.8.0" }, "dependencies": { - "phoenix": "^1.7.7", - "phoenix_html": "^3.3.1", - "phoenix_live_view": "^0.20.5", - "simple-css-reset": "^3.0.0" + "topbar": "^2.0.1" }, "devDependencies": { "@babel/eslint-parser": "^7.23.10", diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 00000000..2e7d5ab3 --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,99 @@ +/* eslint-env node */ + +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration + +const plugin = require('tailwindcss/plugin'); +const fs = require('fs'); +const path = require('path'); + +module.exports = { + content: ['./js/**/*.js', '../lib/*_web.ex', '../lib/*_web/**/*.*ex'], + theme: { + extend: { + colors: { + brand: '#FD4F00' + } + } + }, + plugins: [ + require('@tailwindcss/forms'), + // Allows prefixing tailwind classes with LiveView classes to add rules + // only when LiveView classes are applied, for example: + // + //
+ // + plugin(({addVariant}) => + addVariant('phx-no-feedback', ['.phx-no-feedback&', '.phx-no-feedback &']) + ), + plugin(({addVariant}) => + addVariant('phx-click-loading', [ + '.phx-click-loading&', + '.phx-click-loading &' + ]) + ), + plugin(({addVariant}) => + addVariant('phx-submit-loading', [ + '.phx-submit-loading&', + '.phx-submit-loading &' + ]) + ), + plugin(({addVariant}) => + addVariant('phx-change-loading', [ + '.phx-change-loading&', + '.phx-change-loading &' + ]) + ), + plugin(({addVariant}) => + addVariant('phx-change-loading', [ + '.phx-change-loading&', + '.phx-change-loading &' + ]) + ) + + // Embeds Heroicons (https://heroicons.com) into your app.css bundle + // See your `CoreComponents.icon/1` for more information. + // plugin(({matchComponents, theme}) => { + // const iconsDir = path.join(__dirname, './vendor/heroicons/optimized'); + // const values = {}; + // const icons = [ + // ['', '/24/outline'], + // ['-solid', '/24/solid'], + // ['-mini', '/20/solid'] + // ]; + + // /* eslint max-nested-callbacks: ["error", 3] */ + // icons.forEach(([suffix, dir]) => { + // fs.readdirSync(path.join(iconsDir, dir)).map((file) => { + // const name = path.basename(file, '.svg') + suffix; + + // values[name] = {name, fullPath: path.join(iconsDir, dir, file)}; + // }); + // }); + + // matchComponents( + // { + // hero: ({name, fullPath}) => { + // const content = fs + // .readFileSync(fullPath) + // .toString() + // .replace(/\r?\n|\r/g, ''); + + // return { + // [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + // '-webkit-mask': `var(--hero-${name})`, + // mask: `var(--hero-${name})`, + // 'mask-repeat': 'no-repeat', + // 'background-color': 'currentColor', + // 'vertical-align': 'middle', + // display: 'inline-block', + // width: theme('spacing.5'), + // height: theme('spacing.5') + // }; + // } + // }, + // {values} + // ); + // }) + ] +}; diff --git a/config/config.exs b/config/config.exs index 8832a514..9d28159f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -30,13 +30,24 @@ config :absinthe_security, AbsintheSecurity.Phase.MaxDepthCheck, max_depth_count config :absinthe_security, AbsintheSecurity.Phase.MaxDirectivesCheck, max_directive_count: 100 config :esbuild, - version: "0.16.4", + version: "0.17.11", default: [ - args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), + args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] +config :tailwind, + version: "3.2.7", + default: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] + config :sentry, included_environments: [:all], root_source_code_path: File.cwd!(), diff --git a/config/dev.exs b/config/dev.exs index 10c51388..07282ca2 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -5,13 +5,14 @@ config :elixir_boilerplate, ElixirBoilerplateWeb.Endpoint, debug_errors: true, check_origin: false, watchers: [ - esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} + esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} ], live_reload: [ patterns: [ - ~r{priv/gettext/.*$}, - ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, - ~r{lib/elixir_boilerplate_web/.*(ee?x)$} + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/elixir_boilerplate_web/../.*(ex|heex)$" ] ] @@ -21,3 +22,6 @@ config :logger, :console, format: "[$level] $message\n" config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime + +# Enable dev routes for dashboard and mailbox +config :elixir_boilerplate, dev_routes: true diff --git a/config/prod.exs b/config/prod.exs index 65651460..28f645cf 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -8,3 +8,6 @@ config :logger, :console, format: "$time $metadata[$level] $message\n", level: :info, metadata: ~w(request_id graphql_operation_name)a + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs index a79f2cef..555b2284 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -20,11 +20,14 @@ if get_env("PHX_SERVER", :boolean) == true do config :elixir_boilerplate, ElixirBoilerplateWeb.Endpoint, server: true end +config :elixir_boilerplate, ElixirBoilerplateWeb.Session, + session_key: get_env!("SESSION_KEY"), + session_signing_salt: get_env!("SESSION_SIGNING_SALT") + config :elixir_boilerplate, ElixirBoilerplateWeb.Endpoint, http: [port: get_env!("PORT", :integer)], secret_key_base: get_env!("SECRET_KEY_BASE"), session_key: get_env!("SESSION_KEY"), - session_signing_salt: get_env!("SESSION_SIGNING_SALT"), live_view: [signing_salt: get_env!("SESSION_SIGNING_SALT")], url: get_endpoint_url_config(canonical_uri), static_url: get_endpoint_url_config(static_uri) diff --git a/lib/elixir_boilerplate/application.ex b/lib/elixir_boilerplate/application.ex index e4d125cd..de8505c7 100644 --- a/lib/elixir_boilerplate/application.ex +++ b/lib/elixir_boilerplate/application.ex @@ -7,6 +7,7 @@ defmodule ElixirBoilerplate.Application do def start(_type, _args) do children = [ + ElixirBoilerplateWeb.Telemetry, ElixirBoilerplate.Repo, {Phoenix.PubSub, [name: ElixirBoilerplate.PubSub, adapter: Phoenix.PubSub.PG2]}, ElixirBoilerplateWeb.Endpoint, diff --git a/lib/elixir_boilerplate_web/errors/templates/404.html.heex b/lib/elixir_boilerplate_web/errors/templates/404.html.heex index baa990de..7902e3fb 100644 --- a/lib/elixir_boilerplate_web/errors/templates/404.html.heex +++ b/lib/elixir_boilerplate_web/errors/templates/404.html.heex @@ -1,8 +1,8 @@ - - + + Not found diff --git a/lib/elixir_boilerplate_web/home/templates/header.html.heex b/lib/elixir_boilerplate_web/home/templates/header.html.heex index ae555aa0..7227adda 100644 --- a/lib/elixir_boilerplate_web/home/templates/header.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/header.html.heex @@ -1,5 +1,10 @@ - + -

This repository is the stable base upon which we build our Elixir projects at Mirego.
We want to share it with the world so you can build awesome Elixir applications too.

+

+ This repository is the stable base upon which we build our Elixir projects at Mirego.
We want to share it with the world so you can build awesome Elixir applications too. +

diff --git a/lib/elixir_boilerplate_web/home/templates/index.html.heex b/lib/elixir_boilerplate_web/home/templates/index.html.heex index 5f3e3743..966fe940 100644 --- a/lib/elixir_boilerplate_web/home/templates/index.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index.html.heex @@ -1,4 +1,4 @@
- <.header/> - <.message text={@message}/> + <.header /> + <.message text={@message} />
diff --git a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex index 940e3fb8..d02b88ae 100644 --- a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex @@ -1,6 +1,6 @@
- <.header/> - <.message text={@message}/> + <.header /> + <.message text={@message} />
diff --git a/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex b/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex index 6f2af775..20e98fe7 100644 --- a/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex +++ b/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex @@ -1,8 +1,3 @@ -
to_string(@kind)} - phx-click={hide_flash("#" <> "flash-" <> to_string(@kind))} - phx-hook="Flash" -> +
to_string(@kind)} phx-click={hide_flash("#" <> "flash-" <> to_string(@kind))} phx-hook="Flash"> <%= msg %>
diff --git a/lib/elixir_boilerplate_web/layouts/templates/root.html.heex b/lib/elixir_boilerplate_web/layouts/templates/root.html.heex index 3100e3c8..afa65c97 100644 --- a/lib/elixir_boilerplate_web/layouts/templates/root.html.heex +++ b/lib/elixir_boilerplate_web/layouts/templates/root.html.heex @@ -6,7 +6,8 @@ - + diff --git a/lib/elixir_boilerplate_web/session.ex b/lib/elixir_boilerplate_web/session.ex index 643a721b..08e0d922 100644 --- a/lib/elixir_boilerplate_web/session.ex +++ b/lib/elixir_boilerplate_web/session.ex @@ -9,6 +9,6 @@ defmodule ElixirBoilerplateWeb.Session do end defp app_config(key) do - Keyword.fetch!(Application.get_env(:elixir_boilerplate, ElixirBoilerplateWeb.Endpoint), key) + Keyword.fetch!(Application.get_env(:elixir_boilerplate, __MODULE__), key) end end diff --git a/lib/elixir_boilerplate_web/telemetry.ex b/lib/elixir_boilerplate_web/telemetry.ex new file mode 100644 index 00000000..c5160abc --- /dev/null +++ b/lib/elixir_boilerplate_web/telemetry.ex @@ -0,0 +1,91 @@ +defmodule ElixirBoilerplateWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.start.system_time", + unit: {:native, :millisecond} + ), + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.start.system_time", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.exception.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.socket_connected.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_join.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("elixir_boilerplate.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("elixir_boilerplate.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("elixir_boilerplate.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("elixir_boilerplate.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("elixir_boilerplate.repo.query.idle_time", + unit: {:native, :millisecond}, + description: "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {<%= @web_namespace %>, :count_users, []} + ] + end +end diff --git a/mix.exs b/mix.exs index 6503f4c8..bdde9fd9 100644 --- a/mix.exs +++ b/mix.exs @@ -5,8 +5,8 @@ defmodule ElixirBoilerplate.Mixfile do [ app: :elixir_boilerplate, version: "0.0.1", - erlang: "~> 25.0", - elixir: "~> 1.13", + erlang: "~> 26.0", + elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), test_paths: ["test"], test_pattern: "**/*_test.exs", @@ -32,10 +32,10 @@ defmodule ElixirBoilerplate.Mixfile do defp aliases do [ - "assets.deploy": [ - "esbuild default --minify", - "phx.digest" - ], + setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], + "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["tailwind default", "esbuild default"], + "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"] @@ -46,6 +46,7 @@ defmodule ElixirBoilerplate.Mixfile do [ # Assets bundling {:esbuild, "~> 0.7", runtime: Mix.env() == :dev}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, # HTTP Client {:hackney, "~> 1.18"}, @@ -77,6 +78,10 @@ defmodule ElixirBoilerplate.Mixfile do # Database check {:excellent_migrations, "~> 0.1", only: [:dev, :test], runtime: false}, + # Telemtry plugins + {:telemetry_metrics, "~> 0.6"}, + {:telemetry_poller, "~> 1.0"}, + # Translations {:gettext, "~> 0.22"}, @@ -111,7 +116,11 @@ defmodule ElixirBoilerplate.Mixfile do {:excoveralls, "~> 0.16", only: :test}, # Dialyzer - {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false} + {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, + + # BEAM debugging utilities + {:observer_cli, "~> 1.7"}, + {:recon, "~> 2.5"} ] end diff --git a/mix.lock b/mix.lock index 14c8bb38..f5420411 100644 --- a/mix.lock +++ b/mix.lock @@ -39,6 +39,7 @@ "new_relic_absinthe": {:hex, :new_relic_absinthe, "0.0.4", "57917f99789d9b36e4beb599deba495a474e5bf99a5c70a33717b0e17f1c5d4d", [:mix], [{:absinthe, "~> 1.4", [hex: :absinthe, repo: "hexpm", optional: false]}, {:new_relic_agent, "~> 1.19", [hex: :new_relic_agent, repo: "hexpm", optional: false]}], "hexpm", "6b796662e550ddd07e98ff3df95803a6b2a023605e78e0a45261d3e66341c296"}, "new_relic_agent": {:hex, :new_relic_agent, "1.28.0", "eb015edb4f4887a31ee0488cf9ce97b7e46c9e0208eeff8737d8ea09cd506d09", [:mix], [{:castore, ">= 0.1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ecto, ">= 3.4.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, ">= 3.4.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.5", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.10.4", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.4.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:redix, ">= 0.11.0", [hex: :redix, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef220f429f2d673e78679ec96cd4e8979b2cb2166b204bfe1a43ca974520743a"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [: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", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, @@ -55,11 +56,13 @@ "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [: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", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"}, "sentry": {:hex, :sentry, "9.1.0", "8689b85774003ddcebfd9d48a93bc3f3bf72223983514521aa30645c6f204f86", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "d70c88ab0c6a511594856ae2244d1bd70b8b7a4a42201a3569880f1dd2a3adec"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "styler": {:hex, :styler, "0.11.6", "ad4c5fc1ff72b93107ecb251f595b316c6d97604b742f3473aa036888592b270", [:mix], [], "hexpm", "0b0b9936e91b01a7a9fd7239902581ed1cb5515254357126429a37d1bb3d0078"}, "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, + "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, From 1a1a30e859c8fb4601e98cd67ce0ef249430481d Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Fri, 4 Aug 2023 17:03:40 -0400 Subject: [PATCH 2/8] Adapt the routers, controllers and views to the new component-based structure --- assets/js/app.js | 23 - config/config.exs | 5 +- .../api/version/controller.ex | 8 + .../components/branding.ex | 22 + lib/elixir_boilerplate_web/components/core.ex | 568 ++++++++++++++++++ .../controllers/error_html.ex | 19 + .../controllers/error_json.ex | 15 + .../elixir_boilerplate_web.ex | 111 ++++ lib/elixir_boilerplate_web/endpoint.ex | 26 +- .../errors/templates/404.html.heex | 6 +- .../errors/templates/error_messages.html.heex | 2 +- lib/elixir_boilerplate_web/home/html.ex | 6 +- lib/elixir_boilerplate_web/home/live.ex | 5 +- .../home/templates/header.html.heex | 10 - .../home/templates/home_header.html.heex | 30 + .../home/templates/index.html.heex | 24 +- .../home/templates/index_live.html.heex | 40 +- .../home/templates/message.html.heex | 4 +- lib/elixir_boilerplate_web/layouts/layouts.ex | 10 +- .../layouts/templates/app.html.heex | 6 - .../layouts/templates/flash.html.heex | 3 - .../layouts/templates/live.html.heex | 4 +- .../layouts/templates/root.html.heex | 16 +- lib/elixir_boilerplate_web/plugs/security.ex | 11 +- lib/elixir_boilerplate_web/router.ex | 53 +- 25 files changed, 909 insertions(+), 118 deletions(-) create mode 100644 lib/elixir_boilerplate_web/api/version/controller.ex create mode 100644 lib/elixir_boilerplate_web/components/branding.ex create mode 100644 lib/elixir_boilerplate_web/components/core.ex create mode 100644 lib/elixir_boilerplate_web/controllers/error_html.ex create mode 100644 lib/elixir_boilerplate_web/controllers/error_json.ex create mode 100644 lib/elixir_boilerplate_web/elixir_boilerplate_web.ex delete mode 100644 lib/elixir_boilerplate_web/home/templates/header.html.heex create mode 100644 lib/elixir_boilerplate_web/home/templates/home_header.html.heex delete mode 100644 lib/elixir_boilerplate_web/layouts/templates/flash.html.heex diff --git a/assets/js/app.js b/assets/js/app.js index d8f4b5ff..13fa399b 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -16,33 +16,10 @@ window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide()); import {Socket} from 'phoenix'; import {LiveSocket} from 'phoenix_live_view'; -const FLASH_TTL = 8000; -const Hooks = {}; - -Hooks.Flash = { - mounted() { - this.timer = setTimeout(() => this._hide(), FLASH_TTL); - - this.el.addEventListener('mouseover', () => { - clearTimeout(this.timer); - this.timer = setTimeout(() => this._hide(), FLASH_TTL); - }); - }, - - destroyed() { - clearTimeout(this.timer); - }, - - _hide() { - liveSocket.execJS(this.el, this.el.getAttribute('phx-click')); - } -}; - const csrfToken = document .querySelector("meta[name='csrf-token']") .getAttribute('content'); const liveSocket = new LiveSocket('/live', Socket, { - hooks: Hooks, params: {_csrf_token: csrfToken} // eslint-disable-line camelcase }); diff --git a/config/config.exs b/config/config.exs index 9d28159f..108d2233 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,7 +10,10 @@ config :phoenix, :json_library, Jason config :elixir_boilerplate, ElixirBoilerplateWeb.Endpoint, pubsub_server: ElixirBoilerplate.PubSub, - render_errors: [view: ElixirBoilerplateWeb.Errors, accepts: ~w(html json)] + render_errors: [ + formats: [html: ElixirBoilerplateWeb.Controllers.ErrorHTML, json: ElixirBoilerplateWeb.Controllers.ErrorJSON], + layout: false + ] config :elixir_boilerplate, ElixirBoilerplate.Repo, migration_primary_key: [type: :binary_id, default: {:fragment, "gen_random_uuid()"}], diff --git a/lib/elixir_boilerplate_web/api/version/controller.ex b/lib/elixir_boilerplate_web/api/version/controller.ex new file mode 100644 index 00000000..fc1fabd1 --- /dev/null +++ b/lib/elixir_boilerplate_web/api/version/controller.ex @@ -0,0 +1,8 @@ +defmodule ElixirBoilerplateWeb.Api.Version.Controller do + use ElixirBoilerplateWeb, :controller + + @spec index(Plug.Conn.t(), map) :: Plug.Conn.t() + def index(conn, _) do + json(conn, %{version: Application.get_env(:elixir_boilerplate, :version)}) + end +end diff --git a/lib/elixir_boilerplate_web/components/branding.ex b/lib/elixir_boilerplate_web/components/branding.ex new file mode 100644 index 00000000..220e196f --- /dev/null +++ b/lib/elixir_boilerplate_web/components/branding.ex @@ -0,0 +1,22 @@ +defmodule ElixirBoilerplateWeb.Components.Branding do + @moduledoc """ + Provides branding UI components. + """ + use Phoenix.Component + + @doc """ + Renders the boilerplate logo as an inlined SVG. + ## Examples + <.logo width=500 /> + """ + attr :width, :integer, default: 500 + + def logo(assigns) do + ~H""" + + """ + end +end diff --git a/lib/elixir_boilerplate_web/components/core.ex b/lib/elixir_boilerplate_web/components/core.ex new file mode 100644 index 00000000..56fd82b5 --- /dev/null +++ b/lib/elixir_boilerplate_web/components/core.ex @@ -0,0 +1,568 @@ +defmodule ElixirBoilerplateWeb.Components.Core do + @moduledoc """ + Provides core UI components. + At the first glance, this module may seem daunting, but its goal is + to provide some core building blocks in your application, such modals, + tables, and forms. The components are mostly markup and well documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + The default components use Tailwind CSS, a utility-first CSS framework. + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn + how to customize them or feel free to swap in another framework altogether. + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + import ElixirBoilerplate.Gettext + + @doc """ + Renders a modal. + ## Examples + <.modal id="confirm-modal"> + This is a modal. + + JS commands may be passed to the `:on_cancel` to configure + the closing/cancel event, for example: + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> + This is another modal. + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + slot :inner_block, required: true + + def modal(assigns) do + ~H""" + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + @doc """ + Renders a label. + """ + attr :for, :string, default: nil + slot :inner_block, required: true + + def label(assigns) do + ~H""" + + """ + end + + @doc """ + Generates a generic error message. + """ + slot :inner_block, required: true + + def error(assigns) do + ~H""" +

+ <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> + <%= render_slot(@inner_block) %> +

+ """ + end + + @doc """ + Renders a header with title. + """ + attr :class, :string, default: nil + + slot :inner_block, required: true + slot :subtitle + slot :actions + + def header(assigns) do + ~H""" +
+
+

+ <%= render_slot(@inner_block) %> +

+

+ <%= render_slot(@subtitle) %> +

+
+
<%= render_slot(@actions) %>
+
+ """ + end + + @doc ~S""" + Renders a table with generic styling. + ## Examples + <.table id="users" rows={@users}> + <:col :let={user} label="id"><%= user.id %> + <:col :let={user} label="username"><%= user.username %> + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" +
+ + + + + + + + + + + + + +
<%= col[:label] %><%= gettext("Actions") %>
+
+ + + <%= render_slot(col, @row_item.(row)) %> + +
+
+
+ + + <%= render_slot(action, @row_item.(row)) %> + +
+
+
+ """ + end + + @doc """ + Renders a data list. + ## Examples + <.list> + <:item title="Title"><%= @post.title %> + <:item title="Views"><%= @post.views %> + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
+
+
+
<%= item.title %>
+
<%= render_slot(item) %>
+
+
+
+ """ + end + + @doc """ + Renders a back navigation link. + ## Examples + <.back navigate={~p"/posts"}>Back to posts + """ + attr :navigate, :any, required: true + slot :inner_block, required: true + + def back(assigns) do + ~H""" +
+ <.link navigate={@navigate} class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"> + <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> + <%= render_slot(@inner_block) %> + +
+ """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + You can customize the size and colors of the icons by setting + width, height, and background color classes. + Icons are extracted from your `assets/vendor/heroicons` directory and bundled + within your compiled app.css by the plugin in your `assets/tailwind.config.js`. + ## Examples + <.icon name="hero-x-mark-solid" /> + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :string, default: nil + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + transition: {"transition-all transform ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", "opacity-100 translate-y-0 sm:scale-100"} + ) + end + + def hide(js \\ %JS{}, selector) do + JS.hide(js, + to: selector, + time: 200, + transition: {"transition-all transform ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} + ) + end + + def show_modal(js \\ %JS{}, id) when is_binary(id) do + js + |> JS.show(to: "##{id}") + |> JS.show( + to: "##{id}-bg", + transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} + ) + |> show("##{id}-container") + |> JS.add_class("overflow-hidden", to: "body") + |> JS.focus_first(to: "##{id}-content") + end + + def hide_modal(js \\ %JS{}, id) do + js + |> JS.hide( + to: "##{id}-bg", + transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} + ) + |> hide("##{id}-container") + |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) + |> JS.remove_class("overflow-hidden", to: "body") + |> JS.pop_focus() + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(ElixirBoilerplate.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(ElixirBoilerplate.Gettext, "errors", msg, opts) + end + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end diff --git a/lib/elixir_boilerplate_web/controllers/error_html.ex b/lib/elixir_boilerplate_web/controllers/error_html.ex new file mode 100644 index 00000000..1ecc5be2 --- /dev/null +++ b/lib/elixir_boilerplate_web/controllers/error_html.ex @@ -0,0 +1,19 @@ +defmodule ElixirBoilerplateWeb.Controllers.ErrorHTML do + use ElixirBoilerplateWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/elixir_boilerplate_web/controllers/error_html/404.html.heex + # * lib/elixir_boilerplate_web/controllers/error_html/500.html.heex + # + # embed_templates "error_html/*" + + # The default is to render a plain text page based on + # the template name. For example, "404.html" becomes + # "Not Found". + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/lib/elixir_boilerplate_web/controllers/error_json.ex b/lib/elixir_boilerplate_web/controllers/error_json.ex new file mode 100644 index 00000000..0ccb0a67 --- /dev/null +++ b/lib/elixir_boilerplate_web/controllers/error_json.ex @@ -0,0 +1,15 @@ +defmodule ElixirBoilerplateWeb.Controllers.ErrorJSON do + # If you want to customize a particular status code, + # you may add your own clauses, such as: + # + # def render("500.json", _assigns) do + # %{errors: %{detail: "Internal Server Error"}} + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.json" becomes + # "Not Found". + def render(template, _assigns) do + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} + end +end diff --git a/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex b/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex new file mode 100644 index 00000000..85adc10c --- /dev/null +++ b/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex @@ -0,0 +1,111 @@ +defmodule ElixirBoilerplateWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + This can be used in your application as: + use ElixirBoilerplateWeb, :controller + use ElixirBoilerplateWeb, :html + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + namespace: ElixirBoilerplateWeb, + formats: [:html, :json], + layouts: [html: ElixirBoilerplateWeb.Layouts] + + import Plug.Conn + import ElixirBoilerplate.Gettext + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {ElixirBoilerplateWeb.Layouts, :live} + + unquote(html_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # HTML escaping functionality + import Phoenix.HTML + # Core UI components and translation + import ElixirBoilerplate.Gettext + import ElixirBoilerplateWeb.Components.Branding + import ElixirBoilerplateWeb.Components.Core + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: ElixirBoilerplateWeb.Endpoint, + router: ElixirBoilerplateWeb.Router, + statics: ElixirBoilerplateWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/elixir_boilerplate_web/endpoint.ex b/lib/elixir_boilerplate_web/endpoint.ex index 2d597929..a52106b2 100644 --- a/lib/elixir_boilerplate_web/endpoint.ex +++ b/lib/elixir_boilerplate_web/endpoint.ex @@ -7,9 +7,14 @@ defmodule ElixirBoilerplateWeb.Endpoint do @plug_ssl Plug.SSL.init(rewrite_on: [:x_forwarded_proto], subdomains: true) socket("/socket", ElixirBoilerplateWeb.Socket) - socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: {ElixirBoilerplateWeb.Session, :config, []}]]) - plug(ElixirBoilerplateWeb.Plugs.Security) + socket "/live", Phoenix.LiveView.Socket, + websocket: [ + connect_info: [ + session: {ElixirBoilerplateWeb.Session, :config, []} + ] + ] + plug(:ping) plug(:canonical_host) plug(:force_ssl) @@ -24,7 +29,7 @@ defmodule ElixirBoilerplateWeb.Endpoint do at: "/", from: :elixir_boilerplate, gzip: true, - only: ~w(assets fonts images favicon.ico robots.txt) + only: ElixirBoilerplateWeb.static_paths() ) # Code reloading can be explicitly enabled under the @@ -48,9 +53,8 @@ defmodule ElixirBoilerplateWeb.Endpoint do plug(Plug.MethodOverride) plug(Plug.Head) - plug(ElixirBoilerplateHealth.Router) - plug(ElixirBoilerplateGraphQL.Router) - plug(:halt_if_sent) + plug(:session) + plug(ElixirBoilerplateWeb.Router) @doc """ @@ -115,9 +119,9 @@ defmodule ElixirBoilerplateWeb.Endpoint do end end - # Splitting routers in separate modules has a negative side effect: - # Phoenix.Router does not check the Plug.Conn state and tries to match the - # route even if it was already handled/sent by another router. - defp halt_if_sent(%{state: :sent, halted: false} = conn, _opts), do: halt(conn) - defp halt_if_sent(conn, _opts), do: conn + defp session(conn, _opts) do + opts = Plug.Session.init(ElixirBoilerplateWeb.Session.config()) + + Plug.Session.call(conn, opts) + end end diff --git a/lib/elixir_boilerplate_web/errors/templates/404.html.heex b/lib/elixir_boilerplate_web/errors/templates/404.html.heex index 7902e3fb..63d966bf 100644 --- a/lib/elixir_boilerplate_web/errors/templates/404.html.heex +++ b/lib/elixir_boilerplate_web/errors/templates/404.html.heex @@ -1,11 +1,11 @@ - - + + Not found

Sorry, the page you are looking for does not exist.

- + \ No newline at end of file diff --git a/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex index 56d46a2a..64432ee8 100644 --- a/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex +++ b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex @@ -4,4 +4,4 @@
  • <%= error %>
  • <% end %> -<% end %> +<% end %> \ No newline at end of file diff --git a/lib/elixir_boilerplate_web/home/html.ex b/lib/elixir_boilerplate_web/home/html.ex index 615a3b45..b0834983 100644 --- a/lib/elixir_boilerplate_web/home/html.ex +++ b/lib/elixir_boilerplate_web/home/html.ex @@ -1,13 +1,11 @@ defmodule ElixirBoilerplateWeb.Home.HTML do - use Phoenix.Component + use ElixirBoilerplateWeb, :html embed_templates("templates/*") def render("index.html", assigns), do: index(assigns) attr(:text, :string, required: true) + attr(:class, :string, default: nil) def message(assigns) - - attr(:url, :string, default: "https://github.com/mirego/elixir-boilerplate") - def header(assigns) end diff --git a/lib/elixir_boilerplate_web/home/live.ex b/lib/elixir_boilerplate_web/home/live.ex index 199d6a71..11b294c6 100644 --- a/lib/elixir_boilerplate_web/home/live.ex +++ b/lib/elixir_boilerplate_web/home/live.ex @@ -1,6 +1,5 @@ defmodule ElixirBoilerplateWeb.Home.Live do - @moduledoc false - use Phoenix.LiveView, layout: {ElixirBoilerplateWeb.Layouts, :live} + use ElixirBoilerplateWeb, :live_view def mount(_, _, socket) do socket = assign(socket, :message, "Hello, world!") @@ -22,7 +21,7 @@ defmodule ElixirBoilerplateWeb.Home.Live do end def handle_event("add_flash_success", _, socket) do - socket = put_flash(socket, :success, "Success: #{DateTime.utc_now()}") + socket = put_flash(socket, :info, "Success: #{DateTime.utc_now()}") {:noreply, socket} end diff --git a/lib/elixir_boilerplate_web/home/templates/header.html.heex b/lib/elixir_boilerplate_web/home/templates/header.html.heex deleted file mode 100644 index 7227adda..00000000 --- a/lib/elixir_boilerplate_web/home/templates/header.html.heex +++ /dev/null @@ -1,10 +0,0 @@ - - - - -

    - This repository is the stable base upon which we build our Elixir projects at Mirego.
    We want to share it with the world so you can build awesome Elixir applications too. -

    diff --git a/lib/elixir_boilerplate_web/home/templates/home_header.html.heex b/lib/elixir_boilerplate_web/home/templates/home_header.html.heex new file mode 100644 index 00000000..136270a5 --- /dev/null +++ b/lib/elixir_boilerplate_web/home/templates/home_header.html.heex @@ -0,0 +1,30 @@ +
    +
    +
    + + + + +

    + v<%= Application.spec(:phoenix, :vsn) %> +

    +
    + + +
    +
    diff --git a/lib/elixir_boilerplate_web/home/templates/index.html.heex b/lib/elixir_boilerplate_web/home/templates/index.html.heex index 966fe940..915227c0 100644 --- a/lib/elixir_boilerplate_web/home/templates/index.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index.html.heex @@ -1,4 +1,20 @@ -
    - <.header /> - <.message text={@message} /> -
    +<.home_header /> + +
    + <.flash_group flash={@flash} /> + +
    +
    + + <.logo width={500} /> + + +

    + This repository is the stable base upon which we build our Elixir projects at Mirego.
    + We want to share it with the world so you can build awesome Elixir applications too. +

    + + <.message class="pt-8" text={@message} /> +
    +
    +
    diff --git a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex index d02b88ae..918db5be 100644 --- a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex @@ -1,17 +1,31 @@ -
    - <.header /> - <.message text={@message} /> +<.home_header /> -
    +
    + <.flash_group flash={@flash} /> -
    - - <%= @counter %> - -
    +
    +
    + + <.logo width={500} /> + + +

    + This repository is the stable base upon which we build our Elixir projects at Mirego.
    + We want to share it with the world so you can build awesome Elixir applications too. +

    -
    + <.message class="pt-8" text={@message} /> +
    - - -
    +
    + <.button type="button" phx-click="decrement_counter">- + <%= @counter %> + <.button type="button" phx-click="increment_counter">+ +
    + +
    + <.button phx-click="add_flash_success">Add flash success + <.button phx-click="add_flash_error">Add flash error +
    +
    + diff --git a/lib/elixir_boilerplate_web/home/templates/message.html.heex b/lib/elixir_boilerplate_web/home/templates/message.html.heex index 55f309cf..94e172c7 100644 --- a/lib/elixir_boilerplate_web/home/templates/message.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/message.html.heex @@ -1 +1,3 @@ -

    Message: <%= @text %>

    +

    + Message: <%= @text %> +

    diff --git a/lib/elixir_boilerplate_web/layouts/layouts.ex b/lib/elixir_boilerplate_web/layouts/layouts.ex index cd2164b5..d3c6f328 100644 --- a/lib/elixir_boilerplate_web/layouts/layouts.ex +++ b/lib/elixir_boilerplate_web/layouts/layouts.ex @@ -1,16 +1,8 @@ defmodule ElixirBoilerplateWeb.Layouts do - @moduledoc false - use Phoenix.Component - - alias ElixirBoilerplateWeb.Router.Helpers, as: Routes - alias Phoenix.LiveView.JS + use ElixirBoilerplateWeb, :html embed_templates("templates/*") - attr(:flash, :map, required: true) - attr(:kind, :atom, required: true) - def flash(assigns) - def hide_flash(id) do "lv:clear-flash" |> JS.push() diff --git a/lib/elixir_boilerplate_web/layouts/templates/app.html.heex b/lib/elixir_boilerplate_web/layouts/templates/app.html.heex index 0d09a6c5..ff3178bf 100644 --- a/lib/elixir_boilerplate_web/layouts/templates/app.html.heex +++ b/lib/elixir_boilerplate_web/layouts/templates/app.html.heex @@ -1,9 +1,3 @@
    -
    - <.flash flash={@flash} kind={:success} /> - <.flash flash={@flash} kind={:error} /> - <.flash flash={@flash} kind={:info} /> -
    - <%= @inner_content %>
    diff --git a/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex b/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex deleted file mode 100644 index 20e98fe7..00000000 --- a/lib/elixir_boilerplate_web/layouts/templates/flash.html.heex +++ /dev/null @@ -1,3 +0,0 @@ -
    to_string(@kind)} phx-click={hide_flash("#" <> "flash-" <> to_string(@kind))} phx-hook="Flash"> - <%= msg %> -
    diff --git a/lib/elixir_boilerplate_web/layouts/templates/live.html.heex b/lib/elixir_boilerplate_web/layouts/templates/live.html.heex index 0d09a6c5..d2cdafd8 100644 --- a/lib/elixir_boilerplate_web/layouts/templates/live.html.heex +++ b/lib/elixir_boilerplate_web/layouts/templates/live.html.heex @@ -1,8 +1,6 @@
    - <.flash flash={@flash} kind={:success} /> - <.flash flash={@flash} kind={:error} /> - <.flash flash={@flash} kind={:info} /> + <.flash_group flash={@flash} />
    <%= @inner_content %> diff --git a/lib/elixir_boilerplate_web/layouts/templates/root.html.heex b/lib/elixir_boilerplate_web/layouts/templates/root.html.heex index afa65c97..623935bc 100644 --- a/lib/elixir_boilerplate_web/layouts/templates/root.html.heex +++ b/lib/elixir_boilerplate_web/layouts/templates/root.html.heex @@ -2,15 +2,19 @@ - - - - - - + <%= @inner_content %> diff --git a/lib/elixir_boilerplate_web/plugs/security.ex b/lib/elixir_boilerplate_web/plugs/security.ex index 315c55dd..2837fdf8 100644 --- a/lib/elixir_boilerplate_web/plugs/security.ex +++ b/lib/elixir_boilerplate_web/plugs/security.ex @@ -33,13 +33,20 @@ defmodule ElixirBoilerplateWeb.Plugs.Security do defp media_src_directive, do: "'self'" defp font_src_directive, do: "'self'" defp connect_src_directive, do: "'self'" - defp style_src_directive, do: "'self' 'unsafe-inline'" defp frame_src_directive, do: "'self'" defp image_src_directive, do: "'self' data:" + defp style_src_directive do + if Application.get_env(:elixir_boilerplate, __MODULE__)[:allow_unsafe_scripts] do + "'self' 'unsafe-inline' cdn.jsdelivr.net" + else + "'self'" + end + end + defp script_src_directive do if Application.get_env(:elixir_boilerplate, __MODULE__)[:allow_unsafe_scripts] do - "'self' 'unsafe-eval' 'unsafe-inline'" + "'self' 'unsafe-eval' 'unsafe-inline' cdn.jsdelivr.net" else "'self'" end diff --git a/lib/elixir_boilerplate_web/router.ex b/lib/elixir_boilerplate_web/router.ex index 06e3affd..9ceac6d1 100644 --- a/lib/elixir_boilerplate_web/router.ex +++ b/lib/elixir_boilerplate_web/router.ex @@ -1,5 +1,5 @@ defmodule ElixirBoilerplateWeb.Router do - use Phoenix.Router + use Phoenix.Router, helpers: false import Phoenix.LiveView.Router @@ -11,20 +11,24 @@ defmodule ElixirBoilerplateWeb.Router do json_decoder: Phoenix.json_library() ) - plug(:accepts, ["html", "json"]) + plug(:accepts, ~w[html json]) - plug(:session) plug(:fetch_session) + plug(:fetch_live_flash) plug(:protect_from_forgery) - plug(:fetch_live_flash) + plug(ElixirBoilerplateWeb.Plugs.Security) plug(:put_layout, {ElixirBoilerplateWeb.Layouts, :app}) plug(:put_root_layout, {ElixirBoilerplateWeb.Layouts, :root}) end + pipeline :api do + plug(:accepts, ~w[json]) + end + scope "/" do - pipe_through(:browser) + pipe_through :browser # To enable metrics dashboard use `telemetry_ui_allowed: true` as assigns value # @@ -34,22 +38,41 @@ defmodule ElixirBoilerplateWeb.Router do end scope "/", ElixirBoilerplateWeb do - pipe_through(:browser) + pipe_through :browser get("/", Home.Controller, :index, as: :home) + live("/live", Home.Live, :index, as: :live_home) end - scope "/", ElixirBoilerplateWeb do - pipe_through(:browser) + scope "/api", ElixirBoilerplateWeb.Api do + pipe_through :api - live("/live", Home.Live, :index, as: :live_home) + get("/version", Version.Controller, :index, as: :version) end - # The session will be stored in the cookie and signed, - # this means its contents can be read but not tampered with. - # Set :encryption_salt if you would also like to encrypt it. - defp session(conn, _opts) do - opts = Plug.Session.init(ElixirBoilerplateWeb.Session.config()) - Plug.Session.call(conn, opts) + scope "/" do + pipe_through :api + + forward("/graphql", Absinthe.Plug, schema: ElixirBoilerplateGraphQL.Schema) + + if Mix.env() == :dev do + forward("/graphiql", Absinthe.Plug.GraphiQL, + schema: ElixirBoilerplateGraphQL.Schema, + socket: ElixirBoilerplateWeb.Socket, + interface: :playground + ) + end end + + forward( + "/health", + PlugCheckup, + PlugCheckup.Options.new( + json_encoder: Phoenix.json_library(), + checks: ElixirBoilerplateHealth.checks(), + error_code: ElixirBoilerplateHealth.error_code(), + timeout: :timer.seconds(5), + pretty: false + ) + ) end From 47ba7935ad1e14f265f9d160949f0b9c0f17008d Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Thu, 7 Sep 2023 13:44:19 -0400 Subject: [PATCH 3/8] Comment heroicons configuration from Tailwind for now --- assets/tailwind.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 2e7d5ab3..148264b0 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -4,8 +4,8 @@ // https://tailwindcss.com/docs/configuration const plugin = require('tailwindcss/plugin'); -const fs = require('fs'); -const path = require('path'); +// const fs = require('fs'); +// const path = require('path'); module.exports = { content: ['./js/**/*.js', '../lib/*_web.ex', '../lib/*_web/**/*.*ex'], From aa10254a0d75b2242fc8b2e17e0b11d6ad79fdd0 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Thu, 7 Sep 2023 14:12:36 -0400 Subject: [PATCH 4/8] Fix tests and format --- .../{controllers => errors}/error_html.ex | 2 +- .../{controllers => errors}/error_json.ex | 2 +- .../errors/templates/404.html.heex | 6 +++--- .../errors/templates/error_messages.html.heex | 2 +- .../errors/error_html_test.exs | 16 ++++++++++++++++ .../errors/error_json_test.exs | 13 +++++++++++++ .../{ => errors}/errors_test.exs | 4 +--- 7 files changed, 36 insertions(+), 9 deletions(-) rename lib/elixir_boilerplate_web/{controllers => errors}/error_html.ex (91%) rename lib/elixir_boilerplate_web/{controllers => errors}/error_json.ex (89%) create mode 100644 test/elixir_boilerplate_web/errors/error_html_test.exs create mode 100644 test/elixir_boilerplate_web/errors/error_json_test.exs rename test/elixir_boilerplate_web/{ => errors}/errors_test.exs (96%) diff --git a/lib/elixir_boilerplate_web/controllers/error_html.ex b/lib/elixir_boilerplate_web/errors/error_html.ex similarity index 91% rename from lib/elixir_boilerplate_web/controllers/error_html.ex rename to lib/elixir_boilerplate_web/errors/error_html.ex index 1ecc5be2..ed649f2d 100644 --- a/lib/elixir_boilerplate_web/controllers/error_html.ex +++ b/lib/elixir_boilerplate_web/errors/error_html.ex @@ -1,4 +1,4 @@ -defmodule ElixirBoilerplateWeb.Controllers.ErrorHTML do +defmodule ElixirBoilerplateWeb.Errors.ErrorHTML do use ElixirBoilerplateWeb, :html # If you want to customize your error pages, diff --git a/lib/elixir_boilerplate_web/controllers/error_json.ex b/lib/elixir_boilerplate_web/errors/error_json.ex similarity index 89% rename from lib/elixir_boilerplate_web/controllers/error_json.ex rename to lib/elixir_boilerplate_web/errors/error_json.ex index 0ccb0a67..7d916576 100644 --- a/lib/elixir_boilerplate_web/controllers/error_json.ex +++ b/lib/elixir_boilerplate_web/errors/error_json.ex @@ -1,4 +1,4 @@ -defmodule ElixirBoilerplateWeb.Controllers.ErrorJSON do +defmodule ElixirBoilerplateWeb.Errors.ErrorJSON do # If you want to customize a particular status code, # you may add your own clauses, such as: # diff --git a/lib/elixir_boilerplate_web/errors/templates/404.html.heex b/lib/elixir_boilerplate_web/errors/templates/404.html.heex index 63d966bf..7902e3fb 100644 --- a/lib/elixir_boilerplate_web/errors/templates/404.html.heex +++ b/lib/elixir_boilerplate_web/errors/templates/404.html.heex @@ -1,11 +1,11 @@ - - + + Not found

    Sorry, the page you are looking for does not exist.

    - \ No newline at end of file + diff --git a/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex index 64432ee8..56d46a2a 100644 --- a/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex +++ b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex @@ -4,4 +4,4 @@
  • <%= error %>
  • <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/test/elixir_boilerplate_web/errors/error_html_test.exs b/test/elixir_boilerplate_web/errors/error_html_test.exs new file mode 100644 index 00000000..7591a646 --- /dev/null +++ b/test/elixir_boilerplate_web/errors/error_html_test.exs @@ -0,0 +1,16 @@ +defmodule ElixirBoilerplateWeb.ErrorHtmlTest do + use ElixirBoilerplate.DataCase, async: true + + # Bring render_to_string/3 for testing custom views + import Phoenix.Template + + alias ElixirBoilerplateWeb.Errors.ErrorHTML + + test "renders 404.html" do + assert render_to_string(ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/test/elixir_boilerplate_web/errors/error_json_test.exs b/test/elixir_boilerplate_web/errors/error_json_test.exs new file mode 100644 index 00000000..5d8f4571 --- /dev/null +++ b/test/elixir_boilerplate_web/errors/error_json_test.exs @@ -0,0 +1,13 @@ +defmodule ElixirBoilerplateWeb.ErrorJsonTest do + use ElixirBoilerplate.DataCase, async: true + + alias ElixirBoilerplateWeb.Errors.ErrorJSON + + test "renders 404" do + assert ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert ErrorJSON.render("500.json", %{}) == %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/test/elixir_boilerplate_web/errors_test.exs b/test/elixir_boilerplate_web/errors/errors_test.exs similarity index 96% rename from test/elixir_boilerplate_web/errors_test.exs rename to test/elixir_boilerplate_web/errors/errors_test.exs index 1c407caa..d2be3886 100644 --- a/test/elixir_boilerplate_web/errors_test.exs +++ b/test/elixir_boilerplate_web/errors/errors_test.exs @@ -1,8 +1,6 @@ defmodule ElixirBoilerplateWeb.ErrorsTest do use ElixirBoilerplate.DataCase, async: true - alias ElixirBoilerplateWeb.Errors - defmodule UserRole do @moduledoc false use Ecto.Schema @@ -76,7 +74,7 @@ defmodule ElixirBoilerplateWeb.ErrorsTest do defp changeset_to_error_messages(changeset) do changeset - |> Errors.changeset_to_error_messages() + |> ElixirBoilerplateWeb.Errors.changeset_to_error_messages() |> Phoenix.HTML.Safe.to_iodata() |> IO.iodata_to_binary() end From 10ae4493fc7cfb9850600db8758de389d95444f5 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Thu, 7 Sep 2023 14:40:06 -0400 Subject: [PATCH 5/8] Work around credo warning for error test modules --- test/elixir_boilerplate_web/errors/error_html_test.exs | 3 ++- test/elixir_boilerplate_web/errors/error_json_test.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/elixir_boilerplate_web/errors/error_html_test.exs b/test/elixir_boilerplate_web/errors/error_html_test.exs index 7591a646..f0a83732 100644 --- a/test/elixir_boilerplate_web/errors/error_html_test.exs +++ b/test/elixir_boilerplate_web/errors/error_html_test.exs @@ -1,4 +1,5 @@ -defmodule ElixirBoilerplateWeb.ErrorHtmlTest do +# credo:disable-for-this-line CredoNaming.Check.Consistency.ModuleFilename +defmodule ElixirBoilerplateWeb.Errors.ErrorHtmlTest do use ElixirBoilerplate.DataCase, async: true # Bring render_to_string/3 for testing custom views diff --git a/test/elixir_boilerplate_web/errors/error_json_test.exs b/test/elixir_boilerplate_web/errors/error_json_test.exs index 5d8f4571..27814a14 100644 --- a/test/elixir_boilerplate_web/errors/error_json_test.exs +++ b/test/elixir_boilerplate_web/errors/error_json_test.exs @@ -1,4 +1,5 @@ -defmodule ElixirBoilerplateWeb.ErrorJsonTest do +# credo:disable-for-this-file CredoNaming.Check.Consistency.ModuleFilename +defmodule ElixirBoilerplateWeb.Errors.ErrorJsonTest do use ElixirBoilerplate.DataCase, async: true alias ElixirBoilerplateWeb.Errors.ErrorJSON From e10303281d07129cc77769d317fb5c52c1a25689 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Fri, 8 Sep 2023 11:42:24 -0400 Subject: [PATCH 6/8] Remove topbar, Telemetry and other unnecessary elements --- assets/js/app.js | 9 -- assets/package-lock.json | 8 -- assets/package.json | 3 - lib/elixir_boilerplate/application.ex | 1 - .../home/templates/home_header.html.heex | 30 ------ .../home/templates/index.html.heex | 2 - .../home/templates/index_live.html.heex | 2 - lib/elixir_boilerplate_web/plugs/security.ex | 4 +- lib/elixir_boilerplate_web/telemetry.ex | 91 ------------------- mix.exs | 4 - 10 files changed, 2 insertions(+), 152 deletions(-) delete mode 100644 lib/elixir_boilerplate_web/home/templates/home_header.html.heex delete mode 100644 lib/elixir_boilerplate_web/telemetry.ex diff --git a/assets/js/app.js b/assets/js/app.js index 13fa399b..58f14bdd 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,17 +1,8 @@ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import 'phoenix_html'; -// Show progress bar on live navigation and form submits -import topbar from 'topbar'; - const DELAY_IN_MILISECONDS = 200; -topbar.config({barColors: {0: '#29d'}, shadowColor: 'rgba(0, 0, 0, .3)'}); -window.addEventListener('phx:page-loading-start', (_info) => - topbar.delayedShow(DELAY_IN_MILISECONDS) -); -window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide()); - // Establish Phoenix Socket and LiveView configuration. import {Socket} from 'phoenix'; import {LiveSocket} from 'phoenix_live_view'; diff --git a/assets/package-lock.json b/assets/package-lock.json index dcbad9df..6dadc0ef 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -7,9 +7,6 @@ "": { "name": "elixir-boilerplate", "version": "0.0.1", - "dependencies": { - "topbar": "^2.0.1" - }, "devDependencies": { "@babel/eslint-parser": "^7.23.10", "eslint": "^8.56.0", @@ -3170,11 +3167,6 @@ "node": ">=8.0" } }, - "node_modules/topbar": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/topbar/-/topbar-2.0.2.tgz", - "integrity": "sha512-hCKoSaWxXqGIgjag8rIVajysE41as7ti5z9GDO5rcx2zmII1/rY5zvO9IgKwbf50HL82EzlimL6OmPYPUgbpEw==" - }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", diff --git a/assets/package.json b/assets/package.json index 1cbee6d5..a2e44ce1 100644 --- a/assets/package.json +++ b/assets/package.json @@ -7,9 +7,6 @@ "node": "^20.5.0", "npm": "^9.8.0" }, - "dependencies": { - "topbar": "^2.0.1" - }, "devDependencies": { "@babel/eslint-parser": "^7.23.10", "eslint": "^8.56.0", diff --git a/lib/elixir_boilerplate/application.ex b/lib/elixir_boilerplate/application.ex index de8505c7..e4d125cd 100644 --- a/lib/elixir_boilerplate/application.ex +++ b/lib/elixir_boilerplate/application.ex @@ -7,7 +7,6 @@ defmodule ElixirBoilerplate.Application do def start(_type, _args) do children = [ - ElixirBoilerplateWeb.Telemetry, ElixirBoilerplate.Repo, {Phoenix.PubSub, [name: ElixirBoilerplate.PubSub, adapter: Phoenix.PubSub.PG2]}, ElixirBoilerplateWeb.Endpoint, diff --git a/lib/elixir_boilerplate_web/home/templates/home_header.html.heex b/lib/elixir_boilerplate_web/home/templates/home_header.html.heex deleted file mode 100644 index 136270a5..00000000 --- a/lib/elixir_boilerplate_web/home/templates/home_header.html.heex +++ /dev/null @@ -1,30 +0,0 @@ -
    -
    -
    - - - - -

    - v<%= Application.spec(:phoenix, :vsn) %> -

    -
    - - -
    -
    diff --git a/lib/elixir_boilerplate_web/home/templates/index.html.heex b/lib/elixir_boilerplate_web/home/templates/index.html.heex index 915227c0..9c732571 100644 --- a/lib/elixir_boilerplate_web/home/templates/index.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index.html.heex @@ -1,5 +1,3 @@ -<.home_header /> -
    <.flash_group flash={@flash} /> diff --git a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex index 918db5be..8848fb71 100644 --- a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex @@ -1,5 +1,3 @@ -<.home_header /> -
    <.flash_group flash={@flash} /> diff --git a/lib/elixir_boilerplate_web/plugs/security.ex b/lib/elixir_boilerplate_web/plugs/security.ex index 2837fdf8..a95237fe 100644 --- a/lib/elixir_boilerplate_web/plugs/security.ex +++ b/lib/elixir_boilerplate_web/plugs/security.ex @@ -38,7 +38,7 @@ defmodule ElixirBoilerplateWeb.Plugs.Security do defp style_src_directive do if Application.get_env(:elixir_boilerplate, __MODULE__)[:allow_unsafe_scripts] do - "'self' 'unsafe-inline' cdn.jsdelivr.net" + "'self' 'unsafe-inline'" else "'self'" end @@ -46,7 +46,7 @@ defmodule ElixirBoilerplateWeb.Plugs.Security do defp script_src_directive do if Application.get_env(:elixir_boilerplate, __MODULE__)[:allow_unsafe_scripts] do - "'self' 'unsafe-eval' 'unsafe-inline' cdn.jsdelivr.net" + "'self' 'unsafe-eval' 'unsafe-inline'" else "'self'" end diff --git a/lib/elixir_boilerplate_web/telemetry.ex b/lib/elixir_boilerplate_web/telemetry.ex deleted file mode 100644 index c5160abc..00000000 --- a/lib/elixir_boilerplate_web/telemetry.ex +++ /dev/null @@ -1,91 +0,0 @@ -defmodule ElixirBoilerplateWeb.Telemetry do - use Supervisor - import Telemetry.Metrics - - def start_link(arg) do - Supervisor.start_link(__MODULE__, arg, name: __MODULE__) - end - - @impl true - def init(_arg) do - children = [ - # Telemetry poller will execute the given period measurements - # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics - {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} - # Add reporters as children of your supervision tree. - # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} - ] - - Supervisor.init(children, strategy: :one_for_one) - end - - def metrics do - [ - # Phoenix Metrics - summary("phoenix.endpoint.start.system_time", - unit: {:native, :millisecond} - ), - summary("phoenix.endpoint.stop.duration", - unit: {:native, :millisecond} - ), - summary("phoenix.router_dispatch.start.system_time", - tags: [:route], - unit: {:native, :millisecond} - ), - summary("phoenix.router_dispatch.exception.duration", - tags: [:route], - unit: {:native, :millisecond} - ), - summary("phoenix.router_dispatch.stop.duration", - tags: [:route], - unit: {:native, :millisecond} - ), - summary("phoenix.socket_connected.duration", - unit: {:native, :millisecond} - ), - summary("phoenix.channel_join.duration", - unit: {:native, :millisecond} - ), - summary("phoenix.channel_handled_in.duration", - tags: [:event], - unit: {:native, :millisecond} - ), - - # Database Metrics - summary("elixir_boilerplate.repo.query.total_time", - unit: {:native, :millisecond}, - description: "The sum of the other measurements" - ), - summary("elixir_boilerplate.repo.query.decode_time", - unit: {:native, :millisecond}, - description: "The time spent decoding the data received from the database" - ), - summary("elixir_boilerplate.repo.query.query_time", - unit: {:native, :millisecond}, - description: "The time spent executing the query" - ), - summary("elixir_boilerplate.repo.query.queue_time", - unit: {:native, :millisecond}, - description: "The time spent waiting for a database connection" - ), - summary("elixir_boilerplate.repo.query.idle_time", - unit: {:native, :millisecond}, - description: "The time the connection spent waiting before being checked out for the query" - ), - - # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), - summary("vm.total_run_queue_lengths.total"), - summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io") - ] - end - - defp periodic_measurements do - [ - # A module, function and arguments to be invoked periodically. - # This function must call :telemetry.execute/3 and a metric must be added above. - # {<%= @web_namespace %>, :count_users, []} - ] - end -end diff --git a/mix.exs b/mix.exs index bdde9fd9..bcfc1a11 100644 --- a/mix.exs +++ b/mix.exs @@ -78,10 +78,6 @@ defmodule ElixirBoilerplate.Mixfile do # Database check {:excellent_migrations, "~> 0.1", only: [:dev, :test], runtime: false}, - # Telemtry plugins - {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 1.0"}, - # Translations {:gettext, "~> 0.22"}, From be5039bedde043f4b31d1fb770671510b1fcf133 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Fri, 8 Sep 2023 13:08:18 -0400 Subject: [PATCH 7/8] Extract BEAM utilities for a separate PRs --- mix.exs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mix.exs b/mix.exs index bcfc1a11..a86c926f 100644 --- a/mix.exs +++ b/mix.exs @@ -113,10 +113,6 @@ defmodule ElixirBoilerplate.Mixfile do # Dialyzer {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, - - # BEAM debugging utilities - {:observer_cli, "~> 1.7"}, - {:recon, "~> 2.5"} ] end From 290f7285e8daee50996d6e1cec2804bf14b38453 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Fri, 8 Sep 2023 15:37:27 -0400 Subject: [PATCH 8/8] Refactor ElixirBoilerplateWeb to segment in each components in separate modules --- assets/js/app.js | 2 - .../api/version/controller.ex | 2 +- lib/elixir_boilerplate_web/component.ex | 9 ++ lib/elixir_boilerplate_web/controller.ex | 15 ++++ .../elixir_boilerplate_web.ex | 86 +------------------ .../errors/error_html.ex | 2 +- lib/elixir_boilerplate_web/home/html.ex | 4 +- lib/elixir_boilerplate_web/home/live.ex | 2 +- .../home/templates/index.html.heex | 2 +- .../home/templates/index_live.html.heex | 2 +- lib/elixir_boilerplate_web/html.ex | 14 +++ lib/elixir_boilerplate_web/layouts/layouts.ex | 2 +- lib/elixir_boilerplate_web/live_view.ex | 9 ++ lib/elixir_boilerplate_web/router.ex | 3 + mix.exs | 2 +- 15 files changed, 62 insertions(+), 94 deletions(-) create mode 100644 lib/elixir_boilerplate_web/component.ex create mode 100644 lib/elixir_boilerplate_web/controller.ex create mode 100644 lib/elixir_boilerplate_web/html.ex create mode 100644 lib/elixir_boilerplate_web/live_view.ex diff --git a/assets/js/app.js b/assets/js/app.js index 58f14bdd..47bee88f 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,8 +1,6 @@ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import 'phoenix_html'; -const DELAY_IN_MILISECONDS = 200; - // Establish Phoenix Socket and LiveView configuration. import {Socket} from 'phoenix'; import {LiveSocket} from 'phoenix_live_view'; diff --git a/lib/elixir_boilerplate_web/api/version/controller.ex b/lib/elixir_boilerplate_web/api/version/controller.ex index fc1fabd1..b5d4e357 100644 --- a/lib/elixir_boilerplate_web/api/version/controller.ex +++ b/lib/elixir_boilerplate_web/api/version/controller.ex @@ -1,5 +1,5 @@ defmodule ElixirBoilerplateWeb.Api.Version.Controller do - use ElixirBoilerplateWeb, :controller + use ElixirBoilerplateWeb.Controller @spec index(Plug.Conn.t(), map) :: Plug.Conn.t() def index(conn, _) do diff --git a/lib/elixir_boilerplate_web/component.ex b/lib/elixir_boilerplate_web/component.ex new file mode 100644 index 00000000..342c81c0 --- /dev/null +++ b/lib/elixir_boilerplate_web/component.ex @@ -0,0 +1,9 @@ +defmodule ElixirBoilerplateWeb.Component do + defmacro __using__(_opts) do + quote do + use Phoenix.LiveComponent + + unquote(ElixirBoilerplateWeb.html_helpers()) + end + end +end diff --git a/lib/elixir_boilerplate_web/controller.ex b/lib/elixir_boilerplate_web/controller.ex new file mode 100644 index 00000000..886af04a --- /dev/null +++ b/lib/elixir_boilerplate_web/controller.ex @@ -0,0 +1,15 @@ +defmodule ElixirBoilerplateWeb.Controller do + defmacro __using__(_opts) do + quote do + use Phoenix.Controller, + namespace: ElixirBoilerplateWeb, + formats: [:html, :json], + layouts: [html: ElixirBoilerplateWeb.Layouts] + + import Plug.Conn + import ElixirBoilerplate.Gettext + + unquote(ElixirBoilerplateWeb.verified_routes()) + end + end +end diff --git a/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex b/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex index 85adc10c..fb1645d3 100644 --- a/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex +++ b/lib/elixir_boilerplate_web/elixir_boilerplate_web.ex @@ -1,88 +1,13 @@ defmodule ElixirBoilerplateWeb do - @moduledoc """ - The entrypoint for defining your web interface, such - as controllers, components, channels, and so on. - This can be used in your application as: - use ElixirBoilerplateWeb, :controller - use ElixirBoilerplateWeb, :html - The definitions below will be executed for every controller, - component, etc, so keep them short and clean, focused - on imports, uses and aliases. - Do NOT define functions inside the quoted expressions - below. Instead, define additional modules and import - those modules here. - """ - def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) - def router do - quote do - use Phoenix.Router, helpers: false - - # Import common connection and controller functions to use in pipelines - import Plug.Conn - import Phoenix.Controller - import Phoenix.LiveView.Router - end - end - - def channel do - quote do - use Phoenix.Channel - end - end - - def controller do - quote do - use Phoenix.Controller, - namespace: ElixirBoilerplateWeb, - formats: [:html, :json], - layouts: [html: ElixirBoilerplateWeb.Layouts] - - import Plug.Conn - import ElixirBoilerplate.Gettext - - unquote(verified_routes()) - end - end - - def live_view do - quote do - use Phoenix.LiveView, - layout: {ElixirBoilerplateWeb.Layouts, :live} - - unquote(html_helpers()) - end - end - - def live_component do - quote do - use Phoenix.LiveComponent - - unquote(html_helpers()) - end - end - - def html do - quote do - use Phoenix.Component - - # Import convenience functions from controllers - import Phoenix.Controller, - only: [get_csrf_token: 0, view_module: 1, view_template: 1] - - # Include general helpers for rendering HTML - unquote(html_helpers()) - end - end - - defp html_helpers do + def html_helpers do quote do # HTML escaping functionality import Phoenix.HTML + # Core UI components and translation import ElixirBoilerplate.Gettext - import ElixirBoilerplateWeb.Components.Branding import ElixirBoilerplateWeb.Components.Core # Shortcut for generating JS commands @@ -101,11 +26,4 @@ defmodule ElixirBoilerplateWeb do statics: ElixirBoilerplateWeb.static_paths() end end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end end diff --git a/lib/elixir_boilerplate_web/errors/error_html.ex b/lib/elixir_boilerplate_web/errors/error_html.ex index ed649f2d..54288506 100644 --- a/lib/elixir_boilerplate_web/errors/error_html.ex +++ b/lib/elixir_boilerplate_web/errors/error_html.ex @@ -1,5 +1,5 @@ defmodule ElixirBoilerplateWeb.Errors.ErrorHTML do - use ElixirBoilerplateWeb, :html + use ElixirBoilerplateWeb.HTML # If you want to customize your error pages, # uncomment the embed_templates/1 call below diff --git a/lib/elixir_boilerplate_web/home/html.ex b/lib/elixir_boilerplate_web/home/html.ex index b0834983..fa7493dd 100644 --- a/lib/elixir_boilerplate_web/home/html.ex +++ b/lib/elixir_boilerplate_web/home/html.ex @@ -1,5 +1,7 @@ defmodule ElixirBoilerplateWeb.Home.HTML do - use ElixirBoilerplateWeb, :html + use ElixirBoilerplateWeb.HTML + + alias ElixirBoilerplateWeb.Components.Branding embed_templates("templates/*") diff --git a/lib/elixir_boilerplate_web/home/live.ex b/lib/elixir_boilerplate_web/home/live.ex index 11b294c6..0e649a82 100644 --- a/lib/elixir_boilerplate_web/home/live.ex +++ b/lib/elixir_boilerplate_web/home/live.ex @@ -1,5 +1,5 @@ defmodule ElixirBoilerplateWeb.Home.Live do - use ElixirBoilerplateWeb, :live_view + use ElixirBoilerplateWeb.LiveView def mount(_, _, socket) do socket = assign(socket, :message, "Hello, world!") diff --git a/lib/elixir_boilerplate_web/home/templates/index.html.heex b/lib/elixir_boilerplate_web/home/templates/index.html.heex index 9c732571..432ac231 100644 --- a/lib/elixir_boilerplate_web/home/templates/index.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index.html.heex @@ -4,7 +4,7 @@
    - <.logo width={500} /> +

    diff --git a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex index 8848fb71..203d3f9f 100644 --- a/lib/elixir_boilerplate_web/home/templates/index_live.html.heex +++ b/lib/elixir_boilerplate_web/home/templates/index_live.html.heex @@ -4,7 +4,7 @@

    - <.logo width={500} /> +

    diff --git a/lib/elixir_boilerplate_web/html.ex b/lib/elixir_boilerplate_web/html.ex new file mode 100644 index 00000000..44c19ff9 --- /dev/null +++ b/lib/elixir_boilerplate_web/html.ex @@ -0,0 +1,14 @@ +defmodule ElixirBoilerplateWeb.HTML do + defmacro __using__(_opts) do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(ElixirBoilerplateWeb.html_helpers()) + end + end +end diff --git a/lib/elixir_boilerplate_web/layouts/layouts.ex b/lib/elixir_boilerplate_web/layouts/layouts.ex index d3c6f328..917f05b6 100644 --- a/lib/elixir_boilerplate_web/layouts/layouts.ex +++ b/lib/elixir_boilerplate_web/layouts/layouts.ex @@ -1,5 +1,5 @@ defmodule ElixirBoilerplateWeb.Layouts do - use ElixirBoilerplateWeb, :html + use ElixirBoilerplateWeb.HTML embed_templates("templates/*") diff --git a/lib/elixir_boilerplate_web/live_view.ex b/lib/elixir_boilerplate_web/live_view.ex new file mode 100644 index 00000000..4c3b211a --- /dev/null +++ b/lib/elixir_boilerplate_web/live_view.ex @@ -0,0 +1,9 @@ +defmodule ElixirBoilerplateWeb.LiveView do + defmacro __using__(_opts) do + quote do + use Phoenix.LiveView, layout: {ElixirBoilerplateWeb.Layouts, :live} + + unquote(ElixirBoilerplateWeb.html_helpers()) + end + end +end diff --git a/lib/elixir_boilerplate_web/router.ex b/lib/elixir_boilerplate_web/router.ex index 9ceac6d1..8313de0b 100644 --- a/lib/elixir_boilerplate_web/router.ex +++ b/lib/elixir_boilerplate_web/router.ex @@ -1,6 +1,9 @@ defmodule ElixirBoilerplateWeb.Router do use Phoenix.Router, helpers: false + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller import Phoenix.LiveView.Router pipeline :browser do diff --git a/mix.exs b/mix.exs index a86c926f..963cc71e 100644 --- a/mix.exs +++ b/mix.exs @@ -112,7 +112,7 @@ defmodule ElixirBoilerplate.Mixfile do {:excoveralls, "~> 0.16", only: :test}, # Dialyzer - {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false} ] end