├── .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 | 
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 |
56 |
57 |
65 |
66 |
67 | <.focus_wrap
68 | id={"#{@id}-container"}
69 | phx-mounted={@show && show_modal(@id)}
70 | phx-window-keydown={hide_modal(@on_cancel, @id)}
71 | phx-key="escape"
72 | phx-click-away={hide_modal(@on_cancel, @id)}
73 | class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition"
74 | >
75 |
76 |
84 |
85 |
86 |
98 | <%= render_slot(@inner_block) %>
99 |
100 | <.button
101 | :for={confirm <- @confirm}
102 | id={"#{@id}-confirm"}
103 | phx-click={@on_confirm}
104 | phx-disable-with
105 | class="py-2 px-3"
106 | >
107 | <%= render_slot(confirm) %>
108 |
109 | <.link
110 | :for={cancel <- @cancel}
111 | phx-click={hide_modal(@on_cancel, @id)}
112 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
113 | >
114 | <%= render_slot(cancel) %>
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | """
124 | end
125 |
126 | @doc """
127 | Renders flash notices.
128 |
129 | ## Examples
130 |
131 | <.flash kind={:info} flash={@flash} />
132 | <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
133 | """
134 | attr :id, :string, default: "flash", doc: "the optional id of flash container"
135 | attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
136 | attr :title, :string, default: nil
137 | attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
138 | attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
139 | attr :close, :boolean, default: true, doc: "whether the flash can be closed"
140 | attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
141 |
142 | slot :inner_block, doc: "the optional inner block that renders the flash message"
143 |
144 | def flash(assigns) do
145 | ~H"""
146 | hide("#flash")}
151 | role="alert"
152 | class={[
153 | "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
154 | @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
155 | @kind == :error && "bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
156 | ]}
157 | {@rest}
158 | >
159 |
160 |
161 |
162 | <%= @title %>
163 |
164 |
<%= msg %>
165 |
173 |
174 | """
175 | end
176 |
177 | @doc """
178 | Renders a simple form.
179 |
180 | ## Examples
181 |
182 | <.simple_form :let={f} for={:user} phx-change="validate" phx-submit="save">
183 | <.input field={{f, :email}} label="Email"/>
184 | <.input field={{f, :username}} label="Username" />
185 | <:actions>
186 | <.button>Save
187 |
188 |
189 | """
190 | attr :for, :any, default: nil, doc: "the datastructure for the form"
191 | attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
192 |
193 | attr :rest, :global,
194 | include: ~w(autocomplete name rel action enctype method novalidate target),
195 | doc: "the arbitrary HTML attributes to apply to the form tag"
196 |
197 | slot :inner_block, required: true
198 | slot :actions, doc: "the slot for form actions, such as a submit button"
199 |
200 | def simple_form(assigns) do
201 | ~H"""
202 | <.form :let={f} for={@for} as={@as} {@rest}>
203 |
204 | <%= render_slot(@inner_block, f) %>
205 |
206 | <%= render_slot(action, f) %>
207 |
208 |
209 |
210 | """
211 | end
212 |
213 | @doc """
214 | Renders a button.
215 |
216 | ## Examples
217 |
218 | <.button>Send!
219 | <.button phx-click="go" class="ml-2">Send!
220 | """
221 | attr :type, :string, default: nil
222 | attr :class, :string, default: nil
223 | attr :rest, :global, include: ~w(disabled form name value)
224 |
225 | slot :inner_block, required: true
226 |
227 | def button(assigns) do
228 | ~H"""
229 |
240 | """
241 | end
242 |
243 | @doc """
244 | Renders an input with label and error messages.
245 |
246 | A `%Phoenix.HTML.Form{}` and field name may be passed to the input
247 | to build input names and error messages, or all the attributes and
248 | errors may be passed explicitly.
249 |
250 | ## Examples
251 |
252 | <.input field={{f, :email}} type="email" />
253 | <.input name="my-input" errors={["oh no!"]} />
254 | """
255 | attr :id, :any
256 | attr :name, :any
257 | attr :label, :string, default: nil
258 |
259 | attr :type, :string,
260 | default: "text",
261 | values: ~w(checkbox color date datetime-local email file hidden month number password
262 | range radio search select tel text textarea time url week)
263 |
264 | attr :value, :any
265 | attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
266 | attr :errors, :list
267 | attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
268 | attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
269 | attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
270 | attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
271 | attr :rest, :global, include: ~w(autocomplete cols disabled form max maxlength min minlength
272 | pattern placeholder readonly required rows size step)
273 | slot :inner_block
274 |
275 | def input(%{field: {f, field}} = assigns) do
276 | assigns
277 | |> assign(field: nil)
278 | |> assign_new(:name, fn ->
279 | name = Phoenix.HTML.Form.input_name(f, field)
280 | if assigns.multiple, do: name <> "[]", else: name
281 | end)
282 | |> assign_new(:id, fn -> Phoenix.HTML.Form.input_id(f, field) end)
283 | |> assign_new(:value, fn -> Phoenix.HTML.Form.input_value(f, field) end)
284 | |> assign_new(:errors, fn -> translate_errors(f.errors || [], field) end)
285 | |> input()
286 | end
287 |
288 | def input(%{type: "checkbox"} = assigns) do
289 | assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
290 |
291 | ~H"""
292 |
305 | """
306 | end
307 |
308 | def input(%{type: "select"} = assigns) do
309 | ~H"""
310 |
311 | <.label for={@id}><%= @label %>
312 |
322 | <.error :for={msg <- @errors}><%= msg %>
323 |
324 | """
325 | end
326 |
327 | def input(%{type: "textarea"} = assigns) do
328 | ~H"""
329 |
330 | <.label for={@id}><%= @label %>
331 |
343 | <.error :for={msg <- @errors}><%= msg %>
344 |
345 | """
346 | end
347 |
348 | def input(assigns) do
349 | ~H"""
350 |
351 | <.label for={@id}><%= @label %>
352 |
365 | <.error :for={msg <- @errors}><%= msg %>
366 |
367 | """
368 | end
369 |
370 | defp input_border([] = _errors),
371 | do: "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5"
372 |
373 | defp input_border([_ | _] = _errors),
374 | do: "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
375 |
376 | @doc """
377 | Renders a label.
378 | """
379 | attr :for, :string, default: nil
380 | slot :inner_block, required: true
381 |
382 | def label(assigns) do
383 | ~H"""
384 |
387 | """
388 | end
389 |
390 | @doc """
391 | Generates a generic error message.
392 | """
393 | slot :inner_block, required: true
394 |
395 | def error(assigns) do
396 | ~H"""
397 |
398 |
399 | <%= render_slot(@inner_block) %>
400 |
401 | """
402 | end
403 |
404 | @doc """
405 | Renders a header with title.
406 | """
407 | attr :class, :string, default: nil
408 |
409 | slot :inner_block, required: true
410 | slot :subtitle
411 | slot :actions
412 |
413 | def header(assigns) do
414 | ~H"""
415 |
426 | """
427 | end
428 |
429 | @doc ~S"""
430 | Renders a table with generic styling.
431 |
432 | ## Examples
433 |
434 | <.table id="users" rows={@users}>
435 | <:col :let={user} label="id"><%= user.id %>
436 | <:col :let={user} label="username"><%= user.username %>
437 |
438 | """
439 | attr :id, :string, required: true
440 | attr :row_click, :any, default: nil
441 | attr :rows, :list, required: true
442 |
443 | slot :col, required: true do
444 | attr :label, :string
445 | end
446 |
447 | slot :action, doc: "the slot for showing user actions in the last table column"
448 |
449 | def table(assigns) do
450 | ~H"""
451 |
452 |
453 |
454 |
455 | <%= col[:label] %> |
456 | <%= gettext("Actions") %> |
457 |
458 |
459 |
460 |
465 |
470 |
471 |
472 |
473 |
474 |
475 |
476 | <%= render_slot(col, row) %>
477 |
478 |
479 | |
480 |
481 |
482 |
486 | <%= render_slot(action, row) %>
487 |
488 |
489 | |
490 |
491 |
492 |
493 |
494 | """
495 | end
496 |
497 | @doc """
498 | Renders a data list.
499 |
500 | ## Examples
501 |
502 | <.list>
503 | <:item title="Title"><%= @post.title %>
504 | <:item title="Views"><%= @post.views %>
505 |
506 | """
507 | slot :item, required: true do
508 | attr :title, :string, required: true
509 | end
510 |
511 | def list(assigns) do
512 | ~H"""
513 |
514 |
515 |
516 |
- <%= item.title %>
517 | - <%= render_slot(item) %>
518 |
519 |
520 |
521 | """
522 | end
523 |
524 | @doc """
525 | Renders a back navigation link.
526 |
527 | ## Examples
528 |
529 | <.back navigate={~p"/posts"}>Back to posts
530 | """
531 | attr :navigate, :any, required: true
532 | slot :inner_block, required: true
533 |
534 | def back(assigns) do
535 | ~H"""
536 |
537 | <.link
538 | navigate={@navigate}
539 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
540 | >
541 |
542 | <%= render_slot(@inner_block) %>
543 |
544 |
545 | """
546 | end
547 |
548 | ## JS Commands
549 |
550 | def show(js \\ %JS{}, selector) do
551 | JS.show(js,
552 | to: selector,
553 | transition:
554 | {"transition-all transform ease-out duration-300",
555 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
556 | "opacity-100 translate-y-0 sm:scale-100"}
557 | )
558 | end
559 |
560 | def hide(js \\ %JS{}, selector) do
561 | JS.hide(js,
562 | to: selector,
563 | time: 200,
564 | transition:
565 | {"transition-all transform ease-in duration-200",
566 | "opacity-100 translate-y-0 sm:scale-100",
567 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
568 | )
569 | end
570 |
571 | def show_modal(js \\ %JS{}, id) when is_binary(id) do
572 | js
573 | |> JS.show(to: "##{id}")
574 | |> JS.show(
575 | to: "##{id}-bg",
576 | transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
577 | )
578 | |> show("##{id}-container")
579 | |> JS.add_class("overflow-hidden", to: "body")
580 | |> JS.focus_first(to: "##{id}-content")
581 | end
582 |
583 | def hide_modal(js \\ %JS{}, id) do
584 | js
585 | |> JS.hide(
586 | to: "##{id}-bg",
587 | transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
588 | )
589 | |> hide("##{id}-container")
590 | |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
591 | |> JS.remove_class("overflow-hidden", to: "body")
592 | |> JS.pop_focus()
593 | end
594 |
595 | @doc """
596 | Translates an error message using gettext.
597 | """
598 | def translate_error({msg, opts}) do
599 | # When using gettext, we typically pass the strings we want
600 | # to translate as a static argument:
601 | #
602 | # # Translate "is invalid" in the "errors" domain
603 | # dgettext("errors", "is invalid")
604 | #
605 | # # Translate the number of files with plural rules
606 | # dngettext("errors", "1 file", "%{count} files", count)
607 | #
608 | # Because the error messages we show in our forms and APIs
609 | # are defined inside Ecto, we need to translate them dynamically.
610 | # This requires us to call the Gettext module passing our gettext
611 | # backend as first argument.
612 | #
613 | # Note we use the "errors" domain, which means translations
614 | # should be written to the errors.po file. The :count option is
615 | # set by Ecto and indicates we should also apply plural rules.
616 | if count = opts[:count] do
617 | Gettext.dngettext(AppWeb.Gettext, "errors", msg, msg, count, opts)
618 | else
619 | Gettext.dgettext(AppWeb.Gettext, "errors", msg, opts)
620 | end
621 | end
622 |
623 | @doc """
624 | Translates the errors for a field from a keyword list of errors.
625 | """
626 | def translate_errors(errors, field) when is_list(errors) do
627 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
628 | end
629 |
630 | defp input_equals?(val1, val2) do
631 | Phoenix.HTML.html_escape(val1) == Phoenix.HTML.html_escape(val2)
632 | end
633 | end
634 |
--------------------------------------------------------------------------------
/lib/app_web/components/layouts.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.Layouts do
2 | use AppWeb, :html
3 |
4 | embed_templates "layouts/*"
5 | end
6 |
--------------------------------------------------------------------------------
/lib/app_web/components/layouts/app.html.heex:
--------------------------------------------------------------------------------
1 | <%= @inner_content %>
2 |
--------------------------------------------------------------------------------
/lib/app_web/components/layouts/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <.live_title suffix=" · Phoenix Framework">
8 | <%= assigns[:page_title] || "App" %>
9 |
10 |
11 |
13 |
14 |
15 | <%= @inner_content %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/lib/app_web/controllers/error_html.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.ErrorHTML do
2 | use AppWeb, :html
3 |
4 | # If you want to customize your error pages,
5 | # uncomment the embed_templates/1 call below
6 | # and add pages to the error directory:
7 | #
8 | # * lib/app_web/controllers/error_html/404.html.heex
9 | # * lib/app_web/controllers/error_html/500.html.heex
10 | #
11 | # embed_templates "error_html/*"
12 |
13 | # The default is to render a plain text page based on
14 | # the template name. For example, "404.html" becomes
15 | # "Not Found".
16 | def render(template, _assigns) do
17 | Phoenix.Controller.status_message_from_template(template)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/app_web/controllers/error_json.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.ErrorJSON do
2 | # If you want to customize a particular status code,
3 | # you may add your own clauses, such as:
4 | #
5 | # def render("500.json", _assigns) do
6 | # %{errors: %{detail: "Internal Server Error"}}
7 | # end
8 |
9 | # By default, Phoenix returns the status message from
10 | # the template name. For example, "404.json" becomes
11 | # "Not Found".
12 | def render(template, _assigns) do
13 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/app_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :app
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_app_key",
10 | signing_salt: "n2O/EQxI",
11 | same_site: "Lax"
12 | ]
13 |
14 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
15 |
16 | # Serve at "/" the static files from "priv/static" directory.
17 | #
18 | # You should set gzip to true if you are running phx.digest
19 | # when deploying your static files in production.
20 | plug Plug.Static,
21 | at: "/",
22 | from: :app,
23 | gzip: false,
24 | only: AppWeb.static_paths()
25 |
26 | # Code reloading can be explicitly enabled under the
27 | # :code_reloader configuration of your endpoint.
28 | if code_reloading? do
29 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
30 | plug Phoenix.LiveReloader
31 | plug Phoenix.CodeReloader
32 | end
33 |
34 | plug Phoenix.LiveDashboard.RequestLogger,
35 | param_key: "request_logger",
36 | cookie_key: "request_logger"
37 |
38 | plug Plug.RequestId
39 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
40 |
41 | plug Plug.Parsers,
42 | parsers: [:urlencoded, :multipart, :json],
43 | pass: ["*/*"],
44 | json_decoder: Phoenix.json_library()
45 |
46 | plug Plug.MethodOverride
47 | plug Plug.Head
48 | plug Plug.Session, @session_options
49 | plug AppWeb.Router
50 | end
51 |
--------------------------------------------------------------------------------
/lib/app_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import AppWeb.Gettext
9 |
10 | # Simple translation
11 | gettext("Here is the string to translate")
12 |
13 | # Plural translation
14 | ngettext("Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3)
17 |
18 | # Domain-based translation
19 | dgettext("errors", "Here is the error message to translate")
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :app
24 | end
25 |
--------------------------------------------------------------------------------
/lib/app_web/live/page_live.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.PageLive do
2 | @moduledoc """
3 | Page LiveView
4 | """
5 |
6 | use AppWeb, :live_view
7 |
8 | def mount(_params, _session, socket) do
9 | path = Path.join(:code.priv_dir(:app), "video.mp4")
10 |
11 | {:ok,
12 | socket
13 | |> assign(running?: false)
14 | |> assign(image: nil)
15 | |> assign(prediction: nil)
16 | |> assign(serving: serving())
17 | |> assign(video: Evision.VideoCapture.videoCapture(path))}
18 | end
19 |
20 | def render(assigns) do
21 | ~H"""
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |

