From 8668405143dcf52468b037bf897ae9e25fa98bc9 Mon Sep 17 00:00:00 2001 From: Silas Date: Sat, 13 Jul 2024 15:12:19 -0400 Subject: [PATCH] init commit --- .env | 2 + .formatter.exs | 6 + .gitignore | 37 + .tool-versions | 3 + README.md | 18 + assets/css/app.css | 5 + assets/js/app.js | 44 ++ assets/package-lock.json | 164 +++++ assets/package.json | 14 + assets/tailwind.config.js | 76 ++ assets/vendor/topbar.js | 165 +++++ config/config.exs | 66 ++ config/dev.exs | 86 +++ config/prod.exs | 20 + config/runtime.exs | 117 +++ config/test.exs | 37 + docker-compose.yml | 13 + lib/here_i_am.ex | 9 + lib/here_i_am/application.ex | 36 + lib/here_i_am/devices.ex | 104 +++ lib/here_i_am/devices/device.ex | 18 + lib/here_i_am/mailer.ex | 3 + lib/here_i_am/repo.ex | 5 + lib/here_i_am_web.ex | 113 +++ .../components/core_components.ex | 676 ++++++++++++++++++ lib/here_i_am_web/components/layouts.ex | 14 + .../components/layouts/app.html.heex | 16 + .../components/layouts/root.html.heex | 17 + lib/here_i_am_web/controllers/error_html.ex | 24 + lib/here_i_am_web/controllers/error_json.ex | 21 + .../controllers/page_controller.ex | 9 + lib/here_i_am_web/controllers/page_html.ex | 10 + .../controllers/page_html/home.html.heex | 12 + lib/here_i_am_web/endpoint.ex | 53 ++ lib/here_i_am_web/gettext.ex | 24 + .../live/device_live/form_component.ex | 83 +++ lib/here_i_am_web/live/device_live/index.ex | 47 ++ .../live/device_live/index.html.heex | 47 ++ lib/here_i_am_web/live/device_live/show.ex | 21 + .../live/device_live/show.html.heex | 32 + lib/here_i_am_web/router.ex | 51 ++ lib/here_i_am_web/telemetry.ex | 92 +++ mix.exs | 86 +++ mix.lock | 41 ++ priv/gettext/en/LC_MESSAGES/errors.po | 112 +++ priv/gettext/errors.pot | 109 +++ priv/repo/migrations/.formatter.exs | 4 + .../20240713185614_create_devices.exs | 12 + priv/repo/seeds.exs | 11 + priv/static/favicon.ico | Bin 0 -> 152 bytes priv/static/images/logo.svg | 6 + priv/static/robots.txt | 5 + test/here_i_am/devices_test.exs | 61 ++ .../controllers/error_html_test.exs | 14 + .../controllers/error_json_test.exs | 12 + .../controllers/page_controller_test.exs | 8 + test/here_i_am_web/live/device_live_test.exs | 113 +++ test/support/conn_case.ex | 38 + test/support/data_case.ex | 58 ++ test/support/fixtures/devices_fixtures.ex | 21 + test/test_helper.exs | 2 + 61 files changed, 3123 insertions(+) create mode 100644 .env create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 .tool-versions create mode 100644 README.md create mode 100644 assets/css/app.css create mode 100644 assets/js/app.js create mode 100644 assets/package-lock.json create mode 100644 assets/package.json create mode 100644 assets/tailwind.config.js create mode 100644 assets/vendor/topbar.js create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/prod.exs create mode 100644 config/runtime.exs create mode 100644 config/test.exs create mode 100644 docker-compose.yml create mode 100644 lib/here_i_am.ex create mode 100644 lib/here_i_am/application.ex create mode 100644 lib/here_i_am/devices.ex create mode 100644 lib/here_i_am/devices/device.ex create mode 100644 lib/here_i_am/mailer.ex create mode 100644 lib/here_i_am/repo.ex create mode 100644 lib/here_i_am_web.ex create mode 100644 lib/here_i_am_web/components/core_components.ex create mode 100644 lib/here_i_am_web/components/layouts.ex create mode 100644 lib/here_i_am_web/components/layouts/app.html.heex create mode 100644 lib/here_i_am_web/components/layouts/root.html.heex create mode 100644 lib/here_i_am_web/controllers/error_html.ex create mode 100644 lib/here_i_am_web/controllers/error_json.ex create mode 100644 lib/here_i_am_web/controllers/page_controller.ex create mode 100644 lib/here_i_am_web/controllers/page_html.ex create mode 100644 lib/here_i_am_web/controllers/page_html/home.html.heex create mode 100644 lib/here_i_am_web/endpoint.ex create mode 100644 lib/here_i_am_web/gettext.ex create mode 100644 lib/here_i_am_web/live/device_live/form_component.ex create mode 100644 lib/here_i_am_web/live/device_live/index.ex create mode 100644 lib/here_i_am_web/live/device_live/index.html.heex create mode 100644 lib/here_i_am_web/live/device_live/show.ex create mode 100644 lib/here_i_am_web/live/device_live/show.html.heex create mode 100644 lib/here_i_am_web/router.ex create mode 100644 lib/here_i_am_web/telemetry.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 priv/gettext/en/LC_MESSAGES/errors.po create mode 100644 priv/gettext/errors.pot create mode 100644 priv/repo/migrations/.formatter.exs create mode 100644 priv/repo/migrations/20240713185614_create_devices.exs create mode 100644 priv/repo/seeds.exs create mode 100644 priv/static/favicon.ico create mode 100644 priv/static/images/logo.svg create mode 100644 priv/static/robots.txt create mode 100644 test/here_i_am/devices_test.exs create mode 100644 test/here_i_am_web/controllers/error_html_test.exs create mode 100644 test/here_i_am_web/controllers/error_json_test.exs create mode 100644 test/here_i_am_web/controllers/page_controller_test.exs create mode 100644 test/here_i_am_web/live/device_live_test.exs create mode 100644 test/support/conn_case.ex create mode 100644 test/support/data_case.ex create mode 100644 test/support/fixtures/devices_fixtures.ex create mode 100644 test/test_helper.exs diff --git a/.env b/.env new file mode 100644 index 0000000..39a9469 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +POSTGRES_PASSWORD=postgres +DATABASE_URL=ecto://postgres:password@localhost/here_i_am \ No newline at end of file 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..37ab347 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +here_i_am-*.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/.tool-versions b/.tool-versions new file mode 100644 index 0000000..a1999ec --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +elixir 1.17.2 +erlang 27.0 +nodejs 20.14.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e78746 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# HereIAm + +To start your Phoenix server: + + * Run `mix setup` to install and setup dependencies + * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). + +## Learn more + + * Official website: https://www.phoenixframework.org/ + * Guides: https://hexdocs.pm/phoenix/overview.html + * Docs: https://hexdocs.pm/phoenix + * Forum: https://elixirforum.com/c/phoenix-forum + * Source: https://github.com/phoenixframework/phoenix diff --git a/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..d5e278a --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,44 @@ +// 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, { + longPollFallbackMs: 2500, + 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.show(300)) +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/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..b2a1e2d --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,164 @@ +{ + "name": "here_i_am", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "here_i_am", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "daisyui": "^4.12.10" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.10", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz", + "integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..2bfdf43 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,14 @@ +{ + "name": "here_i_am", + "version": "1.0.0", + "main": "tailwind.config.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "silentsilas", + "license": "MIT", + "description": "", + "devDependencies": { + "daisyui": "^4.12.10" + } +} diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 0000000..7e170fa --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,76 @@ +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration + +const plugin = require("tailwindcss/plugin") +const fs = require("fs") +const path = require("path") + +module.exports = { + content: [ + "./js/**/*.js", + "../lib/here_i_am_web.ex", + "../lib/here_i_am_web/**/*.*ex" + ], + theme: { + extend: { + colors: { + brand: "#FD4F00", + } + }, + }, + plugins: [ + require('daisyui'), + require("@tailwindcss/forms"), + require("@tailwindcss/typography"), + // Allows prefixing tailwind classes with LiveView classes to add rules + // only when LiveView classes are applied, for example: + // + //
+ // + 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 &"])), + + // Embeds Heroicons (https://heroicons.com) into your app.css bundle + // See your `CoreComponents.icon/1` for more information. + // + plugin(function({matchComponents, theme}) { + let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") + let values = {} + let icons = [ + ["", "/24/outline"], + ["-solid", "/24/solid"], + ["-mini", "/20/solid"], + ["-micro", "/16/solid"] + ] + icons.forEach(([suffix, dir]) => { + fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { + let name = path.basename(file, ".svg") + suffix + values[name] = {name, fullPath: path.join(iconsDir, dir, file)} + }) + }) + matchComponents({ + "hero": ({name, fullPath}) => { + let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") + let size = theme("spacing.6") + if (name.endsWith("-mini")) { + size = theme("spacing.5") + } else if (name.endsWith("-micro")) { + size = theme("spacing.4") + } + return { + [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + "-webkit-mask": `var(--hero-${name})`, + "mask": `var(--hero-${name})`, + "mask-repeat": "no-repeat", + "background-color": "currentColor", + "vertical-align": "middle", + "display": "inline-block", + "width": size, + "height": size + } + } + }, {values}) + }) + ] +} diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js new file mode 100644 index 0000000..4195727 --- /dev/null +++ b/assets/vendor/topbar.js @@ -0,0 +1,165 @@ +/** + * @license MIT + * topbar 2.0.0, 2023-02-04 + * https://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]; + }, + show: function (delay) { + if (showing) return; + if (delay) { + if (delayTimerId) return; + delayTimerId = setTimeout(() => topbar.show(), delay); + } else { + 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..1a7987b --- /dev/null +++ b/config/config.exs @@ -0,0 +1,66 @@ +# 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 :here_i_am, + ecto_repos: [HereIAm.Repo], + generators: [timestamp_type: :utc_datetime] + +# Configures the endpoint +config :here_i_am, HereIAmWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [html: HereIAmWeb.ErrorHTML, json: HereIAmWeb.ErrorJSON], + layout: false + ], + pubsub_server: HereIAm.PubSub, + live_view: [signing_salt: "21LxsR26"] + +# 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 :here_i_am, HereIAm.Mailer, adapter: Swoosh.Adapters.Local + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.17.11", + here_i_am: [ + 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.4.3", + here_i_am: [ + 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..bf85ae6 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,86 @@ +import Config + +# Configure your database +config :here_i_am, HereIAm.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "here_i_am_dev", + stacktrace: true, + show_sensitive_data_on_connection_error: true, + pool_size: 10, + port: 5560 + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. +config :here_i_am, HereIAmWeb.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: "Gbwg2ryRZhpf0o2WT0gRGfi0ejovJ6TSW0DfE6atWDarlKZcKbPfJFM50Ogy2urv", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:here_i_am, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:here_i_am, ~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 :here_i_am, HereIAmWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/here_i_am_web/(controllers|live|components)/.*(ex|heex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :here_i_am, 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 + +config :phoenix_live_view, + # Include HEEx debug annotations as HTML comments in rendered markup + debug_heex_annotations: true, + # Enable helpful, but potentially expensive runtime checks + enable_expensive_runtime_checks: true + +# 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..fb0959d --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,20 @@ +import Config + +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix assets.deploy` task, +# which you should run after static files are built and +# before starting your production server. +config :here_i_am, HereIAmWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Configures Swoosh API Client +config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: HereIAm.Finch + +# Disable Swoosh Local Memory Storage +config :swoosh, local: false + +# 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..7c7cefe --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,117 @@ +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/here_i_am 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 :here_i_am, HereIAmWeb.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") in ~w(true 1), do: [:inet6], else: [] + + config :here_i_am, HereIAm.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 :here_i_am, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + + config :here_i_am, HereIAmWeb.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/bandit/Bandit.html#t:options/0 + # 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 :here_i_am, HereIAmWeb.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 config/prod.exs, + # ensuring no data is ever sent via http, always redirecting to https: + # + # config :here_i_am, HereIAmWeb.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 :here_i_am, HereIAm.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..0e15b79 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,37 @@ +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 :here_i_am, HereIAm.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "here_i_am_test#{System.get_env("MIX_TEST_PARTITION")}", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: System.schedulers_online() * 2 + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :here_i_am, HereIAmWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "AcBWPIRaCQMX4441ts2gwE8BpDzDabk6QE/soyKM8p7n1OeYNG8eAyEW3cZXuWb4", + server: false + +# In test we don't send emails +config :here_i_am, HereIAm.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 + +# Enable helpful, but potentially expensive runtime checks +config :phoenix_live_view, + enable_expensive_runtime_checks: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2de9760 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + + db: + image: postgres + restart: always + # set shared memory limit when using docker-compose + shm_size: 128mb + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - 5560:5432 \ No newline at end of file diff --git a/lib/here_i_am.ex b/lib/here_i_am.ex new file mode 100644 index 0000000..39ee209 --- /dev/null +++ b/lib/here_i_am.ex @@ -0,0 +1,9 @@ +defmodule HereIAm do + @moduledoc """ + HereIAm 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/here_i_am/application.ex b/lib/here_i_am/application.ex new file mode 100644 index 0000000..a6f491a --- /dev/null +++ b/lib/here_i_am/application.ex @@ -0,0 +1,36 @@ +defmodule HereIAm.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 = [ + HereIAmWeb.Telemetry, + HereIAm.Repo, + {DNSCluster, query: Application.get_env(:here_i_am, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: HereIAm.PubSub}, + # Start the Finch HTTP client for sending emails + {Finch, name: HereIAm.Finch}, + # Start a worker by calling: HereIAm.Worker.start_link(arg) + # {HereIAm.Worker, arg}, + # Start to serve requests, typically the last entry + HereIAmWeb.Endpoint + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: HereIAm.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 + HereIAmWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/lib/here_i_am/devices.ex b/lib/here_i_am/devices.ex new file mode 100644 index 0000000..79dd768 --- /dev/null +++ b/lib/here_i_am/devices.ex @@ -0,0 +1,104 @@ +defmodule HereIAm.Devices do + @moduledoc """ + The Devices context. + """ + + import Ecto.Query, warn: false + alias HereIAm.Repo + + alias HereIAm.Devices.Device + + @doc """ + Returns the list of devices. + + ## Examples + + iex> list_devices() + [%Device{}, ...] + + """ + def list_devices do + Repo.all(Device) + end + + @doc """ + Gets a single device. + + Raises `Ecto.NoResultsError` if the Device does not exist. + + ## Examples + + iex> get_device!(123) + %Device{} + + iex> get_device!(456) + ** (Ecto.NoResultsError) + + """ + def get_device!(id), do: Repo.get!(Device, id) + + @doc """ + Creates a device. + + ## Examples + + iex> create_device(%{field: value}) + {:ok, %Device{}} + + iex> create_device(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_device(attrs \\ %{}) do + %Device{} + |> Device.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a device. + + ## Examples + + iex> update_device(device, %{field: new_value}) + {:ok, %Device{}} + + iex> update_device(device, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_device(%Device{} = device, attrs) do + device + |> Device.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a device. + + ## Examples + + iex> delete_device(device) + {:ok, %Device{}} + + iex> delete_device(device) + {:error, %Ecto.Changeset{}} + + """ + def delete_device(%Device{} = device) do + Repo.delete(device) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking device changes. + + ## Examples + + iex> change_device(device) + %Ecto.Changeset{data: %Device{}} + + """ + def change_device(%Device{} = device, attrs \\ %{}) do + Device.changeset(device, attrs) + end +end diff --git a/lib/here_i_am/devices/device.ex b/lib/here_i_am/devices/device.ex new file mode 100644 index 0000000..b086449 --- /dev/null +++ b/lib/here_i_am/devices/device.ex @@ -0,0 +1,18 @@ +defmodule HereIAm.Devices.Device do + use Ecto.Schema + import Ecto.Changeset + + schema "devices" do + field :ip_address, :string + field :audio, :string + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(device, attrs) do + device + |> cast(attrs, [:ip_address, :audio]) + |> validate_required([:ip_address, :audio]) + end +end diff --git a/lib/here_i_am/mailer.ex b/lib/here_i_am/mailer.ex new file mode 100644 index 0000000..97e2b9f --- /dev/null +++ b/lib/here_i_am/mailer.ex @@ -0,0 +1,3 @@ +defmodule HereIAm.Mailer do + use Swoosh.Mailer, otp_app: :here_i_am +end diff --git a/lib/here_i_am/repo.ex b/lib/here_i_am/repo.ex new file mode 100644 index 0000000..f9072ee --- /dev/null +++ b/lib/here_i_am/repo.ex @@ -0,0 +1,5 @@ +defmodule HereIAm.Repo do + use Ecto.Repo, + otp_app: :here_i_am, + adapter: Ecto.Adapters.Postgres +end diff --git a/lib/here_i_am_web.ex b/lib/here_i_am_web.ex new file mode 100644 index 0000000..8e7c849 --- /dev/null +++ b/lib/here_i_am_web.ex @@ -0,0 +1,113 @@ +defmodule HereIAmWeb 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 HereIAmWeb, :controller + use HereIAmWeb, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: HereIAmWeb.Layouts] + + import Plug.Conn + import HereIAmWeb.Gettext + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {HereIAmWeb.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 HereIAmWeb.CoreComponents + import HereIAmWeb.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: HereIAmWeb.Endpoint, + router: HereIAmWeb.Router, + statics: HereIAmWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/live_view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/here_i_am_web/components/core_components.ex b/lib/here_i_am_web/components/core_components.ex new file mode 100644 index 0000000..1a34a67 --- /dev/null +++ b/lib/here_i_am_web/components/core_components.ex @@ -0,0 +1,676 @@ +defmodule HereIAmWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At first glance, this module may seem daunting, but its goal is to provide + core building blocks for your application, such as modals, tables, and + forms. The components consist mostly of markup and are well-documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + + The default components use Tailwind CSS, a utility-first CSS framework. + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn + how to customize them or feel free to swap in another framework altogether. + + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + import HereIAmWeb.Gettext + + @doc """ + Renders a modal. + + ## Examples + + <.modal id="confirm-modal"> + This is a modal. + + + JS commands may be passed to the `:on_cancel` to configure + the closing/cancel event, for example: + + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> + This is another modal. + + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + slot :inner_block, required: true + + def modal(assigns) do + ~H""" + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + @doc """ + Renders a label. + """ + attr :for, :string, default: nil + slot :inner_block, required: true + + def label(assigns) do + ~H""" + + """ + end + + @doc """ + Generates a generic error message. + """ + slot :inner_block, required: true + + def error(assigns) do + ~H""" +

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

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

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

