update to phoenix 1.6

This commit is contained in:
shibao 2022-01-21 20:36:25 -05:00
parent f9e66d1336
commit 094e214a55
39 changed files with 621 additions and 340 deletions

4
.gitignore vendored
View File

@ -25,7 +25,7 @@ cannery-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
# The directory NPM downloads your dependencies sources to.
# Ignore assets that are produced by build tools.
/assets/node_modules/
# Since we are building assets from assets/,
@ -36,4 +36,4 @@ npm-debug.log
.elixir_ls/
# direnv
.envrc
.envrc

View File

@ -1,5 +1,5 @@
# Cannery is a personal ammo manager that adjusts to your own needs.
* Easy to Use: Cannery lets you easily keep an eye on your ammo levels after range day
* Easy to Use: Cannery lets you easily keep an eye on your ammo levels before and after range day
* Secure: Self-host your own instance, or use an instance from someone you trust.
* Simple: Access from any internet-capable device
* Simple: Access from any internet-capable device

View File

@ -10,25 +10,7 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "components";
/* LiveView specific classes for your customizations */
.phx-no-feedback.invalid-feedback,
.phx-no-feedback .invalid-feedback {
display: none;
}
.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
}
.phx-disconnected{
cursor: wait;
}
.phx-disconnected *{
pointer-events: none;
}
/* Alerts and form errors */
/* Alerts and form errors used by phx.new */
.alert {
padding: 15px;
margin-bottom: 20px;
@ -60,4 +42,88 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
color: #a94442;
display: block;
margin: -1rem 0 2rem;
}
}
/* LiveView specific classes for your customization */
.phx-no-feedback.invalid-feedback,
.phx-no-feedback .invalid-feedback {
display: none;
}
.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
}
.phx-loading{
cursor: wait;
}
.phx-modal {
opacity: 1!important;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.phx-modal-content {
background-color: #fefefe;
margin: 15vh auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.phx-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.phx-modal-close:hover,
.phx-modal-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.fade-in-scale {
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
}
.fade-out-scale {
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
}
.fade-in {
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
}
.fade-out {
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
}
@keyframes fade-in-scale-keys{
0% { scale: 0.95; opacity: 0; }
100% { scale: 1.0; opacity: 1; }
}
@keyframes fade-out-scale-keys{
0% { scale: 1.0; opacity: 1; }
100% { scale: 0.95; opacity: 0; }
}
@keyframes fade-in-keys{
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes fade-out-keys{
0% { opacity: 1; }
100% { opacity: 0; }
}

View File

@ -1,31 +1,33 @@
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.scss"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import deps with the dep name or local files with a relative path, for example:
//
// import {Socket} from "phoenix"
// import socket from "./socket"
//
import "phoenix_html"
import {Socket} from "phoenix"
import topbar from "topbar"
import {LiveSocket} from "phoenix_live_view"
// 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"
let Hooks = {};
Hooks.MaintainAttrs = {
attrs(){ return this.el.getAttribute("data-attrs").split(", ") },
beforeUpdate(){ this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
updated(){ this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
};
// 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, {hooks: Hooks, params: {_csrf_token: csrfToken}})
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
@ -40,4 +42,3 @@ liveSocket.connect()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

View File

@ -1,63 +0,0 @@
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".
// To use Phoenix channels, the first step is to import Socket,
// and connect at the socket path in "lib/web/endpoint.ex".
//
// Pass the token on params as below. Or remove it
// from the params if you are not using authentication.
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/3" function
// in "lib/web/channels/user_socket.ex":
//
// def connect(%{"token" => token}, socket, _connect_info) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
// {:ok, socket |> assign(:user, user_id)}
// {:error, reason} ->
// :error
// end
// end
//
// Finally, connect to the socket:
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

View File

@ -1109,9 +1109,9 @@
"dev": true
},
"@types/eslint": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz",
"integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.0.tgz",
"integrity": "sha512-JUYa/5JwoqikCy7O7jKtuNe9Z4ZZt615G+1EKfaDGSNEpzaA2OwbV/G1v08Oa7fd1XzlFoSCvt9ePl9/6FyAug==",
"dev": true,
"requires": {
"@types/estree": "*",
@ -1119,9 +1119,9 @@
}
},
"@types/eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
"integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz",
"integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==",
"dev": true,
"requires": {
"@types/eslint": "*",
@ -1382,9 +1382,9 @@
"dev": true
},
"acorn-import-assertions": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz",
"integrity": "sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true
},
"acorn-node": {
@ -3190,9 +3190,9 @@
}
},
"enhanced-resolve": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
"integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
"integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@ -3200,9 +3200,9 @@
},
"dependencies": {
"tapable": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
"integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
}
}
@ -3244,9 +3244,9 @@
}
},
"es-module-lexer": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz",
"integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
"dev": true
},
"escalade": {
@ -3287,9 +3287,9 @@
},
"dependencies": {
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
}
}
@ -9401,9 +9401,9 @@
}
},
"watchpack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz",
"integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
@ -9420,9 +9420,9 @@
}
},
"webpack": {
"version": "5.50.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.50.0.tgz",
"integrity": "sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag==",
"version": "5.67.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.67.0.tgz",
"integrity": "sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.0",
@ -9434,12 +9434,12 @@
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.8.0",
"es-module-lexer": "^0.7.1",
"enhanced-resolve": "^5.8.3",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.4",
"graceful-fs": "^4.2.9",
"json-parse-better-errors": "^1.0.2",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
@ -9447,14 +9447,20 @@
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.2.0",
"webpack-sources": "^3.2.0"
"watchpack": "^2.3.1",
"webpack-sources": "^3.2.3"
},
"dependencies": {
"acorn": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
"integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true
},
"graceful-fs": {
"version": "4.2.9",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
"dev": true
},
"schema-utils": {
@ -9469,15 +9475,15 @@
}
},
"tapable": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
"integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
},
"webpack-sources": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.0.tgz",
"integrity": "sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true
}
}