32 |
33 |
34 | <%= @prediction %>
35 |
36 |
37 |
38 |
39 | """
40 | end
41 |
42 | def handle_event("start", _params, socket) do
43 | send(self(), :run)
44 |
45 | {:noreply, assign(socket, running?: true)}
46 | end
47 |
48 | def handle_info(:run, %{assigns: %{running?: true}} = socket) do
49 | frame = socket.assigns.video |> Evision.VideoCapture.read()
50 | prediction = predict(socket.assigns.serving, frame)
51 |
52 | send(self(), :run)
53 |
54 | {:noreply,
55 | socket
56 | |> assign(prediction: prediction)
57 | |> assign(image: Evision.imencode(".jpg", frame) |> Base.encode64())}
58 | end
59 |
60 | def handle_info(_msg, socket), do: {:noreply, socket}
61 |
62 | ###########
63 | # Private #
64 | ###########
65 |
66 | defp predict(serving, frame) do
67 | tensor = frame |> Evision.Mat.to_nx() |> Nx.backend_transfer()
68 |
69 | %{predictions: [%{label: label}]} = Nx.Serving.run(serving, tensor)
70 |
71 | label
72 | end
73 |
74 | defp serving do
75 | {:ok, model_info} = Bumblebee.load_model({:hf, "microsoft/resnet-50"})
76 | {:ok, featurizer} = Bumblebee.load_featurizer({:hf, "microsoft/resnet-50"})
77 |
78 | Bumblebee.Vision.image_classification(model_info, featurizer,
79 | top_k: 1,
80 | compile: [batch_size: 1],
81 | defn_options: [compiler: EXLA]
82 | )
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/app_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.Router do
2 | use AppWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_live_flash
8 | plug :put_root_layout, {AppWeb.Layouts, :root}
9 | plug :protect_from_forgery
10 | plug :put_secure_browser_headers
11 | end
12 |
13 | pipeline :api do
14 | plug :accepts, ["json"]
15 | end
16 |
17 | scope "/", AppWeb do
18 | pipe_through :browser
19 |
20 | live "/", PageLive, :index
21 | end
22 |
23 | # Other scopes may use custom stacks.
24 | # scope "/api", AppWeb do
25 | # pipe_through :api
26 | # end
27 |
28 | # Enable LiveDashboard and Swoosh mailbox preview in development
29 | if Application.compile_env(:app, :dev_routes) do
30 | # If you want to use the LiveDashboard in production, you should put
31 | # it behind authentication and allow only admins to access it.
32 | # If your application does not have an admins-only section yet,
33 | # you can use Plug.BasicAuth to set up some basic authentication
34 | # as long as you are also using SSL (which you should anyway).
35 | import Phoenix.LiveDashboard.Router
36 |
37 | scope "/dev" do
38 | pipe_through :browser
39 |
40 | live_dashboard "/dashboard", metrics: AppWeb.Telemetry
41 | forward "/mailbox", Plug.Swoosh.MailboxPreview
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/app_web/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.Telemetry do
2 | use Supervisor
3 | import Telemetry.Metrics
4 |
5 | def start_link(arg) do
6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
7 | end
8 |
9 | @impl true
10 | def init(_arg) do
11 | children = [
12 | # Telemetry poller will execute the given period measurements
13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
15 | # Add reporters as children of your supervision tree.
16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
17 | ]
18 |
19 | Supervisor.init(children, strategy: :one_for_one)
20 | end
21 |
22 | def metrics do
23 | [
24 | # Phoenix Metrics
25 | summary("phoenix.endpoint.start.system_time",
26 | unit: {:native, :millisecond}
27 | ),
28 | summary("phoenix.endpoint.stop.duration",
29 | unit: {:native, :millisecond}
30 | ),
31 | summary("phoenix.router_dispatch.start.system_time",
32 | tags: [:route],
33 | unit: {:native, :millisecond}
34 | ),
35 | summary("phoenix.router_dispatch.exception.duration",
36 | tags: [:route],
37 | unit: {:native, :millisecond}
38 | ),
39 | summary("phoenix.router_dispatch.stop.duration",
40 | tags: [:route],
41 | unit: {:native, :millisecond}
42 | ),
43 | summary("phoenix.socket_connected.duration",
44 | unit: {:native, :millisecond}
45 | ),
46 | summary("phoenix.channel_join.duration",
47 | unit: {:native, :millisecond}
48 | ),
49 | summary("phoenix.channel_handled_in.duration",
50 | tags: [:event],
51 | unit: {:native, :millisecond}
52 | ),
53 |
54 | # VM Metrics
55 | summary("vm.memory.total", unit: {:byte, :kilobyte}),
56 | summary("vm.total_run_queue_lengths.total"),
57 | summary("vm.total_run_queue_lengths.cpu"),
58 | summary("vm.total_run_queue_lengths.io")
59 | ]
60 | end
61 |
62 | defp periodic_measurements do
63 | [
64 | # A module, function and arguments to be invoked periodically.
65 | # This function must call :telemetry.execute/3 and a metric must be added above.
66 | # {AppWeb, :count_users, []}
67 | ]
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule App.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :app,
7 | version: "0.1.0",
8 | elixir: "~> 1.14",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | start_permanent: Mix.env() == :prod,
11 | aliases: aliases(),
12 | deps: deps()
13 | ]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [
21 | mod: {App.Application, []},
22 | extra_applications: [:logger, :runtime_tools]
23 | ]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "test/support"]
28 | defp elixirc_paths(_), do: ["lib"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.7.0-rc.0", override: true},
36 | {:phoenix_html, "~> 3.0"},
37 | {:phoenix_live_reload, "~> 1.2", only: :dev},
38 | {:phoenix_live_view, "~> 0.18.3"},
39 | {:heroicons, "~> 0.5"},
40 | {:floki, ">= 0.30.0", only: :test},
41 | {:phoenix_live_dashboard, "~> 0.7.2"},
42 | {:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
43 | {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
44 | {:swoosh, "~> 1.3"},
45 | {:finch, "~> 0.13"},
46 | {:telemetry_metrics, "~> 0.6"},
47 | {:telemetry_poller, "~> 1.0"},
48 | {:gettext, "~> 0.20"},
49 | {:jason, "~> 1.2"},
50 | {:plug_cowboy, "~> 2.5"},
51 | {:bumblebee, "~> 0.1.2"},
52 | {:exla, ">= 0.0.0"},
53 | {:evision, "~> 0.1.27"}
54 | ]
55 | end
56 |
57 | # Aliases are shortcuts or tasks specific to the current project.
58 | # For example, to install project dependencies and perform other setup tasks, run:
59 | #
60 | # $ mix setup
61 | #
62 | # See the documentation for `Mix` for more info on aliases.
63 | defp aliases do
64 | [
65 | setup: ["deps.get", "assets.setup"],
66 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
67 | "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
68 | ]
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "axon": {:hex, :axon, "0.3.1", "d2f678871d439ff623c50eecb255e2e89de9e2cd5d4bb7a40d9a80f3a46b86d1", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.4.0", [hex: :nx, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "01e0c085a3f26d0cccd44e29a2f308c537ad71d573bb853130f1fd484caccd55"},
3 | "bumblebee": {:hex, :bumblebee, "0.1.2", "bdc596400b0039df80400c49a9a3c8f0df2000427006dbbc02ba8c308f2b3881", [:mix], [{:axon, "~> 0.3.1", [hex: :axon, repo: "hexpm", optional: false]}, {:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.4.1", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.2.0", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}], "hexpm", "b360f533e30ac3e0a397aeb33f7f7ae100d26d1efe28f91a5774034bac45f29e"},
4 | "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
5 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.5", "ac3ef86f31ab579b856192a948e956cc3e4bb5006e303c4ab4b24958108e218a", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "ee5b2e56eb03798231a3d322579fff509139a534ef54205d04c188e18cab1f57"},
6 | "complex": {:hex, :complex, "0.4.3", "84db4aad241099a8785446ac6eacf498bf3a60634a0e45c7745d875714ddbf98", [:mix], [], "hexpm", "2ceda96ebddcc22697974f1a2666d4cc5dfdd34f8cd8c4f9dced037bcb41eeb5"},
7 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
8 | "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"},
9 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
10 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
11 | "dll_loader_helper": {:hex, :dll_loader_helper, "0.1.10", "ba85d66f82c1748513dbaee71aa9d0593bb9a65dba246b980753c4d683b0a07b", [:make, :mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}], "hexpm", "c0d02a2d8cd0085252f7551a343f89060bb7beb3f303d991e46a7370ed257485"},
12 | "elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"},
13 | "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"},
14 | "evision": {:hex, :evision, "0.1.27", "af3a4ea17c94a658d2bb6d681fd4d12a8e383a2568d95641c51d2d1d6d1ee5f0", [:make, :mix, :rebar3], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:dll_loader_helper, "~> 0.1", [hex: :dll_loader_helper, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "58586b53b38faacd740b836e55f6693995d90aeddf1c28a26ee830fbbcc101c0"},
15 | "exla": {:hex, :exla, "0.4.2", "7d5008c36c942de75efddffe4a4e6aac98da722261b7188b23b1363282a146a8", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.4.2", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "c7c5d70073c30ca4fee3981d992d27f2a2c1d8333b012ab8d0f7330c3624ee79"},
16 | "expo": {:hex, :expo, "0.3.0", "13127c1d5f653b2927f2616a4c9ace5ae372efd67c7c2693b87fd0fdc30c6feb", [:mix], [], "hexpm", "fb3cd4bf012a77bc1608915497dae2ff684a06f0fa633c7afa90c4d72b881823"},
17 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
18 | "finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
19 | "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
20 | "gettext": {:hex, :gettext, "0.22.0", "a25d71ec21b1848957d9207b81fd61cb25161688d282d58bdafef74c2270bdc4", [:mix], [{:expo, "~> 0.3.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "cb0675141576f73720c8e49b4f0fd3f2c69f0cd8c218202724d4aebab8c70ace"},
21 | "heroicons": {:hex, :heroicons, "0.5.2", "a7ae72460ecc4b74a4ba9e72f0b5ac3c6897ad08968258597da11c2b0b210683", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7ef96f455c1c136c335f1da0f1d7b12c34002c80a224ad96fc0ebf841a6ffef5"},
22 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
23 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
24 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
25 | "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
26 | "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
27 | "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
28 | "nx": {:hex, :nx, "0.4.2", "444e9cc1b1e95edf8c9d9d9f22635349a0cd60cb6a07d4954f3016b2d6d178d7", [:mix], [{:complex, "~> 0.4.3", [hex: :complex, repo: "hexpm", optional: false]}], "hexpm", "9d8f110cf733c4bbc86f0a5fe08f6537e106c39bbcb6dfabc7ef33f14f12edb3"},
29 | "nx_image": {:hex, :nx_image, "0.1.0", "ae10fa41fa95126f934d6160ef4320f7db583535fb868415f2562fe19969d245", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "60a2928164cdca540b4c180ff25579b97a5f2a650fc890d40db3e1a7798c93ad"},
30 | "phoenix": {:hex, :phoenix, "1.7.0-rc.2", "8faaff6f699aad2fe6a003c627da65d0864c868a4c10973ff90abfd7286c1f27", [: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.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "71abde2f67330c55b625dcc0e42bf76662dbadc7553c4f545c2f3759f40f7487"},
31 | "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
32 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
33 | "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"},
34 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [: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.1", [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", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"},
35 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
36 | "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
37 | "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [: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", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
38 | "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [: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", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
39 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
40 | "progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"},
41 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
42 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.5.5", "a075a92c8e748ce5c4f7b2cf573a072d206a6d8d99c53f627e81d3f2b10616a3", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e8a7f1abfec8d68683bb25d14efc88496f091ef113f7f4c45d39f3606f7223f6"},
43 | "swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
44 | "tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"},
45 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
46 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
47 | "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
48 | "tokenizers": {:hex, :tokenizers, "0.2.0", "3aa9811396680f849803f6a3978a310a653059613592710ce5f883d67ff17a33", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "2496fd44cf96bcefc70e75cf7e34126de8b63ccb9ad35967d6d5d8661cbdb6b7"},
49 | "unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
50 | "websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"},
51 | "websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [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.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"},
52 | "xla": {:hex, :xla, "0.4.3", "cf6201aaa44d990298996156a83a16b9a87c5fbb257758dbf4c3e83c5e1c4b96", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "caae164b56dcaec6fbcabcd7dea14303afde07623b0cfa4a3cd2576b923105f5"},
53 | }
54 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philipbrown/video-object-detection/e77ecc61f6e633600b3b0473b35d0140b682f6fe/preview.gif
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here has no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philipbrown/video-object-detection/e77ecc61f6e633600b3b0473b35d0140b682f6fe/priv/static/favicon.ico
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/priv/video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philipbrown/video-object-detection/e77ecc61f6e633600b3b0473b35d0140b682f6fe/priv/video.mp4
--------------------------------------------------------------------------------
/test/app_web/controllers/error_html_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.ErrorHTMLTest do
2 | use AppWeb.ConnCase, async: true
3 |
4 | # Bring render_to_string/4 for testing custom views
5 | import Phoenix.Template
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(AppWeb.ErrorHTML, "404", "html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(AppWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/app_web/controllers/error_json_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.ErrorJSONTest do
2 | use AppWeb.ConnCase, async: true
3 |
4 | test "renders 404" do
5 | assert AppWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6 | end
7 |
8 | test "renders 500" do
9 | assert AppWeb.ErrorJSON.render("500.json", %{}) ==
10 | %{errors: %{detail: "Internal Server Error"}}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/app_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.PageControllerTest do
2 | use AppWeb.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get(conn, ~p"/")
6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule AppWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use AppWeb.ConnCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # The default endpoint for testing
23 | @endpoint AppWeb.Endpoint
24 |
25 | use AppWeb, :verified_routes
26 |
27 | # Import conveniences for testing with connections
28 | import Plug.Conn
29 | import Phoenix.ConnTest
30 | import AppWeb.ConnCase
31 | end
32 | end
33 |
34 | setup _tags do
35 | {:ok, conn: Phoenix.ConnTest.build_conn()}
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------