diff --git a/lib/live_view_events/send.ex b/lib/live_view_events/notify.ex similarity index 53% rename from lib/live_view_events/send.ex rename to lib/live_view_events/notify.ex index 8c16552..91e0d2d 100644 --- a/lib/live_view_events/send.ex +++ b/lib/live_view_events/notify.ex @@ -5,6 +5,31 @@ defmodule LiveViewEvents.Notify do @doc """ Use this macro instead of the default `assigns(socket, assign)` in + `c:Phoenix.LiveComponent.update/2`. + + This will detect if `c:Phoenix.LiveComponent.update/2` is being called + because of an event send with either `notify_to/2` or `notify_to/3` + and handle it with `c:Phoenix.LiveView.handle_info/2`. Otherwise, + it will assign the given `assigns` to `socket`. + + If there is no `c:Phoenix.LiveView.handle_info/2` defined in the + component, sending an event won't raise an error but if there is one + defined and it cannot handle the received message, it will. + + Furthermore, if the handler returns anything that is not a tuple + `{:noreply, socket}`, it'll raise an exception too. + + ## Why using `c:Phoenix.LiveView.handle_info/2` in components? + + In one word: consistency. Messages coming from the client are + handled by `c:Phoenix.LiveView.handle_event/3` or by + `c:Phoenix.LiveComponent.handle_event/3`. For messages sent from + the server are currently being handled by `c:Phoenix.LiveView.handle_info/2` + in live views, with not official way to do this but the hack this + library is based on. + + The hack is basically send an update with `Phoenix.LiveView.send_update/3` + and handle it in `c:Phoenix.LiveComponent.update/2` """ defmacro handle_info_or_assign(socket, assigns) do quote do @@ -47,8 +72,9 @@ defmodule LiveViewEvents.Notify do def notify_to(:self, message), do: notify_to(self(), message) def notify_to(pid, message) when is_pid(pid), do: send(pid, message) - def notify_to({module, id}, message) do - Phoenix.LiveView.send_update(self(), module, %{:id => id, @assign_name_for_event => message}) + def notify_to(target, message) when is_tuple(target) do + {pid, module, id} = process_tuple(target) + Phoenix.LiveView.send_update(pid, module, %{:id => id, @assign_name_for_event => message}) end @doc """ @@ -59,10 +85,14 @@ defmodule LiveViewEvents.Notify do def notify_to(:self, message, params), do: notify_to(self(), message, params) def notify_to(pid, message, params) when is_pid(pid), do: send(pid, {message, params}) - def notify_to({module, id}, message, params) do - Phoenix.LiveView.send_update(self(), module, %{ + def notify_to(target, message, params) when is_tuple(target) do + {pid, module, id}=process_tuple(target) + Phoenix.LiveView.send_update(pid, module, %{ :id => id, @assign_name_for_event => {message, params} }) end + + defp process_tuple({module, id}), do: {self(), module, id} + defp process_tuple({pid, _module, _id}=target) when is_pid(pid), do: target end diff --git a/test_app/.formatter.exs b/test_app/.formatter.exs new file mode 100644 index 0000000..e945e12 --- /dev/null +++ b/test_app/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:phoenix], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] +] diff --git a/test_app/.gitignore b/test_app/.gitignore new file mode 100644 index 0000000..89f67c1 --- /dev/null +++ b/test_app/.gitignore @@ -0,0 +1,27 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +test_app-*.tar + diff --git a/test_app/README.md b/test_app/README.md new file mode 100644 index 0000000..88ab700 --- /dev/null +++ b/test_app/README.md @@ -0,0 +1,18 @@ +# TestApp + +To start your Phoenix server: + + * Run `mix setup` to install and setup dependencies + * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). + +## Learn more + + * Official website: https://www.phoenixframework.org/ + * Guides: https://hexdocs.pm/phoenix/overview.html + * Docs: https://hexdocs.pm/phoenix + * Forum: https://elixirforum.com/c/phoenix-forum + * Source: https://github.com/phoenixframework/phoenix diff --git a/test_app/config/config.exs b/test_app/config/config.exs new file mode 100644 index 0000000..ee59105 --- /dev/null +++ b/test_app/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +# Configures the endpoint +config :test_app, TestAppWeb.Endpoint, + url: [host: "localhost"], + render_errors: [ + formats: [html: TestAppWeb.ErrorHTML, json: TestAppWeb.ErrorJSON], + layout: false + ], + pubsub_server: TestApp.PubSub, + live_view: [signing_salt: "Yv51WWUs"] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/test_app/config/dev.exs b/test_app/config/dev.exs new file mode 100644 index 0000000..ab4b449 --- /dev/null +++ b/test_app/config/dev.exs @@ -0,0 +1,62 @@ +import Config + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. +config :test_app, TestAppWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. + http: [ip: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "/OOtixijCMkathudAQ6iWMDxAguNzdKOUBFOLZo/2h9IjzdYcX63RdVs4uvmdB3f", + watchers: [] + +# ## SSL Support +# +# In order to use HTTPS in development, a self-signed +# certificate can be generated by running the following +# Mix task: +# +# mix phx.gen.cert +# +# Run `mix help phx.gen.cert` for more information. +# +# The `http:` config above can be replaced with: +# +# https: [ +# port: 4001, +# cipher_suite: :strong, +# keyfile: "priv/cert/selfsigned_key.pem", +# certfile: "priv/cert/selfsigned.pem" +# ], +# +# If desired, both `http:` and `https:` keys can be +# configured to run both http and https servers on +# different ports. + +# Watch static and templates for browser reloading. +config :test_app, TestAppWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"lib/test_app_web/(controllers|live|components)/.*(ex|heex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :test_app, dev_routes: true + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Set a higher stacktrace during development. Avoid configuring such +# in production as building large stacktraces may be expensive. +config :phoenix, :stacktrace_depth, 20 + +# Initialize plugs at runtime for faster development compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/test_app/config/prod.exs b/test_app/config/prod.exs new file mode 100644 index 0000000..1fe2d9e --- /dev/null +++ b/test_app/config/prod.exs @@ -0,0 +1,7 @@ +import Config + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/test_app/config/runtime.exs b/test_app/config/runtime.exs new file mode 100644 index 0000000..6b617b4 --- /dev/null +++ b/test_app/config/runtime.exs @@ -0,0 +1,82 @@ +import Config + +# config/runtime.exs is executed for all environments, including +# during releases. It is executed after compilation and before the +# system starts, so it is typically used to load production configuration +# and secrets from environment variables or elsewhere. Do not define +# any compile-time configuration in here, as it won't be applied. +# The block below contains prod specific runtime configuration. + +# ## Using releases +# +# If you use `mix release`, you need to explicitly enable the server +# by passing the PHX_SERVER=true when you start it: +# +# PHX_SERVER=true bin/test_app start +# +# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` +# script that automatically sets the env var above. +if System.get_env("PHX_SERVER") do + config :test_app, TestAppWeb.Endpoint, server: true +end + +if config_env() == :prod do + # The secret key base is used to sign/encrypt cookies and other secrets. + # A default value is used in config/dev.exs and config/test.exs but you + # want to use a different value for prod and you most likely don't want + # to check this value into version control, so we use an environment + # variable instead. + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :test_app, TestAppWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :test_app, TestAppWeb.Endpoint, + # https: [ + # ..., + # port: 443, + # cipher_suite: :strong, + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") + # ] + # + # The `cipher_suite` is set to `:strong` to support only the + # latest and more secure SSL ciphers. This means old browsers + # and clients may not be supported. You can set it to + # `:compatible` for wider support. + # + # `:keyfile` and `:certfile` expect an absolute path to the key + # and cert in disk or a relative path inside priv, for example + # "priv/ssl/server.key". For all supported SSL configuration + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 + # + # We also recommend setting `force_ssl` in your endpoint, ensuring + # no data is ever sent via http, always redirecting to https: + # + # config :test_app, TestAppWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. +end diff --git a/test_app/config/test.exs b/test_app/config/test.exs new file mode 100644 index 0000000..b75a84e --- /dev/null +++ b/test_app/config/test.exs @@ -0,0 +1,14 @@ +import Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :test_app, TestAppWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "EdKwrnAZO/we479ohsBsJbnpaK+0LV2SONLY83a+FNvycfw2Rfp7C7WtVWSbrAZS", + server: false + +# Print only warnings and errors during test +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/test_app/lib/test_app.ex b/test_app/lib/test_app.ex new file mode 100644 index 0000000..800559e --- /dev/null +++ b/test_app/lib/test_app.ex @@ -0,0 +1,9 @@ +defmodule TestApp do + @moduledoc """ + TestApp keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/test_app/lib/test_app/application.ex b/test_app/lib/test_app/application.ex new file mode 100644 index 0000000..d78dc9b --- /dev/null +++ b/test_app/lib/test_app/application.ex @@ -0,0 +1,34 @@ +defmodule TestApp.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + # Start the Telemetry supervisor + TestAppWeb.Telemetry, + # Start the PubSub system + {Phoenix.PubSub, name: TestApp.PubSub}, + # Start the Endpoint (http/https) + TestAppWeb.Endpoint + # Start a worker by calling: TestApp.Worker.start_link(arg) + # {TestApp.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: TestApp.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + TestAppWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/test_app/lib/test_app_web.ex b/test_app/lib/test_app_web.ex new file mode 100644 index 0000000..182a6a9 --- /dev/null +++ b/test_app/lib/test_app_web.ex @@ -0,0 +1,111 @@ +defmodule TestAppWeb 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 TestAppWeb, :controller + use TestAppWeb, :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, + formats: [:html, :json], + layouts: [html: TestAppWeb.Layouts] + + import Plug.Conn + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {TestAppWeb.Layouts, :app} + + 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 TestAppWeb.CoreComponents + + # 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: TestAppWeb.Endpoint, + router: TestAppWeb.Router, + statics: TestAppWeb.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/test_app/lib/test_app_web/components/core_components.ex b/test_app/lib/test_app_web/components/core_components.ex new file mode 100644 index 0000000..38a3b0d --- /dev/null +++ b/test_app/lib/test_app_web/components/core_components.ex @@ -0,0 +1,661 @@ +defmodule TestAppWeb.CoreComponents 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 as 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 + + @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] %>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 + # You can make use of gettext to translate error messages by + # uncommenting and adjusting the following code: + + # if count = opts[:count] do + # Gettext.dngettext(TestAppWeb.Gettext, "errors", msg, msg, count, opts) + # else + # Gettext.dgettext(TestAppWeb.Gettext, "errors", msg, opts) + # end + + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + 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/test_app/lib/test_app_web/components/layouts.ex b/test_app/lib/test_app_web/components/layouts.ex new file mode 100644 index 0000000..44412f8 --- /dev/null +++ b/test_app/lib/test_app_web/components/layouts.ex @@ -0,0 +1,5 @@ +defmodule TestAppWeb.Layouts do + use TestAppWeb, :html + + embed_templates "layouts/*" +end diff --git a/test_app/lib/test_app_web/components/layouts/app.html.heex b/test_app/lib/test_app_web/components/layouts/app.html.heex new file mode 100644 index 0000000..e23bfc8 --- /dev/null +++ b/test_app/lib/test_app_web/components/layouts/app.html.heex @@ -0,0 +1,32 @@ +
+
+
+ + + +

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

