init commit
This commit is contained in:
commit
8668405143
|
@ -0,0 +1,2 @@
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
DATABASE_URL=ecto://postgres:password@localhost/here_i_am
|
|
@ -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"]
|
||||||
|
]
|
|
@ -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/
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
elixir 1.17.2
|
||||||
|
erlang 27.0
|
||||||
|
nodejs 20.14.0
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
/* This file is for your main application CSS */
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
//
|
||||||
|
// <div class="phx-click-loading:animate-ping">
|
||||||
|
//
|
||||||
|
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})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
|
@ -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));
|
|
@ -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"
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule HereIAm.Mailer do
|
||||||
|
use Swoosh.Mailer, otp_app: :here_i_am
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule HereIAm.Repo do
|
||||||
|
use Ecto.Repo,
|
||||||
|
otp_app: :here_i_am,
|
||||||
|
adapter: Ecto.Adapters.Postgres
|
||||||
|
end
|
|
@ -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
|
|
@ -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.
|
||||||
|
</.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.
|
||||||
|
</.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"""
|
||||||
|
<div
|
||||||
|
id={@id}
|
||||||
|
phx-mounted={@show && show_modal(@id)}
|
||||||
|
phx-remove={hide_modal(@id)}
|
||||||
|
data-cancel={JS.exec(@on_cancel, "phx-remove")}
|
||||||
|
class="relative z-50 hidden"
|
||||||
|
>
|
||||||
|
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 overflow-y-auto"
|
||||||
|
aria-labelledby={"#{@id}-title"}
|
||||||
|
aria-describedby={"#{@id}-description"}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div class="flex min-h-full items-center justify-center">
|
||||||
|
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
|
||||||
|
<.focus_wrap
|
||||||
|
id={"#{@id}-container"}
|
||||||
|
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
phx-key="escape"
|
||||||
|
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
|
||||||
|
>
|
||||||
|
<div class="absolute top-6 right-5">
|
||||||
|
<button
|
||||||
|
phx-click={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
type="button"
|
||||||
|
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
|
||||||
|
aria-label={gettext("close")}
|
||||||
|
>
|
||||||
|
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id={"#{@id}-content"}>
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</div>
|
||||||
|
</.focus_wrap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders flash notices.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.flash kind={:info} flash={@flash} />
|
||||||
|
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
|
||||||
|
"""
|
||||||
|
attr :id, :string, doc: "the optional id of flash container"
|
||||||
|
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
|
||||||
|
attr :title, :string, default: nil
|
||||||
|
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
|
||||||
|
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
|
||||||
|
|
||||||
|
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
||||||
|
|
||||||
|
def flash(assigns) do
|
||||||
|
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div
|
||||||
|
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
||||||
|
id={@id}
|
||||||
|
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
||||||
|
role="alert"
|
||||||
|
class={[
|
||||||
|
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
|
||||||
|
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
|
||||||
|
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
|
||||||
|
]}
|
||||||
|
{@rest}
|
||||||
|
>
|
||||||
|
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
||||||
|
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
|
||||||
|
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
|
||||||
|
<%= @title %>
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm leading-5"><%= msg %></p>
|
||||||
|
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
|
||||||
|
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Shows the flash group with standard titles and content.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.flash_group flash={@flash} />
|
||||||
|
"""
|
||||||
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||||
|
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
||||||
|
|
||||||
|
def flash_group(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div id={@id}>
|
||||||
|
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
|
||||||
|
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
|
||||||
|
<.flash
|
||||||
|
id="client-error"
|
||||||
|
kind={:error}
|
||||||
|
title={gettext("We can't find the internet")}
|
||||||
|
phx-disconnected={show(".phx-client-error #client-error")}
|
||||||
|
phx-connected={hide("#client-error")}
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<%= gettext("Attempting to reconnect") %>
|
||||||
|
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||||||
|
</.flash>
|
||||||
|
|
||||||
|
<.flash
|
||||||
|
id="server-error"
|
||||||
|
kind={:error}
|
||||||
|
title={gettext("Something went wrong!")}
|
||||||
|
phx-disconnected={show(".phx-server-error #server-error")}
|
||||||
|
phx-connected={hide("#server-error")}
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<%= gettext("Hang in there while we get back on track") %>
|
||||||
|
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||||||
|
</.flash>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a simple form.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.simple_form for={@form} phx-change="validate" phx-submit="save">
|
||||||
|
<.input field={@form[:email]} label="Email"/>
|
||||||
|
<.input field={@form[:username]} label="Username" />
|
||||||
|
<:actions>
|
||||||
|
<.button>Save</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
"""
|
||||||
|
attr :for, :any, required: true, doc: "the data structure for the form"
|
||||||
|
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
|
||||||
|
|
||||||
|
attr :rest, :global,
|
||||||
|
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
|
||||||
|
doc: "the arbitrary HTML attributes to apply to the form tag"
|
||||||
|
|
||||||
|
slot :inner_block, required: true
|
||||||
|
slot :actions, doc: "the slot for form actions, such as a submit button"
|
||||||
|
|
||||||
|
def simple_form(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||||
|
<div class="mt-10 space-y-8 bg-white">
|
||||||
|
<%= render_slot(@inner_block, f) %>
|
||||||
|
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||||
|
<%= render_slot(action, f) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</.form>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a button.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.button>Send!</.button>
|
||||||
|
<.button phx-click="go" class="ml-2">Send!</.button>
|
||||||
|
"""
|
||||||
|
attr :type, :string, default: nil
|
||||||
|
attr :class, :string, default: nil
|
||||||
|
attr :rest, :global, include: ~w(disabled form name value)
|
||||||
|
|
||||||
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
def button(assigns) do
|
||||||
|
~H"""
|
||||||
|
<button
|
||||||
|
type={@type}
|
||||||
|
class={[
|
||||||
|
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
|
||||||
|
"text-sm font-semibold leading-6 text-white active:text-white/80",
|
||||||
|
@class
|
||||||
|
]}
|
||||||
|
{@rest}
|
||||||
|
>
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</button>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders an input with label and error messages.
|
||||||
|
|
||||||
|
A `Phoenix.HTML.FormField` may be passed as argument,
|
||||||
|
which is used to retrieve the input name, id, and values.
|
||||||
|
Otherwise all attributes may be passed explicitly.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
This function accepts all HTML input types, considering that:
|
||||||
|
|
||||||
|
* You may also set `type="select"` to render a `<select>` tag
|
||||||
|
|
||||||
|
* `type="checkbox"` is used exclusively to render boolean values
|
||||||
|
|
||||||
|
* For live file uploads, see `Phoenix.Component.live_file_input/1`
|
||||||
|
|
||||||
|
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
||||||
|
for more information. Unsupported types, such as hidden and radio,
|
||||||
|
are best written directly in your templates.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.input field={@form[:email]} type="email" />
|
||||||
|
<.input name="my-input" errors={["oh no!"]} />
|
||||||
|
"""
|
||||||
|
attr :id, :any, default: nil
|
||||||
|
attr :name, :any
|
||||||
|
attr :label, :string, default: nil
|
||||||
|
attr :value, :any
|
||||||
|
|
||||||
|
attr :type, :string,
|
||||||
|
default: "text",
|
||||||
|
values: ~w(checkbox color date datetime-local email file month number password
|
||||||
|
range search select tel text textarea time url week)
|
||||||
|
|
||||||
|
attr :field, Phoenix.HTML.FormField,
|
||||||
|
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||||
|
|
||||||
|
attr :errors, :list, default: []
|
||||||
|
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
|
||||||
|
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||||
|
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||||
|
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||||
|
|
||||||
|
attr :rest, :global,
|
||||||
|
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
||||||
|
multiple pattern placeholder readonly required rows size step)
|
||||||
|
|
||||||
|
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||||
|
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
|
||||||
|
|
||||||
|
assigns
|
||||||
|
|> assign(field: nil, id: assigns.id || field.id)
|
||||||
|
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|
||||||
|
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||||
|
|> assign_new(:value, fn -> field.value end)
|
||||||
|
|> input()
|
||||||
|
end
|
||||||
|
|
||||||
|
def input(%{type: "checkbox"} = assigns) do
|
||||||
|
assigns =
|
||||||
|
assign_new(assigns, :checked, fn ->
|
||||||
|
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
||||||
|
end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
||||||
|
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={@id}
|
||||||
|
name={@name}
|
||||||
|
value="true"
|
||||||
|
checked={@checked}
|
||||||
|
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||||
|
{@rest}
|
||||||
|
/>
|
||||||
|
<%= @label %>
|
||||||
|
</label>
|
||||||
|
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def input(%{type: "select"} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.label for={@id}><%= @label %></.label>
|
||||||
|
<select
|
||||||
|
id={@id}
|
||||||
|
name={@name}
|
||||||
|
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
|
||||||
|
multiple={@multiple}
|
||||||
|
{@rest}
|
||||||
|
>
|
||||||
|
<option :if={@prompt} value=""><%= @prompt %></option>
|
||||||
|
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
||||||
|
</select>
|
||||||
|
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def input(%{type: "textarea"} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.label for={@id}><%= @label %></.label>
|
||||||
|
<textarea
|
||||||
|
id={@id}
|
||||||
|
name={@name}
|
||||||
|
class={[
|
||||||
|
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
|
||||||
|
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
||||||
|
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||||
|
]}
|
||||||
|
{@rest}
|
||||||
|
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
||||||
|
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||||
|
def input(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.label for={@id}><%= @label %></.label>
|
||||||
|
<input
|
||||||
|
type={@type}
|
||||||
|
name={@name}
|
||||||
|
id={@id}
|
||||||
|
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||||
|
class={[
|
||||||
|
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||||
|
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
||||||
|
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||||
|
]}
|
||||||
|
{@rest}
|
||||||
|
/>
|
||||||
|
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a label.
|
||||||
|
"""
|
||||||
|
attr :for, :string, default: nil
|
||||||
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
def label(assigns) do
|
||||||
|
~H"""
|
||||||
|
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</label>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a generic error message.
|
||||||
|
"""
|
||||||
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
def error(assigns) do
|
||||||
|
~H"""
|
||||||
|
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
|
||||||
|
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</h1>
|
||||||
|
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
||||||
|
<%= render_slot(@subtitle) %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none"><%= render_slot(@actions) %></div>
|
||||||
|
</header>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc ~S"""
|
||||||
|
Renders a table with generic styling.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.table id="users" rows={@users}>
|
||||||
|
<:col :let={user} label="id"><%= user.id %></:col>
|
||||||
|
<:col :let={user} label="username"><%= user.username %></:col>
|
||||||
|
</.table>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
|
||||||
|
<table class="w-[40rem] mt-11 sm:w-full">
|
||||||
|
<thead class="text-sm text-left leading-6 text-zinc-500">
|
||||||
|
<tr>
|
||||||
|
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
|
||||||
|
<th :if={@action != []} class="relative p-0 pb-4">
|
||||||
|
<span class="sr-only"><%= gettext("Actions") %></span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
id={@id}
|
||||||
|
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
|
||||||
|
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
|
||||||
|
>
|
||||||
|
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
|
||||||
|
<td
|
||||||
|
:for={{col, i} <- Enum.with_index(@col)}
|
||||||
|
phx-click={@row_click && @row_click.(row)}
|
||||||
|
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
||||||
|
>
|
||||||
|
<div class="block py-4 pr-6">
|
||||||
|
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
||||||
|
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
||||||
|
<%= render_slot(col, @row_item.(row)) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td :if={@action != []} class="relative w-14 p-0">
|
||||||
|
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
||||||
|
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
||||||
|
<span
|
||||||
|
:for={action <- @action}
|
||||||
|
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||||
|
>
|
||||||
|
<%= render_slot(action, @row_item.(row)) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a data list.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.list>
|
||||||
|
<:item title="Title"><%= @post.title %></:item>
|
||||||
|
<:item title="Views"><%= @post.views %></:item>
|
||||||
|
</.list>
|
||||||
|
"""
|
||||||
|
slot :item, required: true do
|
||||||
|
attr :title, :string, required: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def list(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="mt-14">
|
||||||
|
<dl class="-my-4 divide-y divide-zinc-100">
|
||||||
|
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
||||||
|
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
|
||||||
|
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a back navigation link.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.back navigate={~p"/posts"}>Back to posts</.back>
|
||||||
|
"""
|
||||||
|
attr :navigate, :any, required: true
|
||||||
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
def back(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="mt-16">
|
||||||
|
<.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) %>
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<span class={[@name, @class]} />
|
||||||
|
"""
|
||||||
|
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
|
|
@ -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
|
|
@ -0,0 +1,16 @@
|
||||||
|
<header class="px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a href="/">
|
||||||
|
<button class="btn btn-primary btn-outline">Here I Am</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900"></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div class="mx-auto max-w-2xl">
|
||||||
|
<.flash_group flash={@flash} />
|
||||||
|
<%= @inner_content %>
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="[scrollbar-gutter:stable]">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="csrf-token" content={get_csrf_token()} />
|
||||||
|
<.live_title suffix=" · Phoenix Framework">
|
||||||
|
<%= assigns[:page_title] || "HereIAm" %>
|
||||||
|
</.live_title>
|
||||||
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= @inner_content %>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
||||||
|
<.flash_group flash={@flash} />
|
||||||
|
|
||||||
|
<div class="container mx-auto text-center py-10">
|
||||||
|
<article class="prose mx-auto px-4">
|
||||||
|
<p>
|
||||||
|
This site allows you to set an MP3 to play when your device connects to the network.
|
||||||
|
</p>
|
||||||
|
<a href="/devices">
|
||||||
|
<button class="btn btn-primary mt-5">Manage Devices</button>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
</div>
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,83 @@
|
||||||
|
defmodule HereIAmWeb.DeviceLive.FormComponent do
|
||||||
|
use HereIAmWeb, :live_component
|
||||||
|
|
||||||
|
alias HereIAm.Devices
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.header>
|
||||||
|
<%= @title %>
|
||||||
|
<:subtitle>Use this form to manage device records in your database.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.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</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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
|
|
@ -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
|
|
@ -0,0 +1,47 @@
|
||||||
|
<.header>
|
||||||
|
Listing Devices
|
||||||
|
<:actions>
|
||||||
|
<.link patch={~p"/devices/new"}>
|
||||||
|
<.button>New Device</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.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>
|
||||||
|
<:col :let={{_id, device}} label="Audio"><%= device.audio %></:col>
|
||||||
|
<:action :let={{_id, device}}>
|
||||||
|
<div class="sr-only">
|
||||||
|
<.link navigate={~p"/devices/#{device}"}>Show</.link>
|
||||||
|
</div>
|
||||||
|
<.link patch={~p"/devices/#{device}/edit"}>Edit</.link>
|
||||||
|
</:action>
|
||||||
|
<:action :let={{id, device}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: device.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
|
||||||
|
<.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"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
|
@ -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
|
|
@ -0,0 +1,32 @@
|
||||||
|
<.header>
|
||||||
|
Device <%= @device.id %>
|
||||||
|
<:subtitle>This is a device record from your database.</:subtitle>
|
||||||
|
<:actions>
|
||||||
|
<.link patch={~p"/devices/#{@device}/show/edit"} phx-click={JS.push_focus()}>
|
||||||
|
<.button>Edit device</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.list>
|
||||||
|
<:item title="IP Address"><%= @device.ip_address %></:item>
|
||||||
|
<:item title="Audio"><%= @device.audio %></:item>
|
||||||
|
</.list>
|
||||||
|
|
||||||
|
<.back navigate={~p"/devices"}>Back to devices</.back>
|
||||||
|
|
||||||
|
<.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}"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"},
|
||||||
|
}
|
|
@ -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 ""
|
|
@ -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 ""
|
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
import_deps: [:ecto_sql],
|
||||||
|
inputs: ["*.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
|
|
@ -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.
|
Binary file not shown.
After Width: | Height: | Size: 152 B |
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
|
||||||
|
fill="#FD4F00"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -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: /
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(HereIAm.Repo, :manual)
|
Loading…
Reference in New Issue