View File

@ -33,7 +33,7 @@
"style-loader": "^3.2.1",
"tailwindcss": "^2.2.7",
"terser-webpack-plugin": "^5.1.3",
"webpack": "^5.50.0",
"webpack": "^5.67.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^3.11.2"
}

View File

@ -1,4 +1,4 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
# 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: *

157
assets/vendor/topbar.js vendored Normal file
View File

@ -0,0 +1,157 @@
/**
* @license MIT
* topbar 1.0.0, 2021-01-06
* 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,
progressTimerId,
fadeTimerId,
currentProgress,
showing,
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 () {
if (showing) return;
showing = true;
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
if (!canvas) createCanvas();
canvas.style.opacity = 1;
canvas.style.display = "block";
topbar.progress(0);
if (options.autoRun) {
(function loop() {
progressTimerId = window.requestAnimationFrame(loop);
topbar.progress(
"+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
);
})();
}
},
progress: function (to) {
if (typeof to === "undefined") return currentProgress;
if (typeof to === "string") {
to =
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
? currentProgress
: 0) + parseFloat(to);
}
currentProgress = to > 1 ? 1 : to;
repaint();
return currentProgress;
},
hide: function () {
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));

View File

@ -1,5 +1,5 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
@ -8,7 +8,8 @@
import Config
config :cannery,
ecto_repos: [Cannery.Repo]
ecto_repos: [Cannery.Repo],
generators: [binary_id: true]
# Configures the endpoint
config :cannery, CanneryWeb.Endpoint,
@ -25,6 +26,28 @@ config :cannery, :generators,
binary_id: true,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
# 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 :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Local
# Swoosh API client is needed for adapters other than SMTP.
config :swoosh, :api_client, false
# Configure esbuild (the version is required)
# config :esbuild,
# version: "0.14.0",
# default: [
# args:
# ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
# cd: Path.expand("../assets", __DIR__),
# env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
# ]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",

View File

@ -5,6 +5,7 @@ config :cannery, Cannery.Repo,
url:
System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@localhost/cannery_dev",
show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
@ -12,12 +13,18 @@ config :cannery, Cannery.Repo,
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
# with esbuild to bundle .js and .css sources.
config :cannery, CanneryWeb.Endpoint,
debug_errors: true,
code_reloader: true,
# 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: {0, 0, 0, 0}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
watchers: [
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
# esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
@ -58,8 +65,8 @@ config :cannery, CanneryWeb.Endpoint,
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/cannery_web/.*(live|views)/.*(ex|leex)$",
~r"lib/cannery_web/templates/.*(eex|leex)$"
~r"lib/cannery_web/(live|views)/.*(ex)$",
~r"lib/cannery_web/templates/.*(eex)$"
]
]

View File

@ -25,14 +25,14 @@ config :logger, level: :info
# to the previous section and set your `:url` port to 443:
#
# config :cannery, CanneryWeb.Endpoint,
# ...
# url: [host: "localhost", port: 443],
# ...,
# url: [host: "example.com", port: 443],
# https: [
# ...,
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
# transport_options: [socket_opts: [:inet6]]
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# ]
#
# The `cipher_suite` is set to `:strong` to support only the

View File

@ -1,33 +0,0 @@
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
import Config
database_url =
System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@cannery-db/cannery"
config :cannery, Cannery.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
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("HOST") || "localhost"
config :cannery, CanneryWeb.Endpoint,
url: [scheme: "https", host: host, port: "443"],
http: [
port: String.to_integer(System.get_env("PORT") || "4000"),
transport_options: [socket_opts: [:inet6]]
],
secret_key_base: secret_key_base,
server: true,
registration: System.get_env("REGISTRATION") || "invite"

83
config/runtime.exs Normal file
View File

@ -0,0 +1,83 @@
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.
# Start the phoenix server if environment is set and running in a release
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
config :cannery, CanneryWeb.Endpoint, server: true
end
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@cannery-db/cannery"
maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
config :cannery, Cannery.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("HOST") || "localhost"
config :cannery, CanneryWeb.Endpoint,
url: [scheme: "https", host: host, port: 443],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
],
secret_key_base: secret_key_base,
server: true,
registration: System.get_env("REGISTRATION") || "invite"
# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
# config :cannery, CanneryWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.
# ## 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 :cannery, Cannery.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

View File

@ -11,15 +11,23 @@ config :bcrypt_elixir, :log_rounds, 1
config :cannery, Cannery.Repo,
username: "postgres",
password: "postgres",
database: "cannery_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
database: "cannery_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :cannery, CanneryWeb.Endpoint,
http: [port: 4002],
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
server: false
# In test we don't send emails.
config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test
# Print only warnings and errors during test
config :logger, level: :warn
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

View File

@ -5,19 +5,21 @@ defmodule Cannery.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Start the Ecto repository
Cannery.Repo,
Cannery.Repo.Migrator,
# Start the Telemetry supervisor
CanneryWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Cannery.PubSub},
# Start the Endpoint (http/https)
CanneryWeb.Endpoint
CanneryWeb.Endpoint,
# Start a worker by calling: Cannery.Worker.start_link(arg)
# {Cannery.Worker, arg}
# {Cannery.Worker, arg},
# Automatically migrate on start
Cannery.Repo.Migrator
]
# See https://hexdocs.pm/elixir/Supervisor.html
@ -28,6 +30,7 @@ defmodule Cannery.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
CanneryWeb.Endpoint.config_change(changed, removed)
:ok

3
lib/cannery/mailer.ex Normal file
View File

@ -0,0 +1,3 @@
defmodule Cannery.Mailer do
use Swoosh.Mailer, otp_app: :cannery
end

View File

@ -46,7 +46,7 @@ defmodule CanneryWeb do
quote do
use Phoenix.LiveView,
layout: {CanneryWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
@ -59,6 +59,14 @@ defmodule CanneryWeb do
end
end
def component do
quote do
use Phoenix.Component
unquote(view_helpers())
end
end
def router do
quote do
use Phoenix.Router
@ -81,7 +89,7 @@ defmodule CanneryWeb do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import LiveView helpers (live_render, live_component, live_patch, etc)
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
import CanneryWeb.LiveHelpers

View File

@ -1,35 +0,0 @@
defmodule CanneryWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", CanneryWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, socket |> assign(:user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# CanneryWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View File

@ -3,6 +3,7 @@ defmodule CanneryWeb.UserAuth do
import Phoenix.Controller
alias Cannery.Accounts
alias CanneryWeb.{HomeLive}
alias CanneryWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days.
@ -138,7 +139,7 @@ defmodule CanneryWeb.UserAuth do
|> halt()
end
end
@doc """
Used for routes that require the user to be an admin.
"""
@ -149,7 +150,7 @@ defmodule CanneryWeb.UserAuth do
conn
|> put_flash(:error, "You are not authorized to view this page.")
|> maybe_store_return_to()
|> redirect(to: Routes.home_path(conn, :index))
|> redirect(to: Routes.live_path(conn, HomeLive))
|> halt()
end
end

View File

@ -3,7 +3,7 @@ defmodule CanneryWeb.UserRegistrationController do
alias Cannery.{Accounts, Invites}
alias Cannery.Accounts.User
alias CanneryWeb.UserAuth
alias CanneryWeb.{HomeLive, UserAuth}
def new(conn, %{"invite" => invite_token}) do
invite = Invites.get_invite_by_token(invite_token)
@ -13,7 +13,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, this invite was not found or expired")
|> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
|> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@ -23,7 +23,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, public registration is disabled")
|> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
|> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@ -41,7 +41,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, this invite was not found or expired")
|> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
|> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@ -51,7 +51,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, public registration is disabled")
|> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
|> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end

View File

@ -2,7 +2,7 @@ defmodule CanneryWeb.UserSettingsController do
use CanneryWeb, :controller
alias Cannery.Accounts
alias CanneryWeb.UserAuth
alias CanneryWeb.{HomeLive, UserAuth}
plug :assign_email_and_password_changesets
@ -70,7 +70,7 @@ defmodule CanneryWeb.UserSettingsController do
conn
|> put_flash(:error, "Your account has been deleted")
|> redirect(to: Routes.home_path(conn, :index))
|> redirect(to: Routes.live_path(conn, HomeLive))
else
conn
|> put_flash(:error, "Unable to delete user")

View File

@ -7,13 +7,9 @@ defmodule CanneryWeb.Endpoint do
@session_options [
store: :cookie,
key: "_cannery_key",
signing_salt: "fxAnJltS"
signing_salt: "N8eMKwCG"
]
socket "/socket", CanneryWeb.UserSocket,
websocket: true,
longpoll: false
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.

View File

@ -13,7 +13,7 @@
<li class="flex flex-row justify-center space-x-2">
<b>Easy to Use:</b>
<p>
Cannery lets you easily keep an eye on your ammo levels after range day
Cannery lets you easily keep an eye on your ammo levels before and after range day
</p>
</li>
<li class="flex flex-row justify-center space-x-2">

View File

@ -26,7 +26,7 @@ defmodule CanneryWeb.ModalComponent do
<%# modal content %>
<div class="w-full flex flex-col space-y-4 justify-center items-center">
<%= live_component @socket, @component, @opts %>
<%= live_component @component, @opts %>
</div>
</div>
</div>

View File

@ -24,7 +24,7 @@ defmodule CanneryWeb.Router do
scope "/", CanneryWeb do
pipe_through :browser
live "/", HomeLive, :index
live "/", HomeLive
end
## Authentication routes
@ -94,4 +94,16 @@ defmodule CanneryWeb.Router do
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
# Enables the Swoosh mailbox preview in development.
#
# Note that preview only shows emails that were sent by the same
# node running the Phoenix server.
if Mix.env() == :dev do
scope "/dev" do
pipe_through :browser
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end

View File

@ -31,11 +31,27 @@ defmodule CanneryWeb.Telemetry do
),
# Database Metrics
summary("cannery.repo.query.total_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.decode_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.query_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.queue_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.idle_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("cannery.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("cannery.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("cannery.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("cannery.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}),

View File

@ -2,20 +2,20 @@
<header class="mb-4 px-8 py-4 w-full bg-primary-400">
<%= render "topbar.html", assigns %>
</header>
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
<%= if get_flash(@conn, :info) do %>
<p class="alert alert-info" role="alert">
<%= get_flash(@conn, :info) %>
</p>
<% end %>
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert">
<%= get_flash(@conn, :error) %>
</p>
<% end %>
</div>
<%= @inner_content %>
</main>

View File

@ -1,16 +1,21 @@
<main role="main" class="container min-w-full min-h-full">
<main class="container min-w-full min-h-full">
<header>
<%= live_component CanneryWeb.Live.Component.Topbar, current_user: assigns[:current_user] %>
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
<%= if live_flash(@flash, :info) do %>
<p class="alert alert-info" role="alert">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info">
<%= live_flash(@flash, :info) %>
</p>
<% end %>
<%= if live_flash(@flash, :error) do %>
<p class="alert alert-danger" role="alert">
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error">
<%= live_flash(@flash, :error) %>
</p>
<% end %>

View File

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Cannery", suffix: "" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")}/>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
</head>
<body class="m-0 p-0 min-w-full min-h-full">
<%= @inner_content %>

View File

@ -1,6 +1,6 @@
<nav role="navigation">
<div class="flex flex-row justify-between items-center space-x-4">
<%= link to: Routes.home_path(CanneryWeb.Endpoint, :index) do %>
<%= link to: Routes.live_path(CanneryWeb.Endpoint, HomeLive) do %>
<h1 class="leading-5 text-xl text-white hover:underline">Cannery</h1>
<% end %>

View File

@ -1,7 +1,12 @@
defmodule CanneryWeb.LayoutView do
use CanneryWeb, :view
alias Cannery.{Accounts}
alias CanneryWeb.{HomeLive}
# Phoenix LiveDashboard is available only in development by default,
# so we instruct Elixir to not warn if the dashboard route is missing.
@compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
def get_title(conn) do
if conn.assigns |> Map.has_key?(:title) do
"Cannery | #{conn.assigns.title}"

31
mix.exs
View File

@ -5,9 +5,9 @@ defmodule Cannery.MixProject do
[
app: :cannery,
version: "0.1.0",
elixir: "~> 1.7",