+
+ +
+
+
+
+ <.flash_group flash={@flash} /> + <%= @inner_content %> +
+
diff --git a/test_app/lib/test_app_web/components/layouts/root.html.heex b/test_app/lib/test_app_web/components/layouts/root.html.heex new file mode 100644 index 0000000..9df4ed8 --- /dev/null +++ b/test_app/lib/test_app_web/components/layouts/root.html.heex @@ -0,0 +1,17 @@ + + + + + + + <.live_title suffix=" · Phoenix Framework"> + <%= assigns[:page_title] || "TestApp" %> + + + + + + <%= @inner_content %> + + diff --git a/test_app/lib/test_app_web/components/receiver.ex b/test_app/lib/test_app_web/components/receiver.ex new file mode 100644 index 0000000..7c8db8d --- /dev/null +++ b/test_app/lib/test_app_web/components/receiver.ex @@ -0,0 +1,45 @@ +defmodule TestAppWeb.Components.Receiver do + use TestAppWeb, :live_component + + use LiveViewEvents + + def mount(socket) do + socket = assign(socket, :messages, []) + + {:ok, socket} + end + + def update(assigns, socket) do + socket = socket |> handle_info_or_assign(assigns) + + {:ok, socket} + end + + def render(assigns) do + ~H""" + + """ + end + + def handle_info({message, params}, socket) do + socket = update(socket, :messages, &[%{name: message, params: params} | &1]) + + {:noreply, socket} + end + + def handle_info(message, socket) do + socket = update(socket, :messages, &[%{name: message, params: :empty} | &1]) + + {:noreply, socket} + end + + defp display_params(:empty), do: "empty" + defp display_params(other), do: Jason.encode!(other) +end diff --git a/test_app/lib/test_app_web/components/sender.ex b/test_app/lib/test_app_web/components/sender.ex new file mode 100644 index 0000000..f57614b --- /dev/null +++ b/test_app/lib/test_app_web/components/sender.ex @@ -0,0 +1,19 @@ +defmodule TestAppWeb.Components.Sender do + use TestAppWeb, :live_component + + use LiveViewEvents + + def render(assigns) do + ~H""" + + """ + end + + def handle_event("send", _params, socket) do + message_params = socket.assigns.message_params + + apply(LiveViewEvents.Notify, :notify_to, message_params) + + {:noreply, socket} + end +end diff --git a/test_app/lib/test_app_web/controllers/error_html.ex b/test_app/lib/test_app_web/controllers/error_html.ex new file mode 100644 index 0000000..1c68554 --- /dev/null +++ b/test_app/lib/test_app_web/controllers/error_html.ex @@ -0,0 +1,19 @@ +defmodule TestAppWeb.ErrorHTML do + use TestAppWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/test_app_web/controllers/error_html/404.html.heex + # * lib/test_app_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/test_app/lib/test_app_web/controllers/error_json.ex b/test_app/lib/test_app_web/controllers/error_json.ex new file mode 100644 index 0000000..cb97522 --- /dev/null +++ b/test_app/lib/test_app_web/controllers/error_json.ex @@ -0,0 +1,15 @@ +defmodule TestAppWeb.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/test_app/lib/test_app_web/controllers/page_controller.ex b/test_app/lib/test_app_web/controllers/page_controller.ex new file mode 100644 index 0000000..660b0fa --- /dev/null +++ b/test_app/lib/test_app_web/controllers/page_controller.ex @@ -0,0 +1,9 @@ +defmodule TestAppWeb.PageController do + use TestAppWeb, :controller + + def home(conn, _params) do + # The home page is often custom made, + # so skip the default app layout. + render(conn, :home, layout: false) + end +end diff --git a/test_app/lib/test_app_web/controllers/page_html.ex b/test_app/lib/test_app_web/controllers/page_html.ex new file mode 100644 index 0000000..af916f0 --- /dev/null +++ b/test_app/lib/test_app_web/controllers/page_html.ex @@ -0,0 +1,5 @@ +defmodule TestAppWeb.PageHTML do + use TestAppWeb, :html + + embed_templates "page_html/*" +end diff --git a/test_app/lib/test_app_web/controllers/page_html/home.html.heex b/test_app/lib/test_app_web/controllers/page_html/home.html.heex new file mode 100644 index 0000000..7e85cc5 --- /dev/null +++ b/test_app/lib/test_app_web/controllers/page_html/home.html.heex @@ -0,0 +1,223 @@ + +<.flash_group flash={@flash} /> + +
+
+ +

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

