diff --git a/.formatter.exs b/.formatter.exs
new file mode 100644
index 0000000..ef8840c
--- /dev/null
+++ b/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
+]
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9e29a1a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+aprs-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
diff --git a/README.md b/README.md
index b145260..a6053e9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,19 @@
-# aprs.me
\ No newline at end of file
+# Aprs
+
+To start your Phoenix server:
+
+ * Install dependencies with `mix deps.get`
+ * Create and migrate your database with `mix ecto.setup`
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/assets/css/app.css b/assets/css/app.css
new file mode 100644
index 0000000..378c8f9
--- /dev/null
+++ b/assets/css/app.css
@@ -0,0 +1,5 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+/* This file is for your main application CSS */
diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 0000000..44a8122
--- /dev/null
+++ b/assets/js/app.js
@@ -0,0 +1,41 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
+
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", info => topbar.delayedShow(200))
+window.addEventListener("phx:page-loading-stop", info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
+
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
new file mode 100644
index 0000000..b611701
--- /dev/null
+++ b/assets/tailwind.config.js
@@ -0,0 +1,26 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+const plugin = require("tailwindcss/plugin")
+
+module.exports = {
+ content: [
+ "./js/**/*.js",
+ "../lib/*_web.ex",
+ "../lib/*_web/**/*.*ex"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#FD4F00",
+ }
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
+ ]
+}
\ No newline at end of file
diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js
new file mode 100644
index 0000000..4176ede
--- /dev/null
+++ b/assets/vendor/topbar.js
@@ -0,0 +1,167 @@
+/**
+ * @license MIT
+ * topbar 1.0.0, 2021-01-06
+ * Modifications:
+ * - add delayedShow(time) (2022-09-21)
+ * http://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ delayedShow: function(time) {
+ if (showing) return;
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), time);
+ },
+ show: function () {
+ if (showing) return;
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/config/config.exs b/config/config.exs
new file mode 100644
index 0000000..57f610b
--- /dev/null
+++ b/config/config.exs
@@ -0,0 +1,64 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :aprs,
+ ecto_repos: [Aprs.Repo]
+
+# Configures the endpoint
+config :aprs, AprsWeb.Endpoint,
+ url: [host: "localhost"],
+ render_errors: [
+ formats: [html: AprsWeb.ErrorHTML, json: AprsWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: Aprs.PubSub,
+ live_view: [signing_salt: "ees098qG"]
+
+# Configures the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Local
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.14.41",
+ default: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configure tailwind (the version is required)
+config :tailwind,
+ version: "3.1.8",
+ default: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/app.css
+ --output=../priv/static/assets/app.css
+ ),
+ cd: Path.expand("../assets", __DIR__)
+ ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
new file mode 100644
index 0000000..594ba3b
--- /dev/null
+++ b/config/dev.exs
@@ -0,0 +1,80 @@
+import Config
+
+# Configure your database
+config :aprs, Aprs.Repo,
+ username: "postgres",
+ password: "postgres",
+ hostname: "localhost",
+ database: "aprs_dev",
+ stacktrace: true,
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we use it
+# with esbuild to bundle .js and .css sources.
+config :aprs, AprsWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "Vv8Uh9w7tSoac1wBo7A8MyTSE9J+RzNeAzPTGcsW2InUBHpPBgt1dACJrIZorfdH",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :aprs, AprsWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/aprs_web/(live|views)/.*(ex)$",
+ ~r"lib/aprs_web/templates/.*(eex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :aprs, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
new file mode 100644
index 0000000..416510b
--- /dev/null
+++ b/config/prod.exs
@@ -0,0 +1,21 @@
+import Config
+
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :aprs, AprsWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Configures Swoosh API Client
+config :swoosh, :api_client, Aprs.Finch
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/config/runtime.exs b/config/runtime.exs
new file mode 100644
index 0000000..57d1c65
--- /dev/null
+++ b/config/runtime.exs
@@ -0,0 +1,115 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/aprs start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :aprs, AprsWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+ maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
+
+ config :aprs, Aprs.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
+ socket_options: maybe_ipv6
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :aprs, AprsWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :aprs, AprsWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your endpoint, ensuring
+ # no data is ever sent via http, always redirecting to https:
+ #
+ # config :aprs, AprsWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Also, you may need to configure the Swoosh API client of your choice if you
+ # are not using SMTP. Here is an example of the configuration:
+ #
+ # config :aprs, Aprs.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # For this example you need include a HTTP client required by Swoosh API client.
+ # Swoosh supports Hackney and Finch out of the box:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+end
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 0000000..b7be0a8
--- /dev/null
+++ b/config/test.exs
@@ -0,0 +1,33 @@
+import Config
+
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :aprs, Aprs.Repo,
+ username: "postgres",
+ password: "postgres",
+ hostname: "localhost",
+ database: "aprs_test#{System.get_env("MIX_TEST_PARTITION")}",
+ pool: Ecto.Adapters.SQL.Sandbox,
+ pool_size: 10
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :aprs, AprsWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R",
+ server: false
+
+# In test we don't send emails.
+config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/lib/aprs.ex b/lib/aprs.ex
new file mode 100644
index 0000000..3470bde
--- /dev/null
+++ b/lib/aprs.ex
@@ -0,0 +1,9 @@
+defmodule Aprs do
+ @moduledoc """
+ Aprs keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/lib/aprs/application.ex b/lib/aprs/application.ex
new file mode 100644
index 0000000..e92cf58
--- /dev/null
+++ b/lib/aprs/application.ex
@@ -0,0 +1,38 @@
+defmodule Aprs.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ # Start the Telemetry supervisor
+ AprsWeb.Telemetry,
+ # Start the Ecto repository
+ Aprs.Repo,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: Aprs.PubSub},
+ # Start Finch
+ {Finch, name: Aprs.Finch},
+ # Start the Endpoint (http/https)
+ AprsWeb.Endpoint
+ # Start a worker by calling: Aprs.Worker.start_link(arg)
+ # {Aprs.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: Aprs.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ AprsWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/lib/aprs/mailer.ex b/lib/aprs/mailer.ex
new file mode 100644
index 0000000..579acb7
--- /dev/null
+++ b/lib/aprs/mailer.ex
@@ -0,0 +1,3 @@
+defmodule Aprs.Mailer do
+ use Swoosh.Mailer, otp_app: :aprs
+end
diff --git a/lib/aprs/repo.ex b/lib/aprs/repo.ex
new file mode 100644
index 0000000..1fda4f0
--- /dev/null
+++ b/lib/aprs/repo.ex
@@ -0,0 +1,5 @@
+defmodule Aprs.Repo do
+ use Ecto.Repo,
+ otp_app: :aprs,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/lib/aprs_web.ex b/lib/aprs_web.ex
new file mode 100644
index 0000000..3b935cc
--- /dev/null
+++ b/lib/aprs_web.ex
@@ -0,0 +1,114 @@
+defmodule AprsWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use AprsWeb, :controller
+ use AprsWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ namespace: AprsWeb,
+ formats: [:html, :json],
+ layouts: [html: AprsWeb.Layouts]
+
+ import Plug.Conn
+ import AprsWeb.Gettext
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {AprsWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import AprsWeb.CoreComponents
+ import AprsWeb.Gettext
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: AprsWeb.Endpoint,
+ router: AprsWeb.Router,
+ statics: AprsWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/lib/aprs_web/components/core_components.ex b/lib/aprs_web/components/core_components.ex
new file mode 100644
index 0000000..b8f21a8
--- /dev/null
+++ b/lib/aprs_web/components/core_components.ex
@@ -0,0 +1,623 @@
+defmodule AprsWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ The components in this module use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
+ customize the generated components in this module.
+
+ Icons are provided by [heroicons](https://heroicons.com), using the
+ [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+ import AprsWeb.Gettext
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ Are you sure?
+ <:confirm>OK
+ <:cancel>Cancel
+
+
+ JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
+ for the caller to react to each button press, for example:
+
+ <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
+ Are you sure you?
+ <:confirm>OK
+ <:cancel>Cancel
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ attr :on_confirm, JS, default: %JS{}
+
+ slot :inner_block, required: true
+ slot :title
+ slot :subtitle
+ slot :confirm
+ slot :cancel
+
+ def modal(assigns) do
+ ~H"""
+
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ transition:
+ {"transition-all transform ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all transform ease-in duration-200",
+ "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
+ js
+ |> JS.show(to: "##{id}")
+ |> JS.show(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+ )
+ |> show("##{id}-container")
+ |> JS.focus_first(to: "##{id}-content")
+ end
+
+ def hide_modal(js \\ %JS{}, id) do
+ js
+ |> JS.hide(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+ )
+ |> hide("##{id}-container")
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+ |> JS.pop_focus()
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate "is invalid" in the "errors" domain
+ # dgettext("errors", "is invalid")
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ # This requires us to call the Gettext module passing our gettext
+ # backend as first argument.
+ #
+ # Note we use the "errors" domain, which means translations
+ # should be written to the errors.po file. The :count option is
+ # set by Ecto and indicates we should also apply plural rules.
+ if count = opts[:count] do
+ Gettext.dngettext(AprsWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(AprsWeb.Gettext, "errors", msg, opts)
+ end
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+
+ defp input_equals?(val1, val2) do
+ Phoenix.HTML.html_escape(val1) == Phoenix.HTML.html_escape(val2)
+ end
+end
diff --git a/lib/aprs_web/components/layouts.ex b/lib/aprs_web/components/layouts.ex
new file mode 100644
index 0000000..61765fb
--- /dev/null
+++ b/lib/aprs_web/components/layouts.ex
@@ -0,0 +1,5 @@
+defmodule AprsWeb.Layouts do
+ use AprsWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/lib/aprs_web/components/layouts/app.html.heex b/lib/aprs_web/components/layouts/app.html.heex
new file mode 100644
index 0000000..fa10840
--- /dev/null
+++ b/lib/aprs_web/components/layouts/app.html.heex
@@ -0,0 +1,55 @@
+
+
+
diff --git a/lib/aprs_web/components/layouts/root.html.heex b/lib/aprs_web/components/layouts/root.html.heex
new file mode 100644
index 0000000..bef173c
--- /dev/null
+++ b/lib/aprs_web/components/layouts/root.html.heex
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <.live_title suffix=" ยท Phoenix Framework">
+ <%= assigns[:page_title] || "Aprs" %>
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/lib/aprs_web/controllers/error_html.ex b/lib/aprs_web/controllers/error_html.ex
new file mode 100644
index 0000000..611a0e1
--- /dev/null
+++ b/lib/aprs_web/controllers/error_html.ex
@@ -0,0 +1,19 @@
+defmodule AprsWeb.ErrorHTML do
+ use AprsWeb, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/aprs_web/controllers/error/404.html.heex
+ # * lib/aprs_web/controllers/error/500.html.heex
+ #
+ # embed_templates "error/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/lib/aprs_web/controllers/error_json.ex b/lib/aprs_web/controllers/error_json.ex
new file mode 100644
index 0000000..af69a0f
--- /dev/null
+++ b/lib/aprs_web/controllers/error_json.ex
@@ -0,0 +1,15 @@
+defmodule AprsWeb.ErrorJSON do
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/lib/aprs_web/controllers/page_controller.ex b/lib/aprs_web/controllers/page_controller.ex
new file mode 100644
index 0000000..cdc5ab4
--- /dev/null
+++ b/lib/aprs_web/controllers/page_controller.ex
@@ -0,0 +1,9 @@
+defmodule AprsWeb.PageController do
+ use AprsWeb, :controller
+
+ def home(conn, _params) do
+ # The home page is often custom made,
+ # so skip the default app layout.
+ render(conn, :home, layout: false)
+ end
+end
diff --git a/lib/aprs_web/controllers/page_html.ex b/lib/aprs_web/controllers/page_html.ex
new file mode 100644
index 0000000..7841d6a
--- /dev/null
+++ b/lib/aprs_web/controllers/page_html.ex
@@ -0,0 +1,5 @@
+defmodule AprsWeb.PageHTML do
+ use AprsWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/lib/aprs_web/controllers/page_html/home.html.heex b/lib/aprs_web/controllers/page_html/home.html.heex
new file mode 100644
index 0000000..ca89da6
--- /dev/null
+++ b/lib/aprs_web/controllers/page_html/home.html.heex
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+ Phoenix Framework
+
+ v1.7
+
+
+
+ Peace of mind from prototype to production.
+
+
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+