├── .formatter.exs ├── .gitignore ├── README.md ├── assets ├── css │ └── app.css ├── js │ └── app.js ├── tailwind.config.js └── vendor │ └── topbar.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── lib ├── app.ex ├── app │ ├── application.ex │ └── mailer.ex ├── app_web.ex └── app_web │ ├── components │ ├── core_components.ex │ ├── layouts.ex │ └── layouts │ │ ├── app.html.heex │ │ └── root.html.heex │ ├── controllers │ ├── error_html.ex │ └── error_json.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── live │ └── page_live.ex │ ├── router.ex │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── preview.gif ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── static │ ├── favicon.ico │ └── robots.txt └── video.mp4 └── test ├── app_web └── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ └── page_controller_test.exs ├── support └── conn_case.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | app-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App 2 | 3 | ![Object Detection](https://github.com/philipbrown/video-object-detection/raw/main/preview.gif "Object Detection") 4 | 5 | To start your Phoenix server: 6 | 7 | * Run `mix setup` to install and setup dependencies 8 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 13 | 14 | ## Learn more 15 | 16 | * Official website: https://www.phoenixframework.org/ 17 | * Guides: https://hexdocs.pm/phoenix/overview.html 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Forum: https://elixirforum.com/c/phoenix-forum 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 27 | 28 | // Show progress bar on live navigation and form submits 29 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 30 | window.addEventListener("phx:page-loading-start", info => topbar.delayedShow(200)) 31 | window.addEventListener("phx:page-loading-stop", info => topbar.hide()) 32 | 33 | // connect if there are any LiveViews on the page 34 | liveSocket.connect() 35 | 36 | // expose liveSocket on window for web console debug logs and latency simulation: 37 | // >> liveSocket.enableDebug() 38 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 39 | // >> liveSocket.disableLatencySim() 40 | window.liveSocket = liveSocket 41 | 42 | -------------------------------------------------------------------------------- /assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | 6 | module.exports = { 7 | content: [ 8 | "./js/**/*.js", 9 | "../lib/*_web.ex", 10 | "../lib/*_web/**/*.*ex" 11 | ], 12 | theme: { 13 | extend: { 14 | colors: { 15 | brand: "#FD4F00", 16 | } 17 | }, 18 | }, 19 | plugins: [ 20 | require("@tailwindcss/forms"), 21 | plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), 22 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 23 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 24 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])) 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 1.0.0, 2021-01-06 4 | * Modifications: 5 | * - add delayedShow(time) (2022-09-21) 6 | * http://buunguyen.github.io/topbar 7 | * Copyright (c) 2021 Buu Nguyen 8 | */ 9 | (function (window, document) { 10 | "use strict"; 11 | 12 | // https://gist.github.com/paulirish/1579671 13 | (function () { 14 | var lastTime = 0; 15 | var vendors = ["ms", "moz", "webkit", "o"]; 16 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 17 | window.requestAnimationFrame = 18 | window[vendors[x] + "RequestAnimationFrame"]; 19 | window.cancelAnimationFrame = 20 | window[vendors[x] + "CancelAnimationFrame"] || 21 | window[vendors[x] + "CancelRequestAnimationFrame"]; 22 | } 23 | if (!window.requestAnimationFrame) 24 | window.requestAnimationFrame = function (callback, element) { 25 | var currTime = new Date().getTime(); 26 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 27 | var id = window.setTimeout(function () { 28 | callback(currTime + timeToCall); 29 | }, timeToCall); 30 | lastTime = currTime + timeToCall; 31 | return id; 32 | }; 33 | if (!window.cancelAnimationFrame) 34 | window.cancelAnimationFrame = function (id) { 35 | clearTimeout(id); 36 | }; 37 | })(); 38 | 39 | var canvas, 40 | currentProgress, 41 | showing, 42 | progressTimerId = null, 43 | fadeTimerId = null, 44 | delayTimerId = null, 45 | addEvent = function (elem, type, handler) { 46 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 47 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 48 | else elem["on" + type] = handler; 49 | }, 50 | options = { 51 | autoRun: true, 52 | barThickness: 3, 53 | barColors: { 54 | 0: "rgba(26, 188, 156, .9)", 55 | ".25": "rgba(52, 152, 219, .9)", 56 | ".50": "rgba(241, 196, 15, .9)", 57 | ".75": "rgba(230, 126, 34, .9)", 58 | "1.0": "rgba(211, 84, 0, .9)", 59 | }, 60 | shadowBlur: 10, 61 | shadowColor: "rgba(0, 0, 0, .6)", 62 | className: null, 63 | }, 64 | repaint = function () { 65 | canvas.width = window.innerWidth; 66 | canvas.height = options.barThickness * 5; // need space for shadow 67 | 68 | var ctx = canvas.getContext("2d"); 69 | ctx.shadowBlur = options.shadowBlur; 70 | ctx.shadowColor = options.shadowColor; 71 | 72 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 73 | for (var stop in options.barColors) 74 | lineGradient.addColorStop(stop, options.barColors[stop]); 75 | ctx.lineWidth = options.barThickness; 76 | ctx.beginPath(); 77 | ctx.moveTo(0, options.barThickness / 2); 78 | ctx.lineTo( 79 | Math.ceil(currentProgress * canvas.width), 80 | options.barThickness / 2 81 | ); 82 | ctx.strokeStyle = lineGradient; 83 | ctx.stroke(); 84 | }, 85 | createCanvas = function () { 86 | canvas = document.createElement("canvas"); 87 | var style = canvas.style; 88 | style.position = "fixed"; 89 | style.top = style.left = style.right = style.margin = style.padding = 0; 90 | style.zIndex = 100001; 91 | style.display = "none"; 92 | if (options.className) canvas.classList.add(options.className); 93 | document.body.appendChild(canvas); 94 | addEvent(window, "resize", repaint); 95 | }, 96 | topbar = { 97 | config: function (opts) { 98 | for (var key in opts) 99 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 100 | }, 101 | delayedShow: function(time) { 102 | if (showing) return; 103 | if (delayTimerId) return; 104 | delayTimerId = setTimeout(() => topbar.show(), time); 105 | }, 106 | show: function () { 107 | if (showing) return; 108 | showing = true; 109 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 110 | if (!canvas) createCanvas(); 111 | canvas.style.opacity = 1; 112 | canvas.style.display = "block"; 113 | topbar.progress(0); 114 | if (options.autoRun) { 115 | (function loop() { 116 | progressTimerId = window.requestAnimationFrame(loop); 117 | topbar.progress( 118 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 119 | ); 120 | })(); 121 | } 122 | }, 123 | progress: function (to) { 124 | if (typeof to === "undefined") return currentProgress; 125 | if (typeof to === "string") { 126 | to = 127 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 128 | ? currentProgress 129 | : 0) + parseFloat(to); 130 | } 131 | currentProgress = to > 1 ? 1 : to; 132 | repaint(); 133 | return currentProgress; 134 | }, 135 | hide: function () { 136 | clearTimeout(delayTimerId); 137 | delayTimerId = null; 138 | if (!showing) return; 139 | showing = false; 140 | if (progressTimerId != null) { 141 | window.cancelAnimationFrame(progressTimerId); 142 | progressTimerId = null; 143 | } 144 | (function loop() { 145 | if (topbar.progress("+.1") >= 1) { 146 | canvas.style.opacity -= 0.05; 147 | if (canvas.style.opacity <= 0.05) { 148 | canvas.style.display = "none"; 149 | fadeTimerId = null; 150 | return; 151 | } 152 | } 153 | fadeTimerId = window.requestAnimationFrame(loop); 154 | })(); 155 | }, 156 | }; 157 | 158 | if (typeof module === "object" && typeof module.exports === "object") { 159 | module.exports = topbar; 160 | } else if (typeof define === "function" && define.amd) { 161 | define(function () { 162 | return topbar; 163 | }); 164 | } else { 165 | this.topbar = topbar; 166 | } 167 | }.call(this, window, document)); 168 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :nx, default_backend: EXLA.Backend 11 | 12 | # Configures the endpoint 13 | config :app, AppWeb.Endpoint, 14 | url: [host: "localhost"], 15 | render_errors: [ 16 | formats: [html: AppWeb.ErrorHTML, json: AppWeb.ErrorJSON], 17 | layout: false 18 | ], 19 | pubsub_server: App.PubSub, 20 | live_view: [signing_salt: "Go2FiTeD"] 21 | 22 | # Configures the mailer 23 | # 24 | # By default it uses the "Local" adapter which stores the emails 25 | # locally. You can see the emails in your browser, at "/dev/mailbox". 26 | # 27 | # For production it's recommended to configure a different adapter 28 | # at the `config/runtime.exs`. 29 | config :app, App.Mailer, adapter: Swoosh.Adapters.Local 30 | 31 | # Configure esbuild (the version is required) 32 | config :esbuild, 33 | version: "0.14.41", 34 | default: [ 35 | args: 36 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 37 | cd: Path.expand("../assets", __DIR__), 38 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 39 | ] 40 | 41 | # Configure tailwind (the version is required) 42 | config :tailwind, 43 | version: "3.2.4", 44 | default: [ 45 | args: ~w( 46 | --config=tailwind.config.js 47 | --input=css/app.css 48 | --output=../priv/static/assets/app.css 49 | ), 50 | cd: Path.expand("../assets", __DIR__) 51 | ] 52 | 53 | # Configures Elixir's Logger 54 | config :logger, :console, 55 | format: "$time $metadata[$level] $message\n", 56 | metadata: [:request_id] 57 | 58 | # Use Jason for JSON parsing in Phoenix 59 | config :phoenix, :json_library, Jason 60 | 61 | # Import environment specific config. This must remain at the bottom 62 | # of this file so it overrides the configuration defined above. 63 | import_config "#{config_env()}.exs" 64 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :app, AppWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "pOX/luOT2nbiyPvDDU8St3bDugkD8OUZYa/4XvStUSBgrj5o2TvrVKWDnaLD81xG", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, 19 | tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Run `mix help phx.gen.cert` for more information. 31 | # 32 | # The `http:` config above can be replaced with: 33 | # 34 | # https: [ 35 | # port: 4001, 36 | # cipher_suite: :strong, 37 | # keyfile: "priv/cert/selfsigned_key.pem", 38 | # certfile: "priv/cert/selfsigned.pem" 39 | # ], 40 | # 41 | # If desired, both `http:` and `https:` keys can be 42 | # configured to run both http and https servers on 43 | # different ports. 44 | 45 | # Watch static and templates for browser reloading. 46 | config :app, AppWeb.Endpoint, 47 | live_reload: [ 48 | patterns: [ 49 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 50 | ~r"priv/gettext/.*(po)$", 51 | ~r"lib/app_web/(controllers|live|components)/.*(ex|heex)$" 52 | ] 53 | ] 54 | 55 | # Enable dev routes for dashboard and mailbox 56 | config :app, dev_routes: true 57 | 58 | # Do not include metadata nor timestamps in development logs 59 | config :logger, :console, format: "[$level] $message\n" 60 | 61 | # Set a higher stacktrace during development. Avoid configuring such 62 | # in production as building large stacktraces may be expensive. 63 | config :phoenix, :stacktrace_depth, 20 64 | 65 | # Initialize plugs at runtime for faster development compilation 66 | config :phoenix, :plug_init_mode, :runtime 67 | 68 | # Disable swoosh api client as it is only required for production adapters. 69 | config :swoosh, :api_client, false 70 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :app, AppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" 13 | 14 | # Configures Swoosh API Client 15 | config :swoosh, :api_client, App.Finch 16 | 17 | # Do not print debug messages in production 18 | config :logger, level: :info 19 | 20 | # Runtime production configuration, including reading 21 | # of environment variables, is done on config/runtime.exs. 22 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/app start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :app, AppWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :app, AppWeb.Endpoint, 40 | url: [host: host, port: 443, scheme: "https"], 41 | http: [ 42 | # Enable IPv6 and bind on all interfaces. 43 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 44 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 45 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 46 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 47 | port: port 48 | ], 49 | secret_key_base: secret_key_base 50 | 51 | # ## SSL Support 52 | # 53 | # To get SSL working, you will need to add the `https` key 54 | # to your endpoint configuration: 55 | # 56 | # config :app, AppWeb.Endpoint, 57 | # https: [ 58 | # ..., 59 | # port: 443, 60 | # cipher_suite: :strong, 61 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 62 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 63 | # ] 64 | # 65 | # The `cipher_suite` is set to `:strong` to support only the 66 | # latest and more secure SSL ciphers. This means old browsers 67 | # and clients may not be supported. You can set it to 68 | # `:compatible` for wider support. 69 | # 70 | # `:keyfile` and `:certfile` expect an absolute path to the key 71 | # and cert in disk or a relative path inside priv, for example 72 | # "priv/ssl/server.key". For all supported SSL configuration 73 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 74 | # 75 | # We also recommend setting `force_ssl` in your endpoint, ensuring 76 | # no data is ever sent via http, always redirecting to https: 77 | # 78 | # config :app, AppWeb.Endpoint, 79 | # force_ssl: [hsts: true] 80 | # 81 | # Check `Plug.SSL` for all available options in `force_ssl`. 82 | 83 | # ## Configuring the mailer 84 | # 85 | # In production you need to configure the mailer to use a different adapter. 86 | # Also, you may need to configure the Swoosh API client of your choice if you 87 | # are not using SMTP. Here is an example of the configuration: 88 | # 89 | # config :app, App.Mailer, 90 | # adapter: Swoosh.Adapters.Mailgun, 91 | # api_key: System.get_env("MAILGUN_API_KEY"), 92 | # domain: System.get_env("MAILGUN_DOMAIN") 93 | # 94 | # For this example you need include a HTTP client required by Swoosh API client. 95 | # Swoosh supports Hackney and Finch out of the box: 96 | # 97 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 98 | # 99 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 100 | end 101 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :app, AppWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "q0tYGt8Eb+ythp6nNJahxI78e5izwlRje2wa1vMww4fXpRxUgZxHbXjm/088lvkm", 8 | server: false 9 | 10 | # In test we don't send emails. 11 | config :app, App.Mailer, adapter: Swoosh.Adapters.Test 12 | 13 | # Disable swoosh api client as it is only required for production adapters. 14 | config :swoosh, :api_client, false 15 | 16 | # Print only warnings and errors during test 17 | config :logger, level: :warning 18 | 19 | # Initialize plugs at runtime for faster test compilation 20 | config :phoenix, :plug_init_mode, :runtime 21 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule App do 2 | @moduledoc """ 3 | App keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/app/application.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | # Start the Telemetry supervisor 12 | AppWeb.Telemetry, 13 | # Start the PubSub system 14 | {Phoenix.PubSub, name: App.PubSub}, 15 | # Start Finch 16 | {Finch, name: App.Finch}, 17 | # Start the Endpoint (http/https) 18 | AppWeb.Endpoint 19 | # Start a worker by calling: App.Worker.start_link(arg) 20 | # {App.Worker, arg} 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: App.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | @impl true 32 | def config_change(changed, _new, removed) do 33 | AppWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/app/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Mailer do 2 | use Swoosh.Mailer, otp_app: :app 3 | end 4 | -------------------------------------------------------------------------------- /lib/app_web.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use AppWeb, :controller 9 | use AppWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: AppWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import AppWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {AppWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | end 59 | end 60 | 61 | def live_component do 62 | quote do 63 | use Phoenix.LiveComponent 64 | 65 | unquote(html_helpers()) 66 | end 67 | end 68 | 69 | def html do 70 | quote do 71 | use Phoenix.Component 72 | 73 | # Import convenience functions from controllers 74 | import Phoenix.Controller, 75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 76 | 77 | # Include general helpers for rendering HTML 78 | unquote(html_helpers()) 79 | end 80 | end 81 | 82 | defp html_helpers do 83 | quote do 84 | # HTML escaping functionality 85 | import Phoenix.HTML 86 | # Core UI components and translation 87 | import AppWeb.CoreComponents 88 | import AppWeb.Gettext 89 | 90 | # Shortcut for generating JS commands 91 | alias Phoenix.LiveView.JS 92 | 93 | # Routes generation with the ~p sigil 94 | unquote(verified_routes()) 95 | end 96 | end 97 | 98 | def verified_routes do 99 | quote do 100 | use Phoenix.VerifiedRoutes, 101 | endpoint: AppWeb.Endpoint, 102 | router: AppWeb.Router, 103 | statics: AppWeb.static_paths() 104 | end 105 | end 106 | 107 | @doc """ 108 | When used, dispatch to the appropriate controller/view/etc. 109 | """ 110 | defmacro __using__(which) when is_atom(which) do 111 | apply(__MODULE__, which, []) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/app_web/components/core_components.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.CoreComponents do 2 | @moduledoc """ 3 | Provides core UI components. 4 | 5 | The components in this module use Tailwind CSS, a utility-first CSS framework. 6 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to 7 | customize the generated components in this module. 8 | 9 | Icons are provided by [heroicons](https://heroicons.com), using the 10 | [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project. 11 | """ 12 | use Phoenix.Component 13 | 14 | alias Phoenix.LiveView.JS 15 | import AppWeb.Gettext 16 | 17 | @doc """ 18 | Renders a modal. 19 | 20 | ## Examples 21 | 22 | <.modal id="confirm-modal"> 23 | Are you sure? 24 | <:confirm>OK 25 | <:cancel>Cancel 26 | 27 | 28 | JS commands may be passed to the `:on_cancel` and `on_confirm` attributes 29 | for the caller to react to each button press, for example: 30 | 31 | <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}> 32 | Are you sure you? 33 | <:confirm>OK 34 | <:cancel>Cancel 35 | 36 | """ 37 | attr :id, :string, required: true 38 | attr :show, :boolean, default: false 39 | attr :on_cancel, JS, default: %JS{} 40 | attr :on_confirm, JS, default: %JS{} 41 | 42 | slot :inner_block, required: true 43 | slot :title 44 | slot :subtitle 45 | slot :confirm 46 | slot :cancel 47 | 48 | def modal(assigns) do 49 | ~H""" 50 |