+

+ Peace of mind from prototype to production. +

+

+ 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. +

+ +
+
diff --git a/test_app/lib/test_app_web/endpoint.ex b/test_app/lib/test_app_web/endpoint.ex new file mode 100644 index 0000000..9038ef1 --- /dev/null +++ b/test_app/lib/test_app_web/endpoint.ex @@ -0,0 +1,46 @@ +defmodule TestAppWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :test_app + + # 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. + @session_options [ + store: :cookie, + key: "_test_app_key", + signing_salt: "vdshQWKc", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :test_app, + gzip: false, + only: TestAppWeb.static_paths() + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug TestAppWeb.Router +end diff --git a/test_app/lib/test_app_web/router.ex b/test_app/lib/test_app_web/router.ex new file mode 100644 index 0000000..610970d --- /dev/null +++ b/test_app/lib/test_app_web/router.ex @@ -0,0 +1,27 @@ +defmodule TestAppWeb.Router do + use TestAppWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {TestAppWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", TestAppWeb do + pipe_through :browser + + get "/", PageController, :home + end + + # Other scopes may use custom stacks. + # scope "/api", TestAppWeb do + # pipe_through :api + # end +end diff --git a/test_app/lib/test_app_web/telemetry.ex b/test_app/lib/test_app_web/telemetry.ex new file mode 100644 index 0000000..d25dbf4 --- /dev/null +++ b/test_app/lib/test_app_web/telemetry.ex @@ -0,0 +1,69 @@ +defmodule TestAppWeb.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_joined.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # 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. + # {TestAppWeb, :count_users, []} + ] + end +end diff --git a/test_app/mix.exs b/test_app/mix.exs new file mode 100644 index 0000000..577b07b --- /dev/null +++ b/test_app/mix.exs @@ -0,0 +1,71 @@ +defmodule TestApp.MixProject do + use Mix.Project + + def project do + [ + app: :test_app, + version: "0.1.0", + elixir: "~> 1.14", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {TestApp.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + phoenix_version = + "PHOENIX_VERSION" + |> System.get_env("1.7.7") + |> IO.inspect(label: "PHOENIX VERSION") + + phoenix_lv_version = + "PHOENIX_LIVE_VIEW_VERSION" + |> System.get_env("0.20.0") + |> IO.inspect(label: "PHOENIX LIVE VIEW VERSION") + + [ + {:phoenix, "~> #{phoenix_version}"}, + {:phoenix_html, "~> 3.3"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> #{phoenix_lv_version}"}, + {:live_isolated_component, "~> 0.7.0", only: [:dev, :test]}, + {:live_view_events, path: "./.."}, + {:floki, ">= 0.30.0", only: :test}, + {:telemetry_metrics, "~> 0.6"}, + {:telemetry_poller, "~> 1.0"}, + {:jason, "~> 1.2"}, + {:plug_cowboy, "~> 2.5"}, + {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to install project dependencies and perform other setup tasks, run: + # + # $ mix setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + setup: ["deps.get"] + ] + end +end diff --git a/test_app/mix.lock b/test_app/mix.lock new file mode 100644 index 0000000..c3c790f --- /dev/null +++ b/test_app/mix.lock @@ -0,0 +1,27 @@ +%{ + "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "live_isolated_component": {:hex, :live_isolated_component, "0.7.1", "9db55ee987484e58b75078ee62010659c8ec2e77afa1b3d6ec91de622c07f8e7", [:mix], [{:phoenix, "~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0 or ~> 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "8a0f380a05b9cf196438ef37dee61d242ab7f6d9c4fa2c73bb8a69304ebfc9a1"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.1.1", "eee6fc570d77ad6851c7bc08de420a47fd1e449ef5ccfa6a77ef68b72e7e51ad", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f82262b54dee533467021723892e15c3267349849f1f737526523ecba4e6baae"}, + "phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [: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.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [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", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.0", "3f3531c835e46a3b45b4c3ca4a09cef7ba1d0f0d0035eef751c7084b8adb1299", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "29875f8a58fb031f2dc8f3be025c92ed78d342b46f9bbf6dfe579549d7c81050"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [: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", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, +} diff --git a/test_app/priv/static/assets/app.css b/test_app/priv/static/assets/app.css new file mode 100644 index 0000000..35d6918 --- /dev/null +++ b/test_app/priv/static/assets/app.css @@ -0,0 +1 @@ +/* This file is for your main application CSS */ diff --git a/test_app/priv/static/assets/app.js b/test_app/priv/static/assets/app.js new file mode 100644 index 0000000..4d5caaa --- /dev/null +++ b/test_app/priv/static/assets/app.js @@ -0,0 +1,11 @@ +// For Phoenix.HTML support, including form and button helpers +// copy the following scripts into your javascript bundle: +// * deps/phoenix_html/priv/static/phoenix_html.js + +// For Phoenix.Channels support, copy the following scripts +// into your javascript bundle: +// * deps/phoenix/priv/static/phoenix.js + +// For Phoenix.LiveView support, copy the following scripts +// into your javascript bundle: +// * deps/phoenix_live_view/priv/static/phoenix_live_view.js diff --git a/test_app/priv/static/assets/home.css b/test_app/priv/static/assets/home.css new file mode 100644 index 0000000..4746663 --- /dev/null +++ b/test_app/priv/static/assets/home.css @@ -0,0 +1,1113 @@ +/* Default styling for the home page, this file can be deleted safely */ + +/* +! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.inset-0 { + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.right-0 { + right: 0px; +} + +.left-\[40rem\] { + left: 40rem; +} + +.z-0 { + z-index: 0; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.-mx-2 { + margin-left: -0.5rem; + margin-right: -0.5rem; +} + +.-my-0\.5 { + margin-top: -0.125rem; + margin-bottom: -0.125rem; +} + +.-my-0 { + margin-top: -0px; + margin-bottom: -0px; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.grid { + display: grid; +} + +.contents { + display: contents; +} + +.hidden { + display: none; +} + +.h-6 { + height: 1.5rem; +} + +.h-full { + height: 100%; +} + +.h-12 { + height: 3rem; +} + +.h-4 { + height: 1rem; +} + +.w-full { + width: 100%; +} + +.w-6 { + width: 1.5rem; +} + +.w-4 { + width: 1rem; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-4 { + gap: 1rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-x-6 { + -moz-column-gap: 1.5rem; + column-gap: 1.5rem; +} + +.gap-y-4 { + row-gap: 1rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-2xl { + border-radius: 1rem; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-zinc-100 { + --tw-border-opacity: 1; + border-color: rgb(244 244 245 / var(--tw-border-opacity)); +} + +.bg-brand\/5 { + background-color: rgb(253 79 0 / 0.05); +} + +.bg-zinc-100 { + --tw-bg-opacity: 1; + background-color: rgb(244 244 245 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-zinc-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 250 250 / var(--tw-bg-opacity)); +} + +.fill-zinc-400 { + fill: #a1a1aa; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-20 { + padding-top: 5rem; + padding-bottom: 5rem; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + +.py-0 { + padding-top: 0px; + padding-bottom: 0px; +} + +.text-\[0\.8125rem\] { + font-size: 0.8125rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-\[2rem\] { + font-size: 2rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-10 { + line-height: 2.5rem; +} + +.leading-7 { + line-height: 1.75rem; +} + +.tracking-tighter { + letter-spacing: -0.05em; +} + +.text-brand { + --tw-text-opacity: 1; + color: rgb(253 79 0 / var(--tw-text-opacity)); +} + +.text-zinc-900 { + --tw-text-opacity: 1; + color: rgb(24 24 27 / var(--tw-text-opacity)); +} + +.text-zinc-600 { + --tw-text-opacity: 1; + color: rgb(82 82 91 / var(--tw-text-opacity)); +} + +.text-zinc-700 { + --tw-text-opacity: 1; + color: rgb(63 63 70 / var(--tw-text-opacity)); +} + +.antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +/* This file is for your main application CSS */ + +.hover\:bg-zinc-200\/80:hover { + background-color: rgb(228 228 231 / 0.8); +} + +.hover\:bg-zinc-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(250 250 250 / var(--tw-bg-opacity)); +} + +.hover\:text-zinc-700:hover { + --tw-text-opacity: 1; + color: rgb(63 63 70 / var(--tw-text-opacity)); +} + +.hover\:text-zinc-900:hover { + --tw-text-opacity: 1; + color: rgb(24 24 27 / var(--tw-text-opacity)); +} + +.active\:text-zinc-900\/70:active { + color: rgb(24 24 27 / 0.7); +} + +.group:hover .group-hover\:bg-zinc-100 { + --tw-bg-opacity: 1; + background-color: rgb(244 244 245 / var(--tw-bg-opacity)); +} + +.group:hover .group-hover\:fill-zinc-600 { + fill: #52525b; +} + +@media (min-width: 640px) { + .sm\:w-auto { + width: auto; + } + + .sm\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:flex-col { + flex-direction: column; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .sm\:py-28 { + padding-top: 7rem; + padding-bottom: 7rem; + } + + .sm\:py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; + } + + .group:hover .sm\:group-hover\:scale-105 { + --tw-scale-x: 1.05; + --tw-scale-y: 1.05; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } +} + +@media (min-width: 1024px) { + .lg\:mx-0 { + margin-left: 0px; + margin-right: 0px; + } + + .lg\:block { + display: block; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width: 1280px) { + .xl\:left-\[50rem\] { + left: 50rem; + } + + .xl\:py-32 { + padding-top: 8rem; + padding-bottom: 8rem; + } + + .xl\:px-28 { + padding-left: 7rem; + padding-right: 7rem; + } +} diff --git a/test_app/priv/static/favicon.ico b/test_app/priv/static/favicon.ico new file mode 100644 index 0000000..73de524 Binary files /dev/null and b/test_app/priv/static/favicon.ico differ diff --git a/test_app/priv/static/images/logo.svg b/test_app/priv/static/images/logo.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/test_app/priv/static/images/logo.svg @@ -0,0 +1,6 @@ + diff --git a/test_app/priv/static/robots.txt b/test_app/priv/static/robots.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/test_app/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test_app/test/live_view_events/notify/to_component_test.exs b/test_app/test/live_view_events/notify/to_component_test.exs new file mode 100644 index 0000000..74810fe --- /dev/null +++ b/test_app/test/live_view_events/notify/to_component_test.exs @@ -0,0 +1,115 @@ +defmodule LiveViewEvents.Notify.ToComponentTest do + use TestAppWeb.ConnCase + + import LiveIsolatedComponent + import Phoenix.Component, only: [live_component: 1, sigil_H: 2] + + alias LiveViewEvents.Notify + alias TestAppWeb.Components.Receiver + alias TestAppWeb.Components.Sender + + @receiver_id "receiver" + + defp live_receiver(), do: live_isolated_component(Receiver, %{id: @receiver_id}) + + defp text(el), do: el |> Floki.text() |> String.replace(~r/\s+/, " ") + defp text(el, selector), do: el |> Floki.find(selector) |> text() + + defp parse_params("empty"), do: %{} + + defp parse_params(text) do + Jason.decode!(text) + end + + defp events(view) do + view + |> render() + |> Floki.find(".message") + |> Enum.map(fn el -> + {text(el, ".name"), el |> text(".params") |> parse_params()} + end) + end + + defp last_event(view), do: view |> events() |> List.first() + + describe "with pid in tuple" do + # In tests, we need to include the pid in the tuple as the live view is a different process. + test "can send event to component without params" do + {:ok, view, _html} = live_receiver() + + assert events(view) == [] + + view |> target() |> Notify.notify_to(:a_message) + + assert last_event(view) == {"a_message", %{}} + + view |> target() |> Notify.notify_to(:another_message) + + assert last_event(view) == {"another_message", %{}} + end + + test "can send events to component with params" do + {:ok, view, _html} = live_receiver() + + assert events(view) == [] + + view |> target() |> Notify.notify_to(:a_message, %{"hello" => "hello"}) + + assert last_event(view) == {"a_message", %{"hello" => "hello"}} + + view |> target() |> Notify.notify_to(:another_message, 5) + + assert last_event(view) == {"another_message", 5} + + view |> target() |> Notify.notify_to(:yet_another_message, ["hola", 5, true]) + + assert last_event(view) == {"yet_another_message", ["hola", 5, true]} + end + end + + describe "without pid in tuple" do + # So we can send the message correctly, we need to use another component + test "can send event without params" do + {:ok, view, _html} = + live_send_and_receiver(assigns: %{message_params: [target(), :message]}) + + assert events(view) == [] + + send_event(view) + + assert last_event(view) == {"message", %{}} + end + + test "can send event with params" do + {:ok, view, _html} = + live_send_and_receiver( + assigns: %{message_params: [target(), :message, %{"a" => "param"}]} + ) + + assert events(view) == [] + + send_event(view) + + assert last_event(view) == {"message", %{"a" => "param"}} + end + end + + def send_event(view), do: view |> element(".sender") |> render_click() + + defp target(), do: {Receiver, @receiver_id} + defp target(view), do: {view.pid, Receiver, @receiver_id} + + defp live_send_and_receiver(opts) do + live_isolated_component( + fn assigns -> + assigns = Phoenix.Component.assign(assigns, :receiver_id, @receiver_id) + + ~H""" + <.live_component module={Sender} id="sender" message_params={@message_params} /> + <.live_component module={Receiver} id={@receiver_id} /> + """ + end, + opts + ) + end +end diff --git a/test_app/test/support/conn_case.ex b/test_app/test/support/conn_case.ex new file mode 100644 index 0000000..cbcdcec --- /dev/null +++ b/test_app/test/support/conn_case.ex @@ -0,0 +1,38 @@ +defmodule TestAppWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use TestAppWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # The default endpoint for testing + @endpoint TestAppWeb.Endpoint + + use TestAppWeb, :verified_routes + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import Phoenix.LiveViewTest + import TestAppWeb.ConnCase + end + end + + setup _tags do + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/test_app/test/test_app_web/controllers/error_html_test.exs b/test_app/test/test_app_web/controllers/error_html_test.exs new file mode 100644 index 0000000..4d0b49e --- /dev/null +++ b/test_app/test/test_app_web/controllers/error_html_test.exs @@ -0,0 +1,14 @@ +defmodule TestAppWeb.ErrorHTMLTest do + use TestAppWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template + + test "renders 404.html" do + assert render_to_string(TestAppWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(TestAppWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/test_app/test/test_app_web/controllers/error_json_test.exs b/test_app/test/test_app_web/controllers/error_json_test.exs new file mode 100644 index 0000000..48a4640 --- /dev/null +++ b/test_app/test/test_app_web/controllers/error_json_test.exs @@ -0,0 +1,12 @@ +defmodule TestAppWeb.ErrorJSONTest do + use TestAppWeb.ConnCase, async: true + + test "renders 404" do + assert TestAppWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert TestAppWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/test_app/test/test_app_web/controllers/page_controller_test.exs b/test_app/test/test_app_web/controllers/page_controller_test.exs new file mode 100644 index 0000000..450e6b3 --- /dev/null +++ b/test_app/test/test_app_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule TestAppWeb.PageControllerTest do + use TestAppWeb.ConnCase + + test "GET /", %{conn: conn} do + conn = get(conn, ~p"/") + assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + end +end diff --git a/test_app/test/test_helper.exs b/test_app/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test_app/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()