+

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

+
+
<%= render_slot(@actions) %>
+
+ """ + end + + @doc ~S""" + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id"><%= user.id %> + <:col :let={user} label="username"><%= user.username %> + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" +
+ + + + + + + + + + + + + +
<%= col[:label] %> + <%= gettext("Actions") %> +
+
+ + + <%= render_slot(col, @row_item.(row)) %> + +
+
+
+ + + <%= render_slot(action, @row_item.(row)) %> + +
+
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title"><%= @post.title %> + <:item title="Views"><%= @post.views %> + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
+
+
+
<%= item.title %>
+
<%= render_slot(item) %>
+
+
+
+ """ + end + + @doc """ + Renders a back navigation link. + + ## Examples + + <.back navigate={~p"/posts"}>Back to posts + """ + attr :navigate, :any, required: true + slot :inner_block, required: true + + def back(assigns) do + ~H""" +
+ <.link + navigate={@navigate} + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" + > + <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> + <%= render_slot(@inner_block) %> + +
+ """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + width, height, and background color classes. + + Icons are extracted from the `deps/heroicons` directory and bundled within + your compiled app.css by the plugin in your `assets/tailwind.config.js`. + + ## Examples + + <.icon name="hero-x-mark-solid" /> + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :string, default: nil + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + time: 300, + 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", + time: 300, + transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} + ) + |> show("##{id}-container") + |> JS.add_class("overflow-hidden", to: "body") + |> JS.focus_first(to: "##{id}-content") + end + + def hide_modal(js \\ %JS{}, id) do + js + |> JS.hide( + to: "##{id}-bg", + transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} + ) + |> hide("##{id}-container") + |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) + |> JS.remove_class("overflow-hidden", to: "body") + |> JS.pop_focus() + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(HereIAmWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(HereIAmWeb.Gettext, "errors", msg, opts) + end + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end diff --git a/lib/here_i_am_web/components/layouts.ex b/lib/here_i_am_web/components/layouts.ex new file mode 100644 index 0000000..7a2c9fa --- /dev/null +++ b/lib/here_i_am_web/components/layouts.ex @@ -0,0 +1,14 @@ +defmodule HereIAmWeb.Layouts do + @moduledoc """ + This module holds different layouts used by your application. + + See the `layouts` directory for all templates available. + The "root" layout is a skeleton rendered as part of the + application router. The "app" layout is set as the default + layout on both `use HereIAmWeb, :controller` and + `use HereIAmWeb, :live_view`. + """ + use HereIAmWeb, :html + + embed_templates "layouts/*" +end diff --git a/lib/here_i_am_web/components/layouts/app.html.heex b/lib/here_i_am_web/components/layouts/app.html.heex new file mode 100644 index 0000000..967269b --- /dev/null +++ b/lib/here_i_am_web/components/layouts/app.html.heex @@ -0,0 +1,16 @@ +
+
+ +
+
+
+
+
+ <.flash_group flash={@flash} /> + <%= @inner_content %> +
+
diff --git a/lib/here_i_am_web/components/layouts/root.html.heex b/lib/here_i_am_web/components/layouts/root.html.heex new file mode 100644 index 0000000..e0a885b --- /dev/null +++ b/lib/here_i_am_web/components/layouts/root.html.heex @@ -0,0 +1,17 @@ + + + + + + + <.live_title suffix=" · Phoenix Framework"> + <%= assigns[:page_title] || "HereIAm" %> + + + + + + <%= @inner_content %> + + diff --git a/lib/here_i_am_web/controllers/error_html.ex b/lib/here_i_am_web/controllers/error_html.ex new file mode 100644 index 0000000..89483db --- /dev/null +++ b/lib/here_i_am_web/controllers/error_html.ex @@ -0,0 +1,24 @@ +defmodule HereIAmWeb.ErrorHTML do + @moduledoc """ + This module is invoked by your endpoint in case of errors on HTML requests. + + See config/config.exs. + """ + use HereIAmWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/here_i_am_web/controllers/error_html/404.html.heex + # * lib/here_i_am_web/controllers/error_html/500.html.heex + # + # embed_templates "error_html/*" + + # The default is to render a plain text page based on + # the template name. For example, "404.html" becomes + # "Not Found". + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/lib/here_i_am_web/controllers/error_json.ex b/lib/here_i_am_web/controllers/error_json.ex new file mode 100644 index 0000000..c42f959 --- /dev/null +++ b/lib/here_i_am_web/controllers/error_json.ex @@ -0,0 +1,21 @@ +defmodule HereIAmWeb.ErrorJSON do + @moduledoc """ + This module is invoked by your endpoint in case of errors on JSON requests. + + See config/config.exs. + """ + + # 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/here_i_am_web/controllers/page_controller.ex b/lib/here_i_am_web/controllers/page_controller.ex new file mode 100644 index 0000000..1ed57f8 --- /dev/null +++ b/lib/here_i_am_web/controllers/page_controller.ex @@ -0,0 +1,9 @@ +defmodule HereIAmWeb.PageController do + use HereIAmWeb, :controller + + def home(conn, _params) do + # The home page is often custom made, + # so skip the default app layout. + render(conn, :home) + end +end diff --git a/lib/here_i_am_web/controllers/page_html.ex b/lib/here_i_am_web/controllers/page_html.ex new file mode 100644 index 0000000..da2e76c --- /dev/null +++ b/lib/here_i_am_web/controllers/page_html.ex @@ -0,0 +1,10 @@ +defmodule HereIAmWeb.PageHTML do + @moduledoc """ + This module contains pages rendered by PageController. + + See the `page_html` directory for all templates available. + """ + use HereIAmWeb, :html + + embed_templates "page_html/*" +end diff --git a/lib/here_i_am_web/controllers/page_html/home.html.heex b/lib/here_i_am_web/controllers/page_html/home.html.heex new file mode 100644 index 0000000..d10ea00 --- /dev/null +++ b/lib/here_i_am_web/controllers/page_html/home.html.heex @@ -0,0 +1,12 @@ +<.flash_group flash={@flash} /> + +
+
+

+ This site allows you to set an MP3 to play when your device connects to the network. +

+ + + +
+
diff --git a/lib/here_i_am_web/endpoint.ex b/lib/here_i_am_web/endpoint.ex new file mode 100644 index 0000000..5ef28f9 --- /dev/null +++ b/lib/here_i_am_web/endpoint.ex @@ -0,0 +1,53 @@ +defmodule HereIAmWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :here_i_am + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + @session_options [ + store: :cookie, + key: "_here_i_am_key", + signing_salt: "WZbxmApa", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]], + longpoll: [connect_info: [session: @session_options]] + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :here_i_am, + gzip: false, + only: HereIAmWeb.static_paths() + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :here_i_am + end + + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug HereIAmWeb.Router +end diff --git a/lib/here_i_am_web/gettext.ex b/lib/here_i_am_web/gettext.ex new file mode 100644 index 0000000..9e74fd6 --- /dev/null +++ b/lib/here_i_am_web/gettext.ex @@ -0,0 +1,24 @@ +defmodule HereIAmWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import HereIAmWeb.Gettext + + # Simple translation + gettext("Here is the string to translate") + + # Plural translation + ngettext("Here is the string to translate", + "Here are the strings to translate", + 3) + + # Domain-based translation + dgettext("errors", "Here is the error message to translate") + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :here_i_am +end diff --git a/lib/here_i_am_web/live/device_live/form_component.ex b/lib/here_i_am_web/live/device_live/form_component.ex new file mode 100644 index 0000000..88b2a8b --- /dev/null +++ b/lib/here_i_am_web/live/device_live/form_component.ex @@ -0,0 +1,83 @@ +defmodule HereIAmWeb.DeviceLive.FormComponent do + use HereIAmWeb, :live_component + + alias HereIAm.Devices + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage device records in your database. + + + <.simple_form + for={@form} + id="device-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:ip_address]} type="text" label="IP Address" /> + <.input field={@form[:audio]} type="text" label="Audio" /> + <:actions> + <.button phx-disable-with="Saving...">Save Device + + +
+ """ + end + + @impl true + def update(%{device: device} = assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_new(:form, fn -> + to_form(Devices.change_device(device)) + end)} + end + + @impl true + def handle_event("validate", %{"device" => device_params}, socket) do + changeset = Devices.change_device(socket.assigns.device, device_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"device" => device_params}, socket) do + save_device(socket, socket.assigns.action, device_params) + end + + defp save_device(socket, :edit, device_params) do + case Devices.update_device(socket.assigns.device, device_params) do + {:ok, device} -> + notify_parent({:saved, device}) + + {:noreply, + socket + |> put_flash(:info, "Device updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_device(socket, :new, device_params) do + case Devices.create_device(device_params) do + {:ok, device} -> + notify_parent({:saved, device}) + + {:noreply, + socket + |> put_flash(:info, "Device created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/here_i_am_web/live/device_live/index.ex b/lib/here_i_am_web/live/device_live/index.ex new file mode 100644 index 0000000..9eb1585 --- /dev/null +++ b/lib/here_i_am_web/live/device_live/index.ex @@ -0,0 +1,47 @@ +defmodule HereIAmWeb.DeviceLive.Index do + use HereIAmWeb, :live_view + + alias HereIAm.Devices + alias HereIAm.Devices.Device + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :devices, Devices.list_devices())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Device") + |> assign(:device, Devices.get_device!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Device") + |> assign(:device, %Device{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Devices") + |> assign(:device, nil) + end + + @impl true + def handle_info({HereIAmWeb.DeviceLive.FormComponent, {:saved, device}}, socket) do + {:noreply, stream_insert(socket, :devices, device)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + device = Devices.get_device!(id) + {:ok, _} = Devices.delete_device(device) + + {:noreply, stream_delete(socket, :devices, device)} + end +end diff --git a/lib/here_i_am_web/live/device_live/index.html.heex b/lib/here_i_am_web/live/device_live/index.html.heex new file mode 100644 index 0000000..9efa015 --- /dev/null +++ b/lib/here_i_am_web/live/device_live/index.html.heex @@ -0,0 +1,47 @@ +<.header> + Listing Devices + <:actions> + <.link patch={~p"/devices/new"}> + <.button>New Device + + + + +<.table + id="devices" + rows={@streams.devices} + row_click={fn {_id, device} -> JS.navigate(~p"/devices/#{device}") end} +> + <:col :let={{_id, device}} label="IP Address"><%= device.ip_address %> + <:col :let={{_id, device}} label="Audio"><%= device.audio %> + <:action :let={{_id, device}}> +
+ <.link navigate={~p"/devices/#{device}"}>Show +
+ <.link patch={~p"/devices/#{device}/edit"}>Edit + + <:action :let={{id, device}}> + <.link + phx-click={JS.push("delete", value: %{id: device.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="device-modal" + show + on_cancel={JS.patch(~p"/devices")} +> + <.live_component + module={HereIAmWeb.DeviceLive.FormComponent} + id={@device.id || :new} + title={@page_title} + action={@live_action} + device={@device} + patch={~p"/devices"} + /> + diff --git a/lib/here_i_am_web/live/device_live/show.ex b/lib/here_i_am_web/live/device_live/show.ex new file mode 100644 index 0000000..6c8545f --- /dev/null +++ b/lib/here_i_am_web/live/device_live/show.ex @@ -0,0 +1,21 @@ +defmodule HereIAmWeb.DeviceLive.Show do + use HereIAmWeb, :live_view + + alias HereIAm.Devices + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:device, Devices.get_device!(id))} + end + + defp page_title(:show), do: "Show Device" + defp page_title(:edit), do: "Edit Device" +end diff --git a/lib/here_i_am_web/live/device_live/show.html.heex b/lib/here_i_am_web/live/device_live/show.html.heex new file mode 100644 index 0000000..10d8793 --- /dev/null +++ b/lib/here_i_am_web/live/device_live/show.html.heex @@ -0,0 +1,32 @@ +<.header> + Device <%= @device.id %> + <:subtitle>This is a device record from your database. + <:actions> + <.link patch={~p"/devices/#{@device}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit device + + + + +<.list> + <:item title="IP Address"><%= @device.ip_address %> + <:item title="Audio"><%= @device.audio %> + + +<.back navigate={~p"/devices"}>Back to devices + +<.modal + :if={@live_action == :edit} + id="device-modal" + show + on_cancel={JS.patch(~p"/devices/#{@device}")} +> + <.live_component + module={HereIAmWeb.DeviceLive.FormComponent} + id={@device.id} + title={@page_title} + action={@live_action} + device={@device} + patch={~p"/devices/#{@device}"} + /> + diff --git a/lib/here_i_am_web/router.ex b/lib/here_i_am_web/router.ex new file mode 100644 index 0000000..597438f --- /dev/null +++ b/lib/here_i_am_web/router.ex @@ -0,0 +1,51 @@ +defmodule HereIAmWeb.Router do + use HereIAmWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {HereIAmWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", HereIAmWeb do + pipe_through :browser + + get "/", PageController, :home + + live "/devices", DeviceLive.Index, :index + live "/devices/new", DeviceLive.Index, :new + live "/devices/:id/edit", DeviceLive.Index, :edit + + live "/devices/:id", DeviceLive.Show, :show + live "/devices/:id/show/edit", DeviceLive.Show, :edit + end + + # Other scopes may use custom stacks. + # scope "/api", HereIAmWeb do + # pipe_through :api + # end + + # Enable LiveDashboard and Swoosh mailbox preview in development + if Application.compile_env(:here_i_am, :dev_routes) do + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + import Phoenix.LiveDashboard.Router + + scope "/dev" do + pipe_through :browser + + live_dashboard "/dashboard", metrics: HereIAmWeb.Telemetry + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + end +end diff --git a/lib/here_i_am_web/telemetry.ex b/lib/here_i_am_web/telemetry.ex new file mode 100644 index 0000000..ae32a77 --- /dev/null +++ b/lib/here_i_am_web/telemetry.ex @@ -0,0 +1,92 @@ +defmodule HereIAmWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.start.system_time", + unit: {:native, :millisecond} + ), + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.start.system_time", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.exception.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.socket_connected.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_joined.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("here_i_am.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("here_i_am.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("here_i_am.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("here_i_am.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("here_i_am.repo.query.idle_time", + unit: {:native, :millisecond}, + description: + "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {HereIAmWeb, :count_users, []} + ] + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d5dbe8b --- /dev/null +++ b/mix.exs @@ -0,0 +1,86 @@ +defmodule HereIAm.MixProject do + use Mix.Project + + def project do + [ + app: :here_i_am, + version: "0.1.0", + elixir: "~> 1.17", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {HereIAm.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:phoenix, "~> 1.7.14"}, + {:phoenix_ecto, "~> 4.5"}, + {:ecto_sql, "~> 3.10"}, + {:postgrex, ">= 0.0.0"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, + {:phoenix_live_view, "~> 1.0.0-rc.1", override: true}, + {:floki, ">= 0.30.0", only: :test}, + {:phoenix_live_dashboard, "~> 0.8.3"}, + {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, + {:heroicons, + github: "tailwindlabs/heroicons", + tag: "v2.1.1", + sparse: "optimized", + app: false, + compile: false, + depth: 1}, + {:swoosh, "~> 1.5"}, + {:finch, "~> 0.13"}, + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:gettext, "~> 0.20"}, + {:jason, "~> 1.2"}, + {:dns_cluster, "~> 0.1.1"}, + {:bandit, "~> 1.5"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to install project dependencies and perform other setup tasks, run: + # + # $ mix setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], + "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["tailwind here_i_am", "esbuild here_i_am"], + "assets.deploy": [ + "tailwind here_i_am --minify", + "esbuild here_i_am --minify", + "phx.digest" + ] + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..f0734cf --- /dev/null +++ b/mix.lock @@ -0,0 +1,41 @@ +%{ + "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, + "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, + "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [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 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, + "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, + "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, + "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [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]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, + "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, + "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, +} diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po new file mode 100644 index 0000000..844c4f5 --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -0,0 +1,112 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" + +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot new file mode 100644 index 0000000..eef2de2 --- /dev/null +++ b/priv/gettext/errors.pot @@ -0,0 +1,109 @@ +## This is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here has no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs new file mode 100644 index 0000000..49f9151 --- /dev/null +++ b/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/priv/repo/migrations/20240713185614_create_devices.exs b/priv/repo/migrations/20240713185614_create_devices.exs new file mode 100644 index 0000000..a90f80e --- /dev/null +++ b/priv/repo/migrations/20240713185614_create_devices.exs @@ -0,0 +1,12 @@ +defmodule HereIAm.Repo.Migrations.CreateDevices do + use Ecto.Migration + + def change do + create table(:devices) do + add :ip_address, :string + add :audio, :string + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs new file mode 100644 index 0000000..d300466 --- /dev/null +++ b/priv/repo/seeds.exs @@ -0,0 +1,11 @@ +# Script for populating the database. You can run it as: +# +# mix run priv/repo/seeds.exs +# +# Inside the script, you can read and write to any of your +# repositories directly: +# +# HereIAm.Repo.insert!(%HereIAm.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f372bfc21cdd8cb47585339d5fa4d9dd424402f GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=@t!V@Ar*{oFEH`~d50E!_s``s q?{G*w(7?#d#v@^nKnY_HKaYb01EZMZjMqTJ89ZJ6T-G@yGywoKK_h|y literal 0 HcmV?d00001 diff --git a/priv/static/images/logo.svg b/priv/static/images/logo.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/priv/static/images/logo.svg @@ -0,0 +1,6 @@ + diff --git a/priv/static/robots.txt b/priv/static/robots.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test/here_i_am/devices_test.exs b/test/here_i_am/devices_test.exs new file mode 100644 index 0000000..25e9396 --- /dev/null +++ b/test/here_i_am/devices_test.exs @@ -0,0 +1,61 @@ +defmodule HereIAm.DevicesTest do + use HereIAm.DataCase + + alias HereIAm.Devices + + describe "devices" do + alias HereIAm.Devices.Device + + import HereIAm.DevicesFixtures + + @invalid_attrs %{ip_address: nil, audio: nil} + + test "list_devices/0 returns all devices" do + device = device_fixture() + assert Devices.list_devices() == [device] + end + + test "get_device!/1 returns the device with given id" do + device = device_fixture() + assert Devices.get_device!(device.id) == device + end + + test "create_device/1 with valid data creates a device" do + valid_attrs = %{ip_address: "some ip_address", audio: "some audio"} + + assert {:ok, %Device{} = device} = Devices.create_device(valid_attrs) + assert device.ip_address == "some ip_address" + assert device.audio == "some audio" + end + + test "create_device/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Devices.create_device(@invalid_attrs) + end + + test "update_device/2 with valid data updates the device" do + device = device_fixture() + update_attrs = %{ip_address: "some updated ip_address", audio: "some updated audio"} + + assert {:ok, %Device{} = device} = Devices.update_device(device, update_attrs) + assert device.ip_address == "some updated ip_address" + assert device.audio == "some updated audio" + end + + test "update_device/2 with invalid data returns error changeset" do + device = device_fixture() + assert {:error, %Ecto.Changeset{}} = Devices.update_device(device, @invalid_attrs) + assert device == Devices.get_device!(device.id) + end + + test "delete_device/1 deletes the device" do + device = device_fixture() + assert {:ok, %Device{}} = Devices.delete_device(device) + assert_raise Ecto.NoResultsError, fn -> Devices.get_device!(device.id) end + end + + test "change_device/1 returns a device changeset" do + device = device_fixture() + assert %Ecto.Changeset{} = Devices.change_device(device) + end + end +end diff --git a/test/here_i_am_web/controllers/error_html_test.exs b/test/here_i_am_web/controllers/error_html_test.exs new file mode 100644 index 0000000..8dc10af --- /dev/null +++ b/test/here_i_am_web/controllers/error_html_test.exs @@ -0,0 +1,14 @@ +defmodule HereIAmWeb.ErrorHTMLTest do + use HereIAmWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template + + test "renders 404.html" do + assert render_to_string(HereIAmWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(HereIAmWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/test/here_i_am_web/controllers/error_json_test.exs b/test/here_i_am_web/controllers/error_json_test.exs new file mode 100644 index 0000000..f4e53eb --- /dev/null +++ b/test/here_i_am_web/controllers/error_json_test.exs @@ -0,0 +1,12 @@ +defmodule HereIAmWeb.ErrorJSONTest do + use HereIAmWeb.ConnCase, async: true + + test "renders 404" do + assert HereIAmWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert HereIAmWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/test/here_i_am_web/controllers/page_controller_test.exs b/test/here_i_am_web/controllers/page_controller_test.exs new file mode 100644 index 0000000..4e0df49 --- /dev/null +++ b/test/here_i_am_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule HereIAmWeb.PageControllerTest do + use HereIAmWeb.ConnCase + + test "GET /", %{conn: conn} do + conn = get(conn, ~p"/") + assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + end +end diff --git a/test/here_i_am_web/live/device_live_test.exs b/test/here_i_am_web/live/device_live_test.exs new file mode 100644 index 0000000..fb927d5 --- /dev/null +++ b/test/here_i_am_web/live/device_live_test.exs @@ -0,0 +1,113 @@ +defmodule HereIAmWeb.DeviceLiveTest do + use HereIAmWeb.ConnCase + + import Phoenix.LiveViewTest + import HereIAm.DevicesFixtures + + @create_attrs %{ip_address: "some ip_address", audio: "some audio"} + @update_attrs %{ip_address: "some updated ip_address", audio: "some updated audio"} + @invalid_attrs %{ip_address: nil, audio: nil} + + defp create_device(_) do + device = device_fixture() + %{device: device} + end + + describe "Index" do + setup [:create_device] + + test "lists all devices", %{conn: conn, device: device} do + {:ok, _index_live, html} = live(conn, ~p"/devices") + + assert html =~ "Listing Devices" + assert html =~ device.ip_address + end + + test "saves new device", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/devices") + + assert index_live |> element("a", "New Device") |> render_click() =~ + "New Device" + + assert_patch(index_live, ~p"/devices/new") + + assert index_live + |> form("#device-form", device: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#device-form", device: @create_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/devices") + + html = render(index_live) + assert html =~ "Device created successfully" + assert html =~ "some ip_address" + end + + test "updates device in listing", %{conn: conn, device: device} do + {:ok, index_live, _html} = live(conn, ~p"/devices") + + assert index_live |> element("#devices-#{device.id} a", "Edit") |> render_click() =~ + "Edit Device" + + assert_patch(index_live, ~p"/devices/#{device}/edit") + + assert index_live + |> form("#device-form", device: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#device-form", device: @update_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/devices") + + html = render(index_live) + assert html =~ "Device updated successfully" + assert html =~ "some updated ip_address" + end + + test "deletes device in listing", %{conn: conn, device: device} do + {:ok, index_live, _html} = live(conn, ~p"/devices") + + assert index_live |> element("#devices-#{device.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#devices-#{device.id}") + end + end + + describe "Show" do + setup [:create_device] + + test "displays device", %{conn: conn, device: device} do + {:ok, _show_live, html} = live(conn, ~p"/devices/#{device}") + + assert html =~ "Show Device" + assert html =~ device.ip_address + end + + test "updates device within modal", %{conn: conn, device: device} do + {:ok, show_live, _html} = live(conn, ~p"/devices/#{device}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Device" + + assert_patch(show_live, ~p"/devices/#{device}/show/edit") + + assert show_live + |> form("#device-form", device: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert show_live + |> form("#device-form", device: @update_attrs) + |> render_submit() + + assert_patch(show_live, ~p"/devices/#{device}") + + html = render(show_live) + assert html =~ "Device updated successfully" + assert html =~ "some updated ip_address" + end + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex new file mode 100644 index 0000000..637fc5c --- /dev/null +++ b/test/support/conn_case.ex @@ -0,0 +1,38 @@ +defmodule HereIAmWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use HereIAmWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # The default endpoint for testing + @endpoint HereIAmWeb.Endpoint + + use HereIAmWeb, :verified_routes + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import HereIAmWeb.ConnCase + end + end + + setup tags do + HereIAm.DataCase.setup_sandbox(tags) + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 0000000..c82f14e --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,58 @@ +defmodule HereIAm.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use HereIAm.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + alias HereIAm.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import HereIAm.DataCase + end + end + + setup tags do + HereIAm.DataCase.setup_sandbox(tags) + :ok + end + + @doc """ + Sets up the sandbox based on the test tags. + """ + def setup_sandbox(tags) do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(HereIAm.Repo, shared: not tags[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end +end diff --git a/test/support/fixtures/devices_fixtures.ex b/test/support/fixtures/devices_fixtures.ex new file mode 100644 index 0000000..6ae0098 --- /dev/null +++ b/test/support/fixtures/devices_fixtures.ex @@ -0,0 +1,21 @@ +defmodule HereIAm.DevicesFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `HereIAm.Devices` context. + """ + + @doc """ + Generate a device. + """ + def device_fixture(attrs \\ %{}) do + {:ok, device} = + attrs + |> Enum.into(%{ + audio: "some audio", + ip_address: "some ip_address" + }) + |> HereIAm.Devices.create_device() + + device + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..46f7e6f --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(HereIAm.Repo, :manual)