Compare commits

...

72 Commits
0.9.1 ... dev

Author SHA1 Message Date
shibao d896257602 actually fix bar graph
continuous-integration/drone/push Build is passing Details
2024-03-19 00:28:30 -04:00
shibao 4ca51a3f53 update dependencies
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-19 00:01:27 -04:00
shibao 96b05e8332 Make bar graph ignore gaps 2024-03-19 00:00:51 -04:00
shibao 557a2cac3d Improve login page autocomplete behavior
continuous-integration/drone/push Build is passing Details
2024-03-18 23:39:06 -04:00
shibao e16e04c114 combine imports
continuous-integration/drone/push Build is passing Details
2024-03-18 23:26:41 -04:00
shibao bbe4d82303 Use bar graph instead of line graph 2024-03-18 23:26:32 -04:00
shibao c69d7843ab fix layout issues
continuous-integration/drone/push Build is passing Details
2024-02-23 23:34:04 -05:00
shibao c18f59050c fix missing ssl and crypto packages
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-23 21:57:25 -05:00
shibao 67d688fc1e create italian gettext
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2024-02-23 21:22:51 -05:00
Weblate 28e5fa56c3 Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
Weblate e301d3dd17 Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
Weblate 4881cf6edb Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
Weblate 6b61c45889 Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
Weblate 4a674a0504 Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
shibao 7e6959fb3b Added translation using Weblate (Italian) 2024-02-23 21:21:42 -05:00
shibao 22f13b0c57 make ammo packs in containers directly navigable in table view 2024-02-23 21:20:32 -05:00
shibao 31024dcc0d fix test mode warning 2024-02-23 21:16:46 -05:00
shibao e843014502 fix credo check for is_admin? 2024-02-23 21:13:34 -05:00
shibao 5d146ce6af fix credo check for is_already_admin? 2024-02-23 21:13:23 -05:00
shibao 27cda3733e update elixir deps 2024-02-23 21:12:11 -05:00
shibao 1965ecba32 bump version 2024-02-23 21:04:25 -05:00
shibao 69e40c6d18 update elixir deps 2024-02-23 21:04:25 -05:00
shibao 34b4b24e67 update npm deps 2024-02-23 21:04:25 -05:00
shibao 7ebed8d4c0 update tool versions 2024-02-23 21:04:25 -05:00
shibao b5619b8606 run mix format
continuous-integration/drone/push Build is passing Details
2023-09-07 19:15:07 -04:00
shibao ef28de53a1 update hex dependencies 2023-09-07 19:11:57 -04:00
shibao fcd5dbc605 bump version
continuous-integration/drone/push Build is failing Details
2023-09-07 19:07:56 -04:00
shibao 7738e68292 run npm audit fix --force 2023-09-07 19:06:56 -04:00
shibao df645a6188 update node packages 2023-09-07 19:06:21 -04:00
shibao bed4fbaf54 update dependencies 2023-09-07 19:05:35 -04:00
shibao f94ef0a2ca fix range page title
continuous-integration/drone/push Build is passing Details
2023-06-05 23:36:25 -04:00
shibao 7cdb8af690 update dependencies 2023-06-05 23:32:52 -04:00
shibao 52c4ab45d5 fix class filter helper functions
continuous-integration/drone/push Build is passing Details
2023-06-05 23:17:43 -04:00
shibao a35f43d6df rename Ammo.get_average_cost and Ammo.get_historical_count 2023-06-05 23:17:43 -04:00
shibao 9edeb1e803 improve Ammo.get_grouped_round_count 2023-06-05 23:17:43 -04:00
shibao 7e55446b3e improve ActivityLog.list_shot_records 2023-06-05 23:17:43 -04:00
shibao 9643e9f46d improve Ammo.get_round_count 2023-06-05 23:17:39 -04:00
shibao 8466fcd1f9 improve ActivityLog.get_grouped_used_counts 2023-06-05 23:16:47 -04:00
shibao e713a2e108 improve ActivityLog.get_used_count 2023-06-05 23:16:00 -04:00
shibao a8fa321040 use sr for shot record in sql 2023-06-05 23:16:00 -04:00
shibao f0536f3030 improve Ammo.get_grouped_packs_count 2023-06-05 23:15:57 -04:00
shibao a94d2eebf4 improve Ammo.get_packs_count 2023-06-05 23:06:28 -04:00
shibao cfc56519f5 fix user registration controller 2023-06-04 00:07:31 -04:00
shibao e80c2018be improve Ammo.list_packs 2023-06-04 00:00:51 -04:00
shibao 71fdd42d96 improve Ammo.list_types 2023-06-03 20:14:20 -04:00
shibao 8e99a57994 improve Containers.list_containers 2023-06-03 20:12:06 -04:00
shibao 7c42dd8a3a improve Containers.list_tags 2023-06-03 19:54:51 -04:00
shibao 79c97d7502 fix error/404 pages not rendering properly 2023-05-12 22:59:53 -04:00
shibao 2e488fa26c fix ammo type sql naming issues 2023-05-12 22:22:46 -04:00
shibao 2179bd5d86 fix table component ids 2023-05-12 21:55:59 -04:00
shibao 49628cb9bb pattern match on user struct in more cases 2023-05-12 21:48:19 -04:00
shibao 8a58d53dc1 fix pack sql naming issues 2023-05-12 21:48:04 -04:00
shibao 9306d0f970 disable arm builds
continuous-integration/drone/push Build is passing Details
2023-04-16 21:35:37 -04:00
shibao 763c86a379 build in arm64 and amd64
continuous-integration/drone/push Build is running Details
2023-04-16 17:05:05 -04:00
shibao b85b1735c0 remove maintain attrs
continuous-integration/drone/push Build is passing Details
2023-04-16 01:10:45 -04:00
shibao ab1a288928 change invite path
continuous-integration/drone/push Build is passing Details
2023-04-16 00:46:49 -04:00
shibao e6ef0a8c68 improve tests
continuous-integration/drone/push Build is passing Details
2023-04-15 21:47:50 -04:00
shibao beeaf521c5 update npm dependencies
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build was killed Details
2023-04-14 23:56:22 -04:00
shibao 8cb6068b85 make tests async
continuous-integration/drone/push Build is failing Details
2023-04-14 23:48:50 -04:00
shibao 334d841d57 add pack lot number to search
continuous-integration/drone/push Build is failing Details
2023-04-14 23:38:28 -04:00
shibao 1037f37be2 upgrade to phoenix 1.7 2023-04-14 23:34:11 -04:00
shibao 1796fb822f fix logger errors 2023-04-14 18:25:06 -04:00
shibao 8ed64f9c87 update elixir dependencies 2023-04-14 18:20:53 -04:00
shibao dd4a9f7119 upgrade npm dependencies 2023-04-14 18:17:11 -04:00
shibao dbafaad500 update node and npm 2023-04-14 18:11:44 -04:00
shibao eb4ce07b5f update erlang 2023-04-14 18:10:59 -04:00
shibao 2b7550a954 update elixir 2023-04-14 18:09:39 -04:00
shibao abaccac9f0 use :rifle as default ammo type
continuous-integration/drone/push Build is passing Details
2023-04-10 20:13:56 -04:00
shibao 2b81978adb make bullet type rifle/pistol type-only field 2023-04-10 20:13:55 -04:00
shibao 17bfe1a987 remove duplicate chamber size column 2023-04-10 20:13:38 -04:00
shibao 81f21a02c4 export shotgun fields
continuous-integration/drone/push Build is passing Details
2023-03-31 22:34:48 -04:00
shibao 9a17d4ad24 add lot number to packs 2023-03-30 23:36:19 -04:00
176 changed files with 12351 additions and 17943 deletions

View File

@ -17,7 +17,7 @@ steps:
- .mix
- name: test
image: elixir:1.14.1-alpine
image: elixir:1.16.1-alpine
environment:
TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
HOST: testing.example.tld
@ -42,7 +42,8 @@ steps:
repo: shibaobun/cannery
purge: true
compress: true
platforms: linux/amd64,linux/arm/v7
platforms:
- linux/amd64
username:
from_secret: docker_username
password:
@ -59,7 +60,8 @@ steps:
repo: shibaobun/cannery
purge: true
compress: true
platforms: linux/amd64,linux/arm/v7
platforms:
- linux/amd64
username:
from_secret: docker_username
password:

View File

@ -1,6 +1,6 @@
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
import_deps: [:ecto, :ecto_sql, :phoenix],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter]
]

View File

@ -1,3 +1,3 @@
elixir 1.14.1-otp-25
erlang 25.1.2
nodejs 18.9.1
elixir 1.16.1-otp-26
erlang 26.2.2
nodejs 21.6.2

View File

@ -1,3 +1,43 @@
# v0.9.9
- Actually fix bar graph
# v0.9.8
- Make bar graph ignore empty days
- Update dependencies
# v0.9.7
- Fix margin on bottom of page
- Use bar graph instead of line graph
- Improve login page autocomplete behavior
# v0.9.6
- Make ammo packs in containers directly navigable in table view
- Update dependencies
# v0.9.5
- Update dependencies
# v0.9.4
- Code quality fixes
- Fix error/404 pages not rendering properly
- Update dependencies
- Fix Range page title
# v0.9.3
- Update dependencies
- Add pack lot number to search
- Improve tests
- Change invite path slightly
- Disable arm builds since ci fails to build
# v0.9.2
- Add lot number to packs
- Don't show price paid and lot number columns when displaying packs if not used
- Fix additional shotgun fields not being exportable
- Fixes duplicate chamber size column for ammo types
- Hide bullet type field when editing/creating shotgun ammo types
- Fix ammo type creation not displaying all the necessary fields on first load
# v0.9.1
- Rename ammo type's "type" to "class" to avoid confusion
- Rename "ammo type" to "type" to avoid confusion

View File

@ -1,4 +1,4 @@
FROM elixir:1.14.1-alpine AS build
FROM elixir:1.16.1-alpine AS build
# install build dependencies
RUN apk add --no-cache build-base npm git python3
@ -37,7 +37,7 @@ RUN mix do compile, release
FROM alpine:latest AS app
RUN apk upgrade --no-cache && \
apk add --no-cache bash openssl libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs
apk add --no-cache bash openssl libssl3 libcrypto3 libgcc libstdc++ ncurses-libs
WORKDIR /app

View File

@ -25,7 +25,6 @@ import 'phoenix_html'
import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view'
import topbar from 'topbar'
import MaintainAttrs from './maintain_attrs'
import ShotLogChart from './shot_log_chart'
import Date from './date'
import DateTime from './datetime'
@ -33,7 +32,7 @@ import DateTime from './datetime'
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
const liveSocket = new LiveSocket('/live', Socket, {
params: { _csrf_token: csrfToken },
hooks: { Date, DateTime, MaintainAttrs, ShotLogChart }
hooks: { Date, DateTime, ShotLogChart }
})
// Show progress bar on live navigation and form submits

View File

@ -1,11 +0,0 @@
// maintain user adjusted attributes, like textbox length on phoenix liveview
// update. https://github.com/phoenixframework/phoenix_live_view/issues/1011
export default {
attrs () {
const attrs = this.el.getAttribute('data-attrs')
if (attrs) { return attrs.split(', ') } else { return [] }
},
beforeUpdate () { this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
updated () { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
}

View File

@ -1,13 +1,12 @@
import { Chart, Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale } from 'chart.js'
import Chart from 'chart.js/auto'
import 'chartjs-adapter-date-fns'
Chart.register(Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale)
export default {
initalizeChart (el) {
const data = JSON.parse(el.dataset.chartData)
this.el.chart = new Chart(el, {
type: 'line',
type: 'bar',
data: {
datasets: [{
label: el.dataset.label,
@ -51,13 +50,17 @@ export default {
stacked: true,
grace: '15%',
ticks: {
padding: 15
padding: 15,
precision: 0
}
},
x: {
type: 'time',
type: 'timeseries',
time: {
unit: 'day'
},
ticks: {
source: 'data'
}
}
},

18871
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@
"description": " ",
"license": "MIT",
"engines": {
"node": "v18.9.1",
"npm": "8.19.1"
"node": "v21.6.2",
"npm": "10.2.4"
},
"scripts": {
"deploy": "NODE_ENV=production webpack --mode production",
@ -13,37 +13,37 @@
"test": "standard"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.3.0",
"chart.js": "^4.2.1",
"@fortawesome/fontawesome-free": "^6.5.1",
"chart.js": "^4.4.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^2.29.3",
"date-fns": "^3.3.1",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"topbar": "^2.0.1"
"topbar": "^2.0.2"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"autoprefixer": "^10.4.17",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.7.5",
"npm-check-updates": "^16.7.12",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.1.0",
"postcss-preset-env": "^8.0.1",
"sass": "^1.59.3",
"sass-loader": "^13.2.1",
"standard": "^17.0.0",
"tailwindcss": "^3.2.7",
"terser-webpack-plugin": "^5.3.7",
"webpack": "^5.76.2",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.1"
"mini-css-extract-plugin": "^2.8.0",
"npm-check-updates": "^16.14.15",
"postcss": "^8.4.35",
"postcss-import": "^16.0.1",
"postcss-loader": "^8.1.0",
"postcss-preset-env": "^9.4.0",
"sass": "^1.71.1",
"sass-loader": "^14.1.1",
"standard": "^17.1.0",
"tailwindcss": "^3.4.1",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2"
}
}

View File

@ -18,7 +18,10 @@ config :cannery, CanneryWeb.Endpoint,
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
render_errors: [view: CanneryWeb.ErrorView, accepts: ~w(html json), layout: false],
render_errors: [
formats: [html: CanneryWeb.ErrorHTML, json: CanneryWeb.ErrorJSON],
layout: false
],
pubsub_server: Cannery.PubSub,
live_view: [signing_salt: "zOLgd3lr"]

View File

@ -59,8 +59,7 @@ 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)$",
~r"lib/cannery_web/templates/.*(eex)$"
~r"lib/cannery_web/*/.*(ex)$"
]
]

View File

@ -12,7 +12,7 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
config :cannery, CanneryWeb.Endpoint, server: true
end
config :cannery, CanneryWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
config :cannery, CanneryWeb.HTMLHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
# Set default locale
config :gettext, :default_locale, System.get_env("LOCALE", "en_US")

View File

@ -26,7 +26,7 @@ config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test
config :cannery, Cannery.Accounts, registration: "public"
# Print only warnings and errors during test
config :logger, level: :warn
config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

View File

@ -374,8 +374,8 @@ defmodule Cannery.Accounts do
@doc """
Deletes the signed token with the given context.
"""
@spec delete_session_token(token :: String.t()) :: :ok
def delete_session_token(token) do
@spec delete_user_session_token(token :: String.t()) :: :ok
def delete_user_session_token(token) do
UserToken.token_and_context_query(token, "session") |> Repo.delete_all()
:ok
end
@ -404,15 +404,15 @@ defmodule Cannery.Accounts do
## Examples
iex> is_admin?(%User{role: :admin})
iex> admin?(%User{role: :admin})
true
iex> is_admin?(%User{})
iex> admin?(%User{})
false
"""
@spec is_admin?(User.t()) :: boolean()
def is_admin?(%User{id: user_id}) do
@spec admin?(User.t()) :: boolean()
def admin?(%User{id: user_id}) do
Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin)
end
@ -421,16 +421,16 @@ defmodule Cannery.Accounts do
## Examples
iex> is_already_admin?(%User{role: :admin})
iex> already_admin?(%User{role: :admin})
true
iex> is_already_admin?(%User{})
iex> already_admin?(%User{})
false
"""
@spec is_already_admin?(User.t() | nil) :: boolean()
def is_already_admin?(%User{role: :admin}), do: true
def is_already_admin?(_invalid_user), do: false
@spec already_admin?(User.t() | nil) :: boolean()
def already_admin?(%User{role: :admin}), do: true
def already_admin?(_invalid_user), do: false
## Confirmation

View File

@ -3,14 +3,15 @@ defmodule Cannery.Email do
Emails that can be sent using Swoosh.
You can find the base email templates at
`lib/cannery_web/templates/layout/email.html.heex` for html emails and
`lib/cannery_web/templates/layout/email.txt.heex` for text emails.
`lib/cannery_web/components/layouts/email_html.html.heex` for html emails and
`lib/cannery_web/components/layouts/email_text.txt.eex` for text emails.
"""
use Phoenix.Swoosh, view: CanneryWeb.EmailView, layout: {CanneryWeb.LayoutView, :email}
import Swoosh.Email
import CanneryWeb.Gettext
import Phoenix.Template
alias Cannery.Accounts.User
alias CanneryWeb.EmailView
alias CanneryWeb.{EmailHTML, Layouts}
@typedoc """
Represents an HTML and text body email that can be sent
@ -28,21 +29,33 @@ defmodule Cannery.Email do
def generate_email("welcome", user, %{"url" => url}) do
user
|> base_email(dgettext("emails", "Confirm your Cannery account"))
|> render_body("confirm_email.html", %{user: user, url: url})
|> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url}))
|> html_email(:confirm_email_html, %{user: user, url: url})
|> text_email(:confirm_email_text, %{user: user, url: url})
end
def generate_email("reset_password", user, %{"url" => url}) do
user
|> base_email(dgettext("emails", "Reset your Cannery password"))
|> render_body("reset_password.html", %{user: user, url: url})
|> text_body(EmailView.render("reset_password.txt", %{user: user, url: url}))
|> html_email(:reset_password_html, %{user: user, url: url})
|> text_email(:reset_password_text, %{user: user, url: url})
end
def generate_email("update_email", user, %{"url" => url}) do
user
|> base_email(dgettext("emails", "Update your Cannery email"))
|> render_body("update_email.html", %{user: user, url: url})
|> text_body(EmailView.render("update_email.txt", %{user: user, url: url}))
|> html_email(:update_email_html, %{user: user, url: url})
|> text_email(:update_email_text, %{user: user, url: url})
end
defp html_email(email, atom, assigns) do
heex = apply(EmailHTML, atom, [assigns])
html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: heex)
email |> html_body(html)
end
defp text_email(email, atom, assigns) do
heex = apply(EmailHTML, atom, [assigns])
text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: heex)
email |> text_body(text)
end
end

View File

@ -8,38 +8,49 @@ defmodule Cannery.ActivityLog do
alias Cannery.{Accounts.User, ActivityLog.ShotRecord, Repo}
alias Ecto.{Multi, Queryable}
@type list_shot_records_option ::
{:search, String.t() | nil}
| {:class, Type.class() | :all | nil}
| {:pack_id, Pack.id() | nil}
@type list_shot_records_options :: [list_shot_records_option()]
@doc """
Returns the list of shot_records.
## Examples
iex> list_shot_records(:all, %User{id: 123})
iex> list_shot_records(%User{id: 123})
[%ShotRecord{}, ...]
iex> list_shot_records("cool", :all, %User{id: 123})
iex> list_shot_records(%User{id: 123}, search: "cool")
[%ShotRecord{notes: "My cool shot record"}, ...]
iex> list_shot_records("cool", :rifle, %User{id: 123})
iex> list_shot_records(%User{id: 123}, search: "cool", class: :rifle)
[%ShotRecord{notes: "Shot some rifle rounds"}, ...]
iex> list_shot_records(%User{id: 123}, pack_id: 456)
[%ShotRecord{pack_id: 456}, ...]
"""
@spec list_shot_records(Type.class() | :all, User.t()) :: [ShotRecord.t()]
@spec list_shot_records(search :: nil | String.t(), Type.class() | :all, User.t()) ::
[ShotRecord.t()]
def list_shot_records(search \\ nil, type, %{id: user_id}) do
from(sg in ShotRecord,
as: :sg,
left_join: ag in Pack,
as: :ag,
on: sg.pack_id == ag.id,
left_join: at in Type,
as: :at,
on: ag.type_id == at.id,
where: sg.user_id == ^user_id,
distinct: sg.id
@spec list_shot_records(User.t()) :: [ShotRecord.t()]
@spec list_shot_records(User.t(), list_shot_records_options()) :: [ShotRecord.t()]
def list_shot_records(%User{id: user_id}, opts \\ []) do
from(sr in ShotRecord,
as: :sr,
left_join: p in Pack,
as: :p,
on: sr.pack_id == p.id,
on: p.user_id == ^user_id,
left_join: t in Type,
as: :t,
on: p.type_id == t.id,
on: t.user_id == ^user_id,
where: sr.user_id == ^user_id,
distinct: sr.id
)
|> list_shot_records_search(search)
|> list_shot_records_filter_type(type)
|> list_shot_records_search(Keyword.get(opts, :search))
|> list_shot_records_class(Keyword.get(opts, :class))
|> list_shot_records_pack_id(Keyword.get(opts, :pack_id))
|> Repo.all()
end
@ -52,45 +63,44 @@ defmodule Cannery.ActivityLog do
query
|> where(
[sg: sg, ag: ag, at: at],
[sr: sr, p: p, t: t],
fragment(
"? @@ websearch_to_tsquery('english', ?)",
sg.search,
sr.search,
^trimmed_search
) or
fragment(
"? @@ websearch_to_tsquery('english', ?)",
ag.search,
p.search,
^trimmed_search
) or
fragment(
"? @@ websearch_to_tsquery('english', ?)",
at.search,
t.search,
^trimmed_search
)
)
|> order_by([sg: sg], {
|> order_by([sr: sr], {
:desc,
fragment(
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
sg.search,
sr.search,
^trimmed_search
)
})
end
@spec list_shot_records_filter_type(Queryable.t(), Type.class() | :all) ::
Queryable.t()
defp list_shot_records_filter_type(query, :rifle),
do: query |> where([at: at], at.class == :rifle)
@spec list_shot_records_class(Queryable.t(), Type.class() | :all | nil) :: Queryable.t()
defp list_shot_records_class(query, class) when class in [:rifle, :pistol, :shotgun],
do: query |> where([t: t], t.class == ^class)
defp list_shot_records_filter_type(query, :pistol),
do: query |> where([at: at], at.class == :pistol)
defp list_shot_records_class(query, _all), do: query
defp list_shot_records_filter_type(query, :shotgun),
do: query |> where([at: at], at.class == :shotgun)
@spec list_shot_records_pack_id(Queryable.t(), Pack.id() | nil) :: Queryable.t()
defp list_shot_records_pack_id(query, pack_id) when pack_id |> is_binary(),
do: query |> where([sr: sr], sr.pack_id == ^pack_id)
defp list_shot_records_filter_type(query, _all), do: query
defp list_shot_records_pack_id(query, _all), do: query
@doc """
Returns a count of shot records.
@ -104,25 +114,13 @@ defmodule Cannery.ActivityLog do
@spec get_shot_record_count!(User.t()) :: integer()
def get_shot_record_count!(%User{id: user_id}) do
Repo.one(
from sg in ShotRecord,
where: sg.user_id == ^user_id,
select: count(sg.id),
from sr in ShotRecord,
where: sr.user_id == ^user_id,
select: count(sr.id),
distinct: true
) || 0
end
@spec list_shot_records_for_pack(Pack.t(), User.t()) :: [ShotRecord.t()]
def list_shot_records_for_pack(
%Pack{id: pack_id, user_id: user_id},
%User{id: user_id}
) do
Repo.all(
from sg in ShotRecord,
where: sg.pack_id == ^pack_id,
where: sg.user_id == ^user_id
)
end
@doc """
Gets a single shot_record.
@ -140,10 +138,10 @@ defmodule Cannery.ActivityLog do
@spec get_shot_record!(ShotRecord.id(), User.t()) :: ShotRecord.t()
def get_shot_record!(id, %User{id: user_id}) do
Repo.one!(
from sg in ShotRecord,
where: sg.id == ^id,
where: sg.user_id == ^user_id,
order_by: sg.date
from sr in ShotRecord,
where: sr.id == ^id,
where: sr.user_id == ^user_id,
order_by: sr.date
)
end
@ -172,9 +170,9 @@ defmodule Cannery.ActivityLog do
fn _repo, %{create_shot_record: %{pack_id: pack_id, user_id: user_id}} ->
pack =
Repo.one(
from ag in Pack,
where: ag.id == ^pack_id,
where: ag.user_id == ^user_id
from p in Pack,
where: p.id == ^pack_id,
where: p.user_id == ^user_id
)
{:ok, pack}
@ -221,7 +219,7 @@ defmodule Cannery.ActivityLog do
|> Multi.run(
:pack,
fn repo, %{update_shot_record: %{pack_id: pack_id, user_id: user_id}} ->
{:ok, repo.one(from ag in Pack, where: ag.id == ^pack_id and ag.user_id == ^user_id)}
{:ok, repo.one(from p in Pack, where: p.id == ^pack_id and p.user_id == ^user_id)}
end
)
|> Multi.update(
@ -266,7 +264,7 @@ defmodule Cannery.ActivityLog do
|> Multi.run(
:pack,
fn repo, %{delete_shot_record: %{pack_id: pack_id, user_id: user_id}} ->
{:ok, repo.one(from ag in Pack, where: ag.id == ^pack_id and ag.user_id == ^user_id)}
{:ok, repo.one(from p in Pack, where: p.id == ^pack_id and p.user_id == ^user_id)}
end
)
|> Multi.update(
@ -287,36 +285,6 @@ defmodule Cannery.ActivityLog do
end
end
@doc """
Returns the number of shot rounds for a pack
"""
@spec get_used_count(Pack.t(), User.t()) :: non_neg_integer()
def get_used_count(%Pack{id: pack_id} = pack, user) do
[pack]
|> get_used_counts(user)
|> Map.get(pack_id, 0)
end
@doc """
Returns the number of shot rounds for multiple packs
"""
@spec get_used_counts([Pack.t()], User.t()) ::
%{optional(Pack.id()) => non_neg_integer()}
def get_used_counts(packs, %User{id: user_id}) do
pack_ids =
packs
|> Enum.map(fn %{id: pack_id} -> pack_id end)
Repo.all(
from sg in ShotRecord,
where: sg.pack_id in ^pack_ids,
where: sg.user_id == ^user_id,
group_by: sg.pack_id,
select: {sg.pack_id, sum(sg.count)}
)
|> Map.new()
end
@doc """
Returns the last entered shot record date for a pack
"""
@ -337,15 +305,18 @@ defmodule Cannery.ActivityLog do
|> Enum.map(fn %Pack{id: pack_id, user_id: ^user_id} -> pack_id end)
Repo.all(
from sg in ShotRecord,
where: sg.pack_id in ^pack_ids,
where: sg.user_id == ^user_id,
group_by: sg.pack_id,
select: {sg.pack_id, max(sg.date)}
from sr in ShotRecord,
where: sr.pack_id in ^pack_ids,
where: sr.user_id == ^user_id,
group_by: sr.pack_id,
select: {sr.pack_id, max(sr.date)}
)
|> Map.new()
end
@type get_used_count_option :: {:pack_id, Pack.id() | nil} | {:type_id, Type.id() | nil}
@type get_used_count_options :: [get_used_count_option()]
@doc """
Gets the total number of rounds shot for a type
@ -353,45 +324,116 @@ defmodule Cannery.ActivityLog do
## Examples
iex> get_used_count_for_type(123, %User{id: 123})
iex> get_used_count(%User{id: 123}, type_id: 123)
35
iex> get_used_count_for_type(456, %User{id: 123})
** (Ecto.NoResultsError)
iex> get_used_count(%User{id: 123}, pack_id: 456)
50
"""
@spec get_used_count_for_type(Type.t(), User.t()) :: non_neg_integer()
def get_used_count_for_type(%Type{id: type_id} = type, user) do
[type]
|> get_used_count_for_types(user)
|> Map.get(type_id, 0)
@spec get_used_count(User.t(), get_used_count_options()) :: non_neg_integer()
def get_used_count(%User{id: user_id}, opts) do
from(sr in ShotRecord,
as: :sr,
left_join: p in Pack,
on: sr.pack_id == p.id,
on: p.user_id == ^user_id,
as: :p,
where: sr.user_id == ^user_id,
where: not (sr.count |> is_nil()),
select: sum(sr.count),
distinct: true
)
|> get_used_count_type_id(Keyword.get(opts, :type_id))
|> get_used_count_pack_id(Keyword.get(opts, :pack_id))
|> Repo.one() || 0
end
@spec get_used_count_pack_id(Queryable.t(), Pack.id() | nil) :: Queryable.t()
defp get_used_count_pack_id(query, pack_id) when pack_id |> is_binary() do
query |> where([sr: sr], sr.pack_id == ^pack_id)
end
defp get_used_count_pack_id(query, _nil), do: query
@spec get_used_count_type_id(Queryable.t(), Type.id() | nil) :: Queryable.t()
defp get_used_count_type_id(query, type_id) when type_id |> is_binary() do
query |> where([p: p], p.type_id == ^type_id)
end
defp get_used_count_type_id(query, _nil), do: query
@type get_grouped_used_counts_option ::
{:packs, [Pack.t()] | nil}
| {:types, [Type.t()] | nil}
| {:group_by, :type_id | :pack_id}
@type get_grouped_used_counts_options :: [get_grouped_used_counts_option()]
@doc """
Gets the total number of rounds shot for multiple types
Gets the total number of rounds shot for multiple types or packs
## Examples
iex> get_used_count_for_types(123, %User{id: 123})
iex> get_grouped_used_counts(
...> %User{id: 123},
...> group_by: :type_id,
...> types: [%Type{id: 456, user_id: 123}]
...> )
35
"""
@spec get_used_count_for_types([Type.t()], User.t()) ::
%{optional(Type.id()) => non_neg_integer()}
def get_used_count_for_types(types, %User{id: user_id}) do
type_ids =
types
|> Enum.map(fn %Type{id: type_id, user_id: ^user_id} -> type_id end)
iex> get_grouped_used_counts(
...> %User{id: 123},
...> group_by: :pack_id,
...> packs: [%Pack{id: 456, user_id: 123}]
...> )
22
Repo.all(
from ag in Pack,
left_join: sg in ShotRecord,
on: ag.id == sg.pack_id,
where: ag.type_id in ^type_ids,
where: not (sg.count |> is_nil()),
group_by: ag.type_id,
select: {ag.type_id, sum(sg.count)}
"""
@spec get_grouped_used_counts(User.t(), get_grouped_used_counts_options()) ::
%{optional(Type.id() | Pack.id()) => non_neg_integer()}
def get_grouped_used_counts(%User{id: user_id}, opts) do
from(p in Pack,
as: :p,
left_join: sr in ShotRecord,
on: p.id == sr.pack_id,
on: p.user_id == ^user_id,
as: :sr,
where: sr.user_id == ^user_id,
where: not (sr.count |> is_nil())
)
|> get_grouped_used_counts_group_by(Keyword.fetch!(opts, :group_by))
|> get_grouped_used_counts_types(Keyword.get(opts, :types))
|> get_grouped_used_counts_packs(Keyword.get(opts, :packs))
|> Repo.all()
|> Map.new()
end
@spec get_grouped_used_counts_group_by(Queryable.t(), :type_id | :pack_id) :: Queryable.t()
defp get_grouped_used_counts_group_by(query, :type_id) do
query
|> group_by([p: p], p.type_id)
|> select([sr: sr, p: p], {p.type_id, sum(sr.count)})
end
defp get_grouped_used_counts_group_by(query, :pack_id) do
query
|> group_by([sr: sr], sr.pack_id)
|> select([sr: sr], {sr.pack_id, sum(sr.count)})
end
@spec get_grouped_used_counts_types(Queryable.t(), [Type.t()] | nil) :: Queryable.t()
defp get_grouped_used_counts_types(query, types) when types |> is_list() do
type_ids = types |> Enum.map(fn %Type{id: type_id} -> type_id end)
query |> where([p: p], p.type_id in ^type_ids)
end
defp get_grouped_used_counts_types(query, _nil), do: query
@spec get_grouped_used_counts_packs(Queryable.t(), [Pack.t()] | nil) :: Queryable.t()
defp get_grouped_used_counts_packs(query, packs) when packs |> is_list() do
pack_ids = packs |> Enum.map(fn %Pack{id: pack_id} -> pack_id end)
query |> where([p: p], p.id in ^pack_ids)
end
defp get_grouped_used_counts_packs(query, _nil), do: query
end

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ defmodule Cannery.Ammo.Pack do
:count,
:notes,
:price_paid,
:lot_number,
:staged,
:type_id,
:container_id
@ -30,6 +31,7 @@ defmodule Cannery.Ammo.Pack do
field :notes, :string
field :price_paid, :float
field :staged, :boolean, default: false
field :lot_number, :string
field :purchased_on, :date
belongs_to :type, Type
@ -45,6 +47,7 @@ defmodule Cannery.Ammo.Pack do
notes: String.t() | nil,
price_paid: float() | nil,
staged: boolean(),
lot_number: String.t() | nil,
purchased_on: Date.t(),
type: Type.t() | nil,
type_id: Type.id(),
@ -77,8 +80,10 @@ defmodule Cannery.Ammo.Pack do
|> change(type_id: type_id)
|> change(user_id: user_id)
|> change(container_id: container_id)
|> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on])
|> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :lot_number])
|> validate_number(:count, greater_than: 0)
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|> validate_length(:lot_number, max: 255)
|> validate_required([:count, :staged, :purchased_on, :type_id, :container_id, :user_id])
end
@ -89,6 +94,9 @@ defmodule Cannery.Ammo.Pack do
pack
|> cast(attrs, [:type_id, :container_id])
|> validate_required([:type_id, :container_id])
|> validate_number(:count, greater_than: 0)
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|> validate_length(:lot_number, max: 255)
|> add_error(:invalid, dgettext("errors", "Please select a type and container"))
end
@ -96,9 +104,19 @@ defmodule Cannery.Ammo.Pack do
@spec update_changeset(t() | new_pack(), attrs :: map(), User.t()) :: changeset()
def update_changeset(pack, attrs, user) do
pack
|> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :container_id])
|> cast(attrs, [
:count,
:price_paid,
:notes,
:staged,
:purchased_on,
:lot_number,
:container_id
])
|> validate_number(:count, greater_than_or_equal_to: 0)
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|> validate_container_id(user)
|> validate_length(:lot_number, max: 255)
|> validate_required([:count, :staged, :purchased_on, :container_id])
end

View File

@ -13,28 +13,38 @@ defmodule Cannery.Ammo.Type do
@derive {Jason.Encoder,
only: [
:id,
:name,
:desc,
:class,
:bullet_type,
:bullet_core,
:cartridge,
:caliber,
:case_material,
:jacket_type,
:muzzle_velocity,
:powder_type,
:powder_grains_per_charge,
:grains,
:pressure,
:primer_type,
:firing_type,
:manufacturer,
:upc,
:tracer,
:incendiary,
:blank,
:corrosive,
:manufacturer,
:upc
:cartridge,
:jacket_type,
:powder_grains_per_charge,
:muzzle_velocity,
:wadding,
:shot_type,
:shot_material,
:shot_size,
:unfired_length,
:brass_height,
:chamber_size,
:load_grains,
:shot_charge_weight,
:dram_equivalent
]}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@ -42,11 +52,9 @@ defmodule Cannery.Ammo.Type do
field :name, :string
field :desc, :string
field :class, Ecto.Enum, values: [:rifle, :shotgun, :pistol]
field :class, Ecto.Enum, values: [:rifle, :shotgun, :pistol], default: :rifle
# common fields
# https://shootersreference.com/reloadingdata/bullet_abbreviations/
field :bullet_type, :string
field :bullet_core, :string
# also gauge for shotguns
field :caliber, :string
@ -65,6 +73,8 @@ defmodule Cannery.Ammo.Type do
field :corrosive, :boolean, default: false
# rifle/pistol fields
# https://shootersreference.com/reloadingdata/bullet_abbreviations/
field :bullet_type, :string
field :cartridge, :string
field :jacket_type, :string
field :powder_grains_per_charge, :integer
@ -95,17 +105,23 @@ defmodule Cannery.Ammo.Type do
class: class(),
bullet_type: String.t() | nil,
bullet_core: String.t() | nil,
cartridge: String.t() | nil,
caliber: String.t() | nil,
case_material: String.t() | nil,
jacket_type: String.t() | nil,
muzzle_velocity: integer() | nil,
powder_type: String.t() | nil,
powder_grains_per_charge: integer() | nil,
grains: integer() | nil,
pressure: String.t() | nil,
primer_type: String.t() | nil,
firing_type: String.t() | nil,
manufacturer: String.t() | nil,
upc: String.t() | nil,
tracer: boolean(),
incendiary: boolean(),
blank: boolean(),
corrosive: boolean(),
cartridge: String.t() | nil,
jacket_type: String.t() | nil,
powder_grains_per_charge: integer() | nil,
muzzle_velocity: integer() | nil,
wadding: String.t() | nil,
shot_type: String.t() | nil,
shot_material: String.t() | nil,
@ -116,12 +132,6 @@ defmodule Cannery.Ammo.Type do
load_grains: integer() | nil,
shot_charge_weight: String.t() | nil,
dram_equivalent: String.t() | nil,
tracer: boolean(),
incendiary: boolean(),
blank: boolean(),
corrosive: boolean(),
manufacturer: String.t() | nil,
upc: String.t() | nil,
user_id: User.id(),
packs: [Pack.t()] | nil,
inserted_at: NaiveDateTime.t(),
@ -140,17 +150,23 @@ defmodule Cannery.Ammo.Type do
:class,
:bullet_type,
:bullet_core,
:cartridge,
:caliber,
:case_material,
:jacket_type,
:muzzle_velocity,
:powder_type,
:powder_grains_per_charge,
:grains,
:pressure,
:primer_type,
:firing_type,
:manufacturer,
:upc,
:tracer,
:incendiary,
:blank,
:corrosive,
:cartridge,
:jacket_type,
:powder_grains_per_charge,
:muzzle_velocity,
:wadding,
:shot_type,
:shot_material,
@ -160,29 +176,26 @@ defmodule Cannery.Ammo.Type do
:chamber_size,
:load_grains,
:shot_charge_weight,
:dram_equivalent,
:tracer,
:incendiary,
:blank,
:corrosive,
:manufacturer,
:upc
:dram_equivalent
]
@spec string_fields() :: [atom()]
defp string_fields,
do: [
:name,
:desc,
:bullet_type,
:bullet_core,
:cartridge,
:caliber,
:case_material,
:jacket_type,
:powder_type,
:pressure,
:primer_type,
:firing_type,
:manufacturer,
:upc,
:cartridge,
:jacket_type,
:wadding,
:shot_type,
:shot_material,
@ -191,9 +204,7 @@ defmodule Cannery.Ammo.Type do
:brass_height,
:chamber_size,
:shot_charge_weight,
:dram_equivalent,
:manufacturer,
:upc
:dram_equivalent
]
@doc false

View File

@ -7,10 +7,13 @@ defmodule Cannery.Containers do
import Ecto.Query, warn: false
alias Cannery.{Accounts.User, Ammo.Pack, Repo}
alias Cannery.Containers.{Container, ContainerTag, Tag}
alias Ecto.Changeset
alias Ecto.{Changeset, Queryable}
@container_preloads [:tags]
@type list_containers_option :: {:search, String.t() | nil}
@type list_containers_options :: [list_containers_option()]
@doc """
Returns the list of containers.
@ -19,30 +22,31 @@ defmodule Cannery.Containers do
iex> list_containers(%User{id: 123})
[%Container{}, ...]
iex> list_containers("cool", %User{id: 123})
iex> list_containers(%User{id: 123}, search: "cool")
[%Container{name: "my cool container"}, ...]
"""
@spec list_containers(User.t()) :: [Container.t()]
@spec list_containers(search :: nil | String.t(), User.t()) :: [Container.t()]
def list_containers(search \\ nil, %User{id: user_id}) do
@spec list_containers(User.t(), list_containers_options()) :: [Container.t()]
def list_containers(%User{id: user_id}, opts \\ []) do
from(c in Container,
as: :c,
left_join: t in assoc(c, :tags),
on: c.user_id == t.user_id,
as: :t,
where: c.user_id == ^user_id,
order_by: c.name,
distinct: c.id,
preload: ^@container_preloads
)
|> list_containers_search(search)
|> list_containers_search(Keyword.get(opts, :search))
|> Repo.all()
end
defp list_containers_search(query, nil), do: query
defp list_containers_search(query, ""), do: query
@spec list_containers_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t()
defp list_containers_search(query, search) when search in ["", nil],
do: query |> order_by([c: c], c.name)
defp list_containers_search(query, search) do
defp list_containers_search(query, search) when search |> is_binary() do
trimmed_search = String.trim(search)
query
@ -203,9 +207,9 @@ defmodule Cannery.Containers do
{:ok, Container.t()} | {:error, Container.changeset()}
def delete_container(%Container{user_id: user_id} = container, %User{id: user_id}) do
Repo.one(
from ag in Pack,
where: ag.container_id == ^container.id,
select: count(ag.id)
from p in Pack,
where: p.container_id == ^container.id,
select: count(p.id)
)
|> case do
0 ->
@ -289,6 +293,9 @@ defmodule Cannery.Containers do
# Container Tags
@type list_tags_option :: {:search, String.t() | nil}
@type list_tags_options :: [list_tags_option()]
@doc """
Returns the list of tags.
@ -297,38 +304,42 @@ defmodule Cannery.Containers do
iex> list_tags(%User{id: 123})
[%Tag{}, ...]
iex> list_tags("cool", %User{id: 123})
iex> list_tags(%User{id: 123}, search: "cool")
[%Tag{name: "my cool tag"}, ...]
"""
@spec list_tags(User.t()) :: [Tag.t()]
@spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()]
def list_tags(search \\ nil, user)
@spec list_tags(User.t(), list_tags_options()) :: [Tag.t()]
def list_tags(%User{id: user_id}, opts \\ []) do
from(t in Tag, as: :t, where: t.user_id == ^user_id)
|> list_tags_search(Keyword.get(opts, :search))
|> Repo.all()
end
def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "",
do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name)
@spec list_tags_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t()
defp list_tags_search(query, search) when search in ["", nil],
do: query |> order_by([t: t], t.name)
def list_tags(search, %{id: user_id}) when search |> is_binary() do
defp list_tags_search(query, search) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from t in Tag,
where: t.user_id == ^user_id,
where:
fragment(
"? @@ websearch_to_tsquery('english', ?)",
t.search,
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
t.search,
^trimmed_search
)
}
query
|> where(
[t: t],
fragment(
"? @@ websearch_to_tsquery('english', ?)",
t.search,
^trimmed_search
)
)
|> order_by([t: t], {
:desc,
fragment(
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
t.search,
^trimmed_search
)
})
end
@doc """

View File

@ -14,17 +14,17 @@ defmodule Cannery.Logger do
|> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
|> pretty_encode()
Logger.error(meta.reason, data: data)
Logger.error("#{meta.reason}: #{data}")
end
def handle_event([:oban, :job, :start], measure, meta, _config) do
data = get_oban_job_data(meta, measure) |> pretty_encode()
Logger.info("Started oban job", data: data)
Logger.info("Started oban job: #{data}")
end
def handle_event([:oban, :job, :stop], measure, meta, _config) do
data = get_oban_job_data(meta, measure) |> pretty_encode()
Logger.info("Finished oban job", data: data)
Logger.info("Finished oban job: #{data}")
end
def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do
@ -33,7 +33,7 @@ defmodule Cannery.Logger do
|> Map.put(:event, unhandled_event)
|> pretty_encode()
Logger.warning("Unhandled oban job event", data: data)
Logger.warning("Unhandled oban job event: #{data}")
end
def handle_event(unhandled_event, measure, meta, config) do
@ -45,7 +45,7 @@ defmodule Cannery.Logger do
config: config
})
Logger.warning("Unhandled telemetry event", data: data)
Logger.warning("Unhandled telemetry event: #{data}")
end
defp get_oban_job_data(%{job: job}, measure) do

View File

@ -1,53 +1,61 @@
defmodule CanneryWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
as controllers, components, channels, and so on.
This can be used in your application as:
use CanneryWeb, :controller
use CanneryWeb, :view
use CanneryWeb, :html
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
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 any helper function in modules
and import those modules here.
below. Instead, define additional modules and import
those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: CanneryWeb
def static_paths, do: ~w(css js 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 CanneryWeb.Gettext
alias CanneryWeb.Router.Helpers, as: Routes
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def view do
def channel do
quote do
use Phoenix.View,
root: "lib/cannery_web/templates",
namespace: CanneryWeb
use Phoenix.Channel
end
end
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: CanneryWeb.Layouts]
# Include shared imports and aliases for views
unquote(view_helpers())
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Plug.Conn
import CanneryWeb.Gettext
unquote(verified_routes())
end
end
def live_view do
quote do
use Phoenix.LiveView, layout: {CanneryWeb.LayoutView, :live}
use Phoenix.LiveView,
layout: {CanneryWeb.Layouts, :app}
on_mount CanneryWeb.InitAssigns
unquote(view_helpers())
unquote(html_helpers())
end
end
@ -55,49 +63,44 @@ defmodule CanneryWeb do
quote do
use Phoenix.LiveComponent
unquote(view_helpers())
unquote(html_helpers())
end
end
def component do
def html do
quote do
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
use Phoenix.Component
unquote(view_helpers())
# 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
def router do
defp html_helpers do
quote do
use Phoenix.Router
use PhoenixHTMLHelpers
import Phoenix.{Component, HTML, HTML.Form}
import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers}
import Phoenix.{Controller, LiveView.Router}
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Plug.Conn
# Shortcut for generating JS commands
alias Phoenix.LiveView.JS
# Routes generation with the ~p sigil
unquote(verified_routes())
end
end
def channel do
def verified_routes do
quote do
use Phoenix.Channel
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import CanneryWeb.Gettext
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
use Phoenix.HTML
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
# Import basic rendering functionality (render, render_layout, etc)
import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers}
import Phoenix.{Component, View}
alias CanneryWeb.Endpoint
alias CanneryWeb.Router.Helpers, as: Routes
use Phoenix.VerifiedRoutes,
endpoint: CanneryWeb.Endpoint,
router: CanneryWeb.Router,
statics: CanneryWeb.static_paths()
end
end

View File

@ -41,7 +41,6 @@
class: "input input-primary col-span-2",
maxlength: 255,
placeholder: gettext("Really great weather"),
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :notes, "col-span-3") %>

View File

@ -71,8 +71,16 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
current_user: current_user,
tag_actions: tag_actions,
actions: actions,
pack_count: Ammo.get_packs_count_for_containers(containers, current_user),
round_count: Ammo.get_round_count_for_containers(containers, current_user)
pack_count:
Ammo.get_grouped_packs_count(current_user,
containers: containers,
group_by: :container_id
),
round_count:
Ammo.get_grouped_round_count(current_user,
containers: containers,
group_by: :container_id
)
}
rows =
@ -109,14 +117,12 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
end
@spec get_value_for_key(atom(), Container.t(), extra_data :: map) :: any()
defp get_value_for_key(:name, %{id: id, name: container_name}, _extra_data) do
assigns = %{id: id, container_name: container_name}
defp get_value_for_key(:name, %{name: container_name} = assigns, _extra_data) do
{container_name,
~H"""
<div class="flex flex-wrap justify-center items-center">
<.link navigate={Routes.container_show_path(Endpoint, :show, @id)} class="link">
<%= @container_name %>
<.link navigate={~p"/container/#{@id}"} class="link">
<%= @name %>
</.link>
</div>
"""}

View File

@ -3,11 +3,11 @@ defmodule CanneryWeb.CoreComponents do
Provides core UI components.
"""
use Phoenix.Component
import CanneryWeb.{Gettext, ViewHelpers}
use CanneryWeb, :verified_routes
import CanneryWeb.{Gettext, HTMLHelpers}
alias Cannery.{Accounts, Accounts.Invite, Accounts.User}
alias Cannery.{Ammo, Ammo.Pack}
alias Cannery.{Containers.Container, Containers.Tag}
alias CanneryWeb.{Endpoint, HomeLive}
alias CanneryWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.{JS, Rendered}
@ -29,13 +29,13 @@ defmodule CanneryWeb.CoreComponents do
## Examples
<.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}>
<.modal return_to={~p"/\#{<%= schema.plural %>}"}>
<.live_component
module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
id={@<%= schema.singular %>.id || :new}
title={@page_title}
action={@live_action}
return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}
return_to={~p"/\#{<%= schema.singular %>}"}
<%= schema.singular %>: @<%= schema.singular %>
/>
</.modal>

View File

@ -5,7 +5,7 @@
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
<.link navigate={~p"/container/#{@container}"} class="link">
<h1 class="px-4 py-2 rounded-lg title text-xl">
<%= @container.name %>
</h1>
@ -27,15 +27,15 @@
<%= @container.location %>
</span>
<%= if @container |> Ammo.get_packs_count_for_container!(@current_user) != 0 do %>
<%= if Ammo.get_packs_count(@current_user, container_id: @container.id) != 0 do %>
<span class="rounded-lg title text-lg">
<%= gettext("Packs:") %>
<%= @container |> Ammo.get_packs_count_for_container!(@current_user) %>
<%= Ammo.get_packs_count(@current_user, container_id: @container.id) %>
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Rounds:") %>
<%= @container |> Ammo.get_round_count_for_container!(@current_user) %>
<%= Ammo.get_round_count(@current_user, container_id: @container.id) %>
</span>
<% end %>

View File

@ -23,7 +23,7 @@
<% end %>
<.qr_code
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
content={url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")}
filename={@invite.name}
/>
@ -36,7 +36,7 @@
id={"code-#{@invite.id}"}
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
phx-no-format
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
><%= url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
<%= if @code_actions, do: render_slot(@code_actions) %>
</div>

View File

@ -5,7 +5,7 @@
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<.link navigate={Routes.pack_show_path(Endpoint, :show, @pack)} class="mb-2 link">
<.link navigate={~p"/ammo/show/#{@pack}"} class="mb-2 link">
<h1 class="title text-xl title-primary-500">
<%= @pack.type.name %>
</h1>
@ -47,10 +47,15 @@
<%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
</span>
<span :if={@pack.lot_number} class="rounded-lg title text-lg">
<%= gettext("Lot number:") %>
<%= @pack.lot_number %>
</span>
<span :if={@container} class="rounded-lg title text-lg">
<%= gettext("Container:") %>
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
<.link navigate={~p"/container/#{@container}"} class="link">
<%= @container.name %>
</.link>
</span>

View File

@ -1,12 +1,9 @@
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-500">
<div class="flex flex-col sm:flex-row justify-between items-center">
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
<.link
navigate={Routes.live_path(Endpoint, HomeLive)}
class="inline mx-2 my-1 leading-5 text-xl text-white"
>
<.link navigate={~p"/"} class="inline mx-2 my-1 leading-5 text-xl text-white">
<img
src={Routes.static_path(Endpoint, "/images/cannery.svg")}
src={~p"/images/cannery.svg"}
alt={gettext("Cannery logo")}
class="inline-block h-8 mx-1"
/>
@ -27,64 +24,43 @@
text-lg text-white text-ellipsis">
<%= if @current_user do %>
<li class="mx-2 my-1">
<.link
navigate={Routes.tag_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<.link navigate={~p"/tags"} class="text-white hover:underline">
<%= gettext("Tags") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
navigate={Routes.container_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<.link navigate={~p"/containers"} class="text-white hover:underline">
<%= gettext("Containers") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
navigate={Routes.type_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<.link navigate={~p"/catalog"} class="text-white hover:underline">
<%= gettext("Catalog") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
navigate={Routes.pack_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<.link navigate={~p"/ammo"} class="text-white hover:underline">
<%= gettext("Ammo") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
navigate={Routes.range_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<.link navigate={~p"/range"} class="text-white hover:underline">
<%= gettext("Range") %>
</.link>
</li>
<li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
<.link
navigate={Routes.invite_index_path(Endpoint, :index)}
class="text-white hover:underline"
>
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
<.link navigate={~p"/invites"} class="text-white hover:underline">
<%= gettext("Invites") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
href={Routes.user_settings_path(Endpoint, :edit)}
class="text-white hover:underline truncate"
>
<.link href={~p"/users/settings"} class="text-white hover:underline truncate">
<%= @current_user.email %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
href={Routes.user_session_path(Endpoint, :delete)}
href={~p"/users/log_out"}
method="delete"
data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
aria-label={gettext("Log out")}
@ -94,13 +70,13 @@
</li>
<li
:if={
@current_user |> Accounts.is_already_admin?() and
@current_user |> Accounts.already_admin?() and
function_exported?(Routes, :live_dashboard_path, 2)
}
class="mx-2 my-1"
>
<.link
navigate={Routes.live_dashboard_path(Endpoint, :home)}
navigate={~p"/dashboard"}
class="text-white hover:underline"
aria-label={gettext("Live Dashboard")}
>
@ -109,18 +85,12 @@
</li>
<% else %>
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
<.link
href={Routes.user_registration_path(Endpoint, :new)}
class="text-white hover:underline truncate"
>
<.link href={~p"/users/register"} class="text-white hover:underline truncate">
<%= dgettext("actions", "Register") %>
</.link>
</li>
<li class="mx-2 my-1">
<.link
href={Routes.user_session_path(Endpoint, :new)}
class="text-white hover:underline truncate"
>
<.link href={~p"/users/log_in"} class="text-white hover:underline truncate">
<%= dgettext("actions", "Log in") %>
</.link>
</li>

View File

@ -0,0 +1,10 @@
defmodule CanneryWeb.EmailHTML do
@moduledoc """
Renders email templates
"""
use CanneryWeb, :html
embed_templates "email_html/*.html", suffix: "_html"
embed_templates "email_html/*.txt", suffix: "_text"
end

View File

@ -9,4 +9,4 @@
<%= dgettext("emails",
"If you didn't create an account at %{url}, please ignore this.",
url: Routes.live_url(Endpoint, HomeLive)) %>
url: ~p"/") %>

View File

@ -7,4 +7,4 @@
<%= dgettext("emails",
"If you didn't request this change from %{url}, please ignore this.",
url: Routes.live_url(Endpoint, HomeLive)) %>
url: ~p"/") %>

View File

@ -7,4 +7,4 @@
<%= dgettext("emails",
"If you didn't request this change from %{url}, please ignore this.",
url: Routes.live_url(Endpoint, HomeLive)) %>
url: ~p"/") %>

View File

@ -0,0 +1,17 @@
defmodule CanneryWeb.Layouts do
@moduledoc """
The root layouts for the entire application
"""
use CanneryWeb, :html
embed_templates "layouts/*"
def get_title(%{assigns: %{title: title}}) when title not in [nil, ""] do
gettext("Cannery | %{title}", title: title)
end
def get_title(_conn) do
gettext("Cannery")
end
end

View File

@ -9,7 +9,7 @@
<hr style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;" />
<a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, HomeLive)}>
<a style="color: rgb(31, 31, 31);" href={~p"/"}>
<%= dgettext(
"emails",
"This email was sent from Cannery, the self-hosted firearm tracker website."

View File

@ -8,4 +8,4 @@
<%= dgettext("emails",
"This email was sent from Cannery at %{url}, the self-hosted firearm tracker website.",
url: Routes.live_url(Endpoint, HomeLive)) %>
url: ~p"/") %>

View File

@ -0,0 +1 @@
<%= @inner_block %>

View File

@ -5,21 +5,12 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %>
<link
rel="shortcut icon"
type="image/jpg"
href={Routes.static_path(@conn, "/images/cannery.svg")}
/>
<link rel="shortcut icon" type="image/jpg" href={~p"/images/cannery.svg"} />
<.live_title suffix={" | #{gettext("Cannery")}"}>
<%= assigns[:page_title] || gettext("Cannery") %>
</.live_title>
<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")}
>
<link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
</script>
</head>

View File

@ -5,7 +5,6 @@ defmodule CanneryWeb.Components.MovePackComponent do
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Ammo, Ammo.Pack, Containers, Containers.Container}
alias CanneryWeb.Endpoint
alias Ecto.Changeset
alias Phoenix.LiveView.Socket
@ -84,13 +83,13 @@ defmodule CanneryWeb.Components.MovePackComponent do
<%= display_emoji("😔") %>
</h2>
<.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
<.link navigate={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "Add another container!") %>
</.link>
<% else %>
<.live_component
module={CanneryWeb.Components.TableComponent}
id="move_pack_table"
id="move-pack-table"
columns={@columns}
rows={@rows}
/>

View File

@ -53,6 +53,9 @@ defmodule CanneryWeb.Components.PackTableComponent do
}
} = socket
) do
lot_number_used = packs |> Enum.any?(fn %{lot_number: lot_number} -> !!lot_number end)
price_paid_used = packs |> Enum.any?(fn %{price_paid: price_paid} -> !!price_paid end)
columns =
[]
|> TableComponent.maybe_compose_columns(
@ -77,8 +80,18 @@ defmodule CanneryWeb.Components.PackTableComponent do
%{label: gettext("Range"), key: :range},
range != []
)
|> TableComponent.maybe_compose_columns(%{label: gettext("CPR"), key: :cpr})
|> TableComponent.maybe_compose_columns(%{label: gettext("Price paid"), key: :price_paid})
|> TableComponent.maybe_compose_columns(
%{label: gettext("Lot number"), key: :lot_number},
lot_number_used
)
|> TableComponent.maybe_compose_columns(
%{label: gettext("CPR"), key: :cpr},
price_paid_used
)
|> TableComponent.maybe_compose_columns(
%{label: gettext("Price paid"), key: :price_paid},
price_paid_used
)
|> TableComponent.maybe_compose_columns(
%{label: gettext("% left"), key: :remaining},
show_used
@ -128,7 +141,12 @@ defmodule CanneryWeb.Components.PackTableComponent do
def render(assigns) do
~H"""
<div id={@id} class="w-full">
<.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} />
<.live_component
module={TableComponent}
id={"pack-table-#{@id}"}
columns={@columns}
rows={@rows}
/>
</div>
"""
end

View File

@ -74,7 +74,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
<div id={@id} class="w-full">
<.live_component
module={CanneryWeb.Components.TableComponent}
id={"table-#{@id}"}
id={"shot-record-table-#{@id}"}
columns={@columns}
rows={@rows}
initial_key={:date}
@ -98,7 +98,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
{pack.type.name,
~H"""
<.link navigate={Routes.pack_show_path(Endpoint, :show, @pack)} class="link">
<.link navigate={~p"/ammo/show/#{@pack}"} class="link">
<%= @pack.type.name %>
</.link>
"""}

View File

@ -55,7 +55,6 @@ defmodule CanneryWeb.Components.TypeTableComponent do
%{label: gettext("Unfired shell length"), key: :unfired_length, type: :string},
%{label: gettext("Brass height"), key: :brass_height, type: :string},
%{label: gettext("Chamber size"), key: :chamber_size, type: :string},
%{label: gettext("Chamber size"), key: :chamber_size, type: :string},
%{label: gettext("Grains"), key: :grains, type: :string},
%{label: gettext("Bullet type"), key: :bullet_type, type: :string},
%{
@ -152,17 +151,25 @@ defmodule CanneryWeb.Components.TypeTableComponent do
)
|> TableComponent.maybe_compose_columns(%{label: gettext("Name"), key: :name, type: :name})
round_counts = types |> Ammo.get_round_count_for_types(current_user)
packs_count = types |> Ammo.get_packs_count_for_types(current_user)
average_costs = types |> Ammo.get_average_cost_for_types(current_user)
round_counts = Ammo.get_grouped_round_count(current_user, types: types, group_by: :type_id)
packs_count = Ammo.get_grouped_packs_count(current_user, types: types, group_by: :type_id)
average_costs = Ammo.get_average_costs(types, current_user)
[used_counts, historical_round_counts, historical_pack_counts, used_pack_counts] =
if show_used do
[
types |> ActivityLog.get_used_count_for_types(current_user),
types |> Ammo.get_historical_count_for_types(current_user),
types |> Ammo.get_packs_count_for_types(current_user, true),
types |> Ammo.get_used_packs_count_for_types(current_user)
ActivityLog.get_grouped_used_counts(current_user, types: types, group_by: :type_id),
Ammo.get_historical_counts(types, current_user),
Ammo.get_grouped_packs_count(current_user,
types: types,
group_by: :type_id,
show_used: true
),
Ammo.get_grouped_packs_count(current_user,
types: types,
group_by: :type_id,
show_used: :only_used
)
]
else
[nil, nil, nil, nil]
@ -193,7 +200,12 @@ defmodule CanneryWeb.Components.TypeTableComponent do
def render(assigns) do
~H"""
<div id={@id} class="w-full">
<.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} />
<.live_component
module={TableComponent}
id={"type-table-#{@id}"}
columns={@columns}
rows={@rows}
/>
</div>
"""
end
@ -262,13 +274,11 @@ defmodule CanneryWeb.Components.TypeTableComponent do
end
end
defp get_type_value(:name, _key, %{name: type_name} = type, _other_data) do
assigns = %{type: type}
defp get_type_value(:name, _key, %{name: type_name} = assigns, _other_data) do
{type_name,
~H"""
<.link navigate={Routes.type_show_path(Endpoint, :show, @type)} class="link">
<%= @type.name %>
<.link navigate={~p"/type/#{@id}"} class="link">
<%= @name %>
</.link>
"""}
end

View File

@ -6,7 +6,8 @@ defmodule CanneryWeb.EmailController do
use CanneryWeb, :controller
alias Cannery.Accounts.User
plug :put_layout, {CanneryWeb.LayoutView, :email}
plug :put_root_layout, html: {CanneryWeb.Layouts, :email_html}
plug :put_layout, false
@sample_assigns %{
email: %{subject: "Example subject"},
@ -18,6 +19,6 @@ defmodule CanneryWeb.EmailController do
Debug route used to preview emails
"""
def preview(conn, %{"id" => template}) do
render(conn, "#{template |> to_string()}.html", @sample_assigns)
render(conn, String.to_existing_atom(template), @sample_assigns)
end
end

View File

@ -0,0 +1,16 @@
defmodule CanneryWeb.ErrorHTML do
use CanneryWeb, :html
embed_templates "error_html/*"
def render(template, _assigns) do
error_string =
case template do
"404.html" -> dgettext("errors", "Not found")
"401.html" -> dgettext("errors", "Unauthorized")
_other_path -> dgettext("errors", "Internal server error")
end
error(%{error_string: error_string})
end
end

View File

@ -24,10 +24,7 @@
<hr class="w-full hr" />
<.link
href={Routes.live_path(Endpoint, HomeLive)}
class="link title text-primary-600 text-lg"
>
<.link href={~p"/"} class="link title text-primary-600 text-lg">
<%= dgettext("errors", "Go back home") %>
</.link>
</div>

View File

@ -0,0 +1,14 @@
defmodule CanneryWeb.ErrorJSON do
import CanneryWeb.Gettext
def render(template, _assigns) do
error_string =
case template do
"404.json" -> dgettext("errors", "Not found")
"401.json" -> dgettext("errors", "Unauthorized")
_other_path -> dgettext("errors", "Internal server error")
end
%{errors: %{detail: error_string}}
end
end

View File

@ -3,14 +3,22 @@ defmodule CanneryWeb.ExportController do
alias Cannery.{ActivityLog, Ammo, Containers}
def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do
types = Ammo.list_types(current_user, :all)
used_counts = types |> ActivityLog.get_used_count_for_types(current_user)
round_counts = types |> Ammo.get_round_count_for_types(current_user)
pack_counts = types |> Ammo.get_packs_count_for_types(current_user)
types = Ammo.list_types(current_user)
total_pack_counts = types |> Ammo.get_packs_count_for_types(current_user, true)
used_counts =
ActivityLog.get_grouped_used_counts(current_user, types: types, group_by: :type_id)
average_costs = types |> Ammo.get_average_cost_for_types(current_user)
round_counts = Ammo.get_grouped_round_count(current_user, types: types, group_by: :type_id)
pack_counts = Ammo.get_grouped_packs_count(current_user, types: types, group_by: :type_id)
total_pack_counts =
Ammo.get_grouped_packs_count(current_user,
types: types,
group_by: :type_id,
show_used: true
)
average_costs = Ammo.get_average_costs(types, current_user)
types =
types
@ -27,8 +35,11 @@ defmodule CanneryWeb.ExportController do
})
end)
packs = Ammo.list_packs(nil, :all, current_user, true)
used_counts = packs |> ActivityLog.get_used_counts(current_user)
packs = Ammo.list_packs(current_user, show_used: true)
used_counts =
ActivityLog.get_grouped_used_counts(current_user, packs: packs, group_by: :pack_id)
original_counts = packs |> Ammo.get_original_counts(current_user)
cprs = packs |> Ammo.get_cprs(current_user)
percentages_remaining = packs |> Ammo.get_percentages_remaining(current_user)
@ -47,20 +58,17 @@ defmodule CanneryWeb.ExportController do
})
end)
shot_records = ActivityLog.list_shot_records(:all, current_user)
shot_records = ActivityLog.list_shot_records(current_user)
containers =
Containers.list_containers(current_user)
|> Enum.map(fn container ->
pack_count = container |> Ammo.get_packs_count_for_container!(current_user)
round_count = container |> Ammo.get_round_count_for_container!(current_user)
container
|> Jason.encode!()
|> Jason.decode!()
|> Map.merge(%{
"pack_count" => pack_count,
"round_count" => round_count
"pack_count" => Ammo.get_packs_count(current_user, container_id: container.id),
"round_count" => Ammo.get_round_count(current_user, container_id: container.id)
})
end)

View File

@ -1,11 +0,0 @@
defmodule CanneryWeb.HomeController do
@moduledoc """
Controller for home page
"""
use CanneryWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end

View File

@ -0,0 +1,5 @@
defmodule CanneryWeb.HomeHTML do
use CanneryWeb, :html
embed_templates "home_html/*"
end

View File

@ -3,12 +3,11 @@ defmodule CanneryWeb.UserAuth do
Functions for user session and authentication
"""
use CanneryWeb, :verified_routes
import Plug.Conn
import Phoenix.Controller
import CanneryWeb.Gettext
alias Cannery.{Accounts, Accounts.User}
alias CanneryWeb.HomeLive
alias CanneryWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
@ -39,7 +38,7 @@ defmodule CanneryWeb.UserAuth do
dgettext("errors", "You must confirm your account and log in to access this page.")
)
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> redirect(to: ~p"/users/log_in")
|> halt()
end
@ -49,8 +48,7 @@ defmodule CanneryWeb.UserAuth do
conn
|> renew_session()
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|> put_token_in_session(token)
|> maybe_write_remember_me_cookie(token, params)
|> redirect(to: user_return_to || signed_in_path(conn))
end
@ -96,7 +94,7 @@ defmodule CanneryWeb.UserAuth do
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
user_token && Accounts.delete_user_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
CanneryWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
@ -105,7 +103,7 @@ defmodule CanneryWeb.UserAuth do
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
|> redirect(to: ~p"/")
end
@doc """
@ -119,19 +117,110 @@ defmodule CanneryWeb.UserAuth do
end
defp ensure_user_token(conn) do
if user_token = get_session(conn, :user_token) do
{user_token, conn}
if token = get_session(conn, :user_token) do
{token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
if user_token = conn.cookies[@remember_me_cookie] do
{user_token, put_session(conn, :user_token, user_token)}
if token = conn.cookies[@remember_me_cookie] do
{token, put_token_in_session(conn, token)}
else
{nil, conn}
end
end
end
@doc """
Handles mounting and authenticating the current_user in LiveViews.
## `on_mount` arguments
* `:mount_current_user` - Assigns current_user
to socket assigns based on user_token, or nil if
there's no user_token or no matching user.
* `:ensure_authenticated` - Authenticates the user from the session,
and assigns the current_user to socket assigns based
on user_token.
Redirects to login page if there's no logged user.
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
Redirects to signed_in_path if there's a logged user.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:
defmodule CanneryWeb.PageLive do
use CanneryWeb, :live_view
on_mount {CanneryWeb.UserAuth, :mount_current_user}
...
end
Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{CanneryWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, mount_current_user(session, socket)}
end
def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)
if socket.assigns.current_user do
{:cont, socket}
else
error_flash = dgettext("errors", "You must log in to access this page.")
socket =
socket
|> Phoenix.LiveView.put_flash(:error, error_flash)
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
{:halt, socket}
end
end
def on_mount(:ensure_admin, _params, session, socket) do
socket = mount_current_user(session, socket)
if socket.assigns.current_user && socket.assigns.current_user.role == :admin do
{:cont, socket}
else
error_flash = dgettext("errors", "You must log in as an administrator to access this page.")
socket =
socket
|> Phoenix.LiveView.put_flash(:error, error_flash)
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
{:halt, socket}
end
end
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)
if socket.assigns.current_user do
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
else
{:cont, socket}
end
end
defp mount_current_user(session, socket) do
Phoenix.Component.assign_new(socket, :current_user, fn ->
if user_token = session["user_token"] do
Accounts.get_user_by_session_token(user_token)
end
end)
end
@doc """
Used for routes that require the user to not be authenticated.
"""
@ -161,7 +250,7 @@ defmodule CanneryWeb.UserAuth do
dgettext("errors", "You must confirm your account and log in to access this page.")
)
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> redirect(to: ~p"/users/log_in")
|> halt()
end
end
@ -176,16 +265,34 @@ defmodule CanneryWeb.UserAuth do
conn
|> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
|> maybe_store_return_to()
|> redirect(to: Routes.live_path(conn, HomeLive))
|> redirect(to: ~p"/")
|> halt()
end
end
def put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
default = Application.fetch_env!(:gettext, :default_locale)
Gettext.put_locale(locale || default)
conn |> put_session(:locale, locale || default)
end
def put_user_locale(conn, _opts) do
default = Application.fetch_env!(:gettext, :default_locale)
Gettext.put_locale(default)
conn |> put_session(:locale, default)
end
defp put_token_in_session(conn, token) do
conn
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: "/"
defp signed_in_path(_conn), do: ~p"/"
end

View File

@ -5,14 +5,14 @@ defmodule CanneryWeb.UserConfirmationController do
alias Cannery.Accounts
def new(conn, _params) do
render(conn, "new.html", page_title: gettext("Confirm your account"))
render(conn, :new, page_title: gettext("Confirm your account"))
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
fn token -> url(CanneryWeb.Endpoint, ~p"/users/confirm/#{token}") end
)
end
@ -22,11 +22,10 @@ defmodule CanneryWeb.UserConfirmationController do
:info,
dgettext(
"prompts",
"If your email is in our system and it has not been confirmed yet, " <>
"you will receive an email with instructions shortly."
"If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
)
)
|> redirect(to: "/")
|> redirect(to: ~p"/")
end
# Do not log in the user after confirmation to avoid a
@ -36,7 +35,7 @@ defmodule CanneryWeb.UserConfirmationController do
{:ok, %{email: email}} ->
conn
|> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
|> redirect(to: "/")
|> redirect(to: ~p"/")
:error ->
# If there is a current user and the account was already confirmed,
@ -45,7 +44,7 @@ defmodule CanneryWeb.UserConfirmationController do
# a warning message.
case conn.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
redirect(conn, to: "/")
redirect(conn, to: ~p"/")
%{} ->
conn
@ -53,7 +52,7 @@ defmodule CanneryWeb.UserConfirmationController do
:error,
dgettext("errors", "User confirmation link is invalid or it has expired.")
)
|> redirect(to: "/")
|> redirect(to: ~p"/")
end
end
end

View File

@ -0,0 +1,6 @@
defmodule CanneryWeb.UserConfirmationHTML do
use CanneryWeb, :html
alias Cannery.Accounts
embed_templates "user_confirmation_html/*"
end

View File

@ -7,7 +7,7 @@
:let={f}
for={%{}}
as={:user}
action={Routes.user_confirmation_path(@conn, :create)}
action={~p"/users/confirm"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
@ -21,14 +21,10 @@
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link
:if={Accounts.allow_registration?()}
href={Routes.user_registration_path(@conn, :new)}
class="btn btn-primary"
>
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
</.link>
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
</.link>
</div>

View File

@ -2,7 +2,6 @@ defmodule CanneryWeb.UserRegistrationController do
use CanneryWeb, :controller
import CanneryWeb.Gettext
alias Cannery.{Accounts, Accounts.Invites}
alias CanneryWeb.{Endpoint, HomeLive}
alias Ecto.Changeset
def new(conn, %{"invite" => invite_token}) do
@ -11,7 +10,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|> redirect(to: ~p"/")
end
end
@ -21,13 +20,13 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|> redirect(to: ~p"/")
end
end
# renders new user registration page
defp render_new(conn, invite_token \\ nil) do
render(conn, "new.html",
render(conn, :new,
changeset: Accounts.change_user_registration(),
invite_token: invite_token,
page_title: gettext("Register")
@ -40,7 +39,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|> redirect(to: ~p"/")
end
end
@ -50,7 +49,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|> redirect(to: ~p"/")
end
end
@ -59,20 +58,20 @@ defmodule CanneryWeb.UserRegistrationController do
{:ok, user} ->
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
fn token -> url(CanneryWeb.Endpoint, ~p"/users/confirm/#{token}") end
)
conn
|> put_flash(:info, dgettext("prompts", "Please check your email to verify your account"))
|> redirect(to: Routes.user_session_path(Endpoint, :new))
|> redirect(to: ~p"/users/log_in")
{:error, :invalid_token} ->
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|> redirect(to: ~p"/")
{:error, %Changeset{} = changeset} ->
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
conn |> render(:new, changeset: changeset, invite_token: invite_token)
end
end
end

View File

@ -0,0 +1,5 @@
defmodule CanneryWeb.UserRegistrationHTML do
use CanneryWeb, :html
embed_templates "user_registration_html/*"
end

View File

@ -6,7 +6,7 @@
<.form
:let={f}
for={@changeset}
action={Routes.user_registration_path(@conn, :create)}
action={~p"/users/register"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
@ -40,10 +40,10 @@
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
</.link>
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/reset_password"} class="btn btn-primary">
<%= dgettext("actions", "Forgot your password?") %>
</.link>
</div>

View File

@ -6,14 +6,14 @@ defmodule CanneryWeb.UserResetPasswordController do
plug :get_user_by_reset_password_token when action in [:edit, :update]
def new(conn, _params) do
render(conn, "new.html", page_title: gettext("Forgot your password?"))
render(conn, :new, page_title: gettext("Forgot your password?"))
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_reset_password_instructions(
user,
&Routes.user_reset_password_url(conn, :edit, &1)
fn token -> url(CanneryWeb.Endpoint, ~p"/users/reset_password/#{token}") end
)
end
@ -23,15 +23,14 @@ defmodule CanneryWeb.UserResetPasswordController do
:info,
dgettext(
"prompts",
"If your email is in our system, you will receive instructions to " <>
"reset your password shortly."
"If your email is in our system, you will receive instructions to reset your password shortly."
)
)
|> redirect(to: "/")
|> redirect(to: ~p"/")
end
def edit(conn, _params) do
render(conn, "edit.html",
render(conn, :edit,
changeset: Accounts.change_user_password(conn.assigns.user),
page_title: gettext("Reset your password")
)
@ -44,10 +43,10 @@ defmodule CanneryWeb.UserResetPasswordController do
{:ok, _} ->
conn
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|> redirect(to: Routes.user_session_path(conn, :new))
|> redirect(to: ~p"/users/log_in")
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
render(conn, :edit, changeset: changeset)
end
end
@ -62,7 +61,7 @@ defmodule CanneryWeb.UserResetPasswordController do
:error,
dgettext("errors", "Reset password link is invalid or it has expired.")
)
|> redirect(to: "/")
|> redirect(to: ~p"/")
|> halt()
end
end

View File

@ -0,0 +1,6 @@
defmodule CanneryWeb.UserResetPasswordHTML do
use CanneryWeb, :html
alias Cannery.Accounts
embed_templates "user_reset_password_html/*"
end

View File

@ -6,7 +6,7 @@
<.form
:let={f}
for={@changeset}
action={Routes.user_reset_password_path(@conn, :update, @token)}
action={~p"/users/reset_password/#{@token}"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
@ -34,14 +34,10 @@
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link
:if={Accounts.allow_registration?()}
href={Routes.user_registration_path(@conn, :new)}
class="btn btn-primary"
>
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
</.link>
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
</.link>
</div>

View File

@ -7,7 +7,7 @@
:let={f}
for={%{}}
as={:user}
action={Routes.user_reset_password_path(@conn, :create)}
action={~p"/users/reset_password"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
@ -21,14 +21,10 @@
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link
:if={Accounts.allow_registration?()}
href={Routes.user_registration_path(@conn, :new)}
class="btn btn-primary"
>
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
</.link>
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
</.link>
</div>

View File

@ -5,7 +5,7 @@ defmodule CanneryWeb.UserSessionController do
alias CanneryWeb.UserAuth
def new(conn, _params) do
render(conn, "new.html", error_message: nil, page_title: gettext("Log in"))
render(conn, :new, error_message: nil, page_title: gettext("Log in"))
end
def create(conn, %{"user" => user_params}) do
@ -14,7 +14,7 @@ defmodule CanneryWeb.UserSessionController do
if user = Accounts.get_user_by_email_and_password(email, password) do
UserAuth.log_in_user(conn, user, user_params)
else
render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password"))
render(conn, :new, error_message: dgettext("errors", "Invalid email or password"))
end
end

View File

@ -0,0 +1,6 @@
defmodule CanneryWeb.UserSessionHTML do
use CanneryWeb, :html
alias Cannery.Accounts
embed_templates "user_session_html/*"
end

View File

@ -6,8 +6,8 @@
<.form
:let={f}
for={@conn}
action={Routes.user_session_path(@conn, :create)}
as="user"
action={~p"/users/log_in"}
as={:user}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<p :if={@error_message} class="alert alert-danger col-span-3">
@ -15,10 +15,18 @@
</p>
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
<%= email_input(f, :email,
autocomplete: :email,
class: "input input-primary col-span-2",
required: true
) %>
<%= label(f, :password, gettext("Password"), class: "title text-lg text-primary-600") %>
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
<%= password_input(f, :password,
autocomplete: "current-password",
class: "input input-primary col-span-2",
required: true
) %>
<%= label(f, :remember_me, gettext("Keep me logged in for 60 days"),
class: "title text-lg text-primary-600"
@ -31,14 +39,10 @@
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link
:if={Accounts.allow_registration?()}
href={Routes.user_registration_path(@conn, :new)}
class="btn btn-primary"
>
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
</.link>
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
<.link href={~p"/users/reset_password"} class="btn btn-primary">
<%= dgettext("actions", "Forgot your password?") %>
</.link>
</div>

View File

@ -2,12 +2,12 @@ defmodule CanneryWeb.UserSettingsController do
use CanneryWeb, :controller
import CanneryWeb.Gettext
alias Cannery.Accounts
alias CanneryWeb.{HomeLive, UserAuth}
alias CanneryWeb.UserAuth
plug :assign_email_and_password_changesets
def edit(conn, _params) do
render(conn, "edit.html", page_title: gettext("Settings"))
render(conn, :edit, page_title: gettext("Settings"))
end
def update(%{assigns: %{current_user: user}} = conn, %{
@ -20,7 +20,7 @@ defmodule CanneryWeb.UserSettingsController do
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&Routes.user_settings_url(conn, :confirm_email, &1)
fn token -> url(CanneryWeb.Endpoint, ~p"/users/settings/confirm_email/#{token}") end
)
conn
@ -31,10 +31,10 @@ defmodule CanneryWeb.UserSettingsController do
"A link to confirm your email change has been sent to the new address."
)
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
|> redirect(to: ~p"/users/settings")
{:error, changeset} ->
conn |> render("edit.html", email_changeset: changeset)
conn |> render(:edit, email_changeset: changeset)
end
end
@ -47,11 +47,11 @@ defmodule CanneryWeb.UserSettingsController do
{:ok, user} ->
conn
|> put_flash(:info, dgettext("prompts", "Password updated successfully."))
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|> put_session(:user_return_to, ~p"/users/settings")
|> UserAuth.log_in_user(user)
{:error, changeset} ->
conn |> render("edit.html", password_changeset: changeset)
conn |> render(:edit, password_changeset: changeset)
end
end
@ -63,10 +63,10 @@ defmodule CanneryWeb.UserSettingsController do
{:ok, _user} ->
conn
|> put_flash(:info, dgettext("prompts", "Language updated successfully."))
|> redirect(to: Routes.user_settings_path(conn, :edit))
|> redirect(to: ~p"/users/settings")
{:error, changeset} ->
conn |> render("edit.html", locale_changeset: changeset)
conn |> render(:edit, locale_changeset: changeset)
end
end
@ -75,7 +75,7 @@ defmodule CanneryWeb.UserSettingsController do
:ok ->
conn
|> put_flash(:info, dgettext("prompts", "Email changed successfully."))
|> redirect(to: Routes.user_settings_path(conn, :edit))
|> redirect(to: ~p"/users/settings")
:error ->
conn
@ -83,7 +83,7 @@ defmodule CanneryWeb.UserSettingsController do
:error,
dgettext("errors", "Email change link is invalid or it has expired.")
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
|> redirect(to: ~p"/users/settings")
end
end
@ -93,11 +93,11 @@ defmodule CanneryWeb.UserSettingsController do
conn
|> put_flash(:error, dgettext("prompts", "Your account has been deleted"))
|> redirect(to: Routes.live_path(conn, HomeLive))
|> redirect(to: ~p"/")
else
conn
|> put_flash(:error, dgettext("errors", "Unable to delete user"))
|> redirect(to: Routes.user_settings_path(conn, :edit))
|> redirect(to: ~p"/users/settings")
end
end

View File

@ -0,0 +1,5 @@
defmodule CanneryWeb.UserSettingsHTML do
use CanneryWeb, :html
embed_templates "user_settings_html/*"
end

View File

@ -8,7 +8,7 @@
<.form
:let={f}
for={@email_changeset}
action={Routes.user_settings_path(@conn, :update)}
action={~p"/users/settings"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<h3 class="title text-primary-600 text-lg text-center col-span-3">
@ -50,7 +50,7 @@
<.form
:let={f}
for={@password_changeset}
action={Routes.user_settings_path(@conn, :update)}
action={~p"/users/settings"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<h3 class="title text-primary-600 text-lg text-center col-span-3">
@ -104,7 +104,7 @@
<.form
:let={f}
for={@locale_changeset}
action={Routes.user_settings_path(@conn, :update)}
action={~p"/users/settings"}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<%= label(f, :locale, dgettext("actions", "Change Language"),
@ -142,17 +142,13 @@
<hr class="hr" />
<div class="flex justify-center items-center">
<.link
href={Routes.export_path(@conn, :export, :json)}
class="mx-4 my-2 btn btn-primary"
target="_blank"
>
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
<%= dgettext("actions", "Export Data as JSON") %>
</.link>
<.link
href={Routes.user_settings_path(@conn, :delete, @current_user)}
method={:delete}
href={~p"/users/settings/#{@current_user}"}
method="delete"
class="mx-4 my-2 btn btn-alert"
data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")}
>

View File

@ -20,7 +20,7 @@ defmodule CanneryWeb.Endpoint do
at: "/",
from: :cannery,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
only: CanneryWeb.static_paths()
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.

View File

@ -3,8 +3,8 @@ defmodule CanneryWeb.ErrorHelpers do
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
import Phoenix.Component
use PhoenixHTMLHelpers
import Phoenix.{Component, HTML.Form}
alias Ecto.Changeset
alias Phoenix.{HTML.Form, LiveView.Rendered}
@ -65,7 +65,7 @@ defmodule CanneryWeb.ErrorHelpers do
changeset
|> changeset_error_map()
|> Enum.map_join(". ", fn {key, errors} ->
"#{key |> humanize()}: #{errors |> Enum.join(", ")}"
"#{key |> Phoenix.Naming.humanize()}: #{errors |> Enum.join(", ")}"
end)
end

View File

@ -1,8 +1,6 @@
defmodule CanneryWeb.ViewHelpers do
defmodule CanneryWeb.HTMLHelpers do
@moduledoc """
Contains common helpers that can be used in liveviews and regular views. These
are automatically imported into any Phoenix View using `use CanneryWeb,
:view`
Contains common helpers that are used for rendering
"""
use Phoenix.Component

View File

@ -31,7 +31,6 @@
id: "container-form-desc",
class: "input input-primary col-span-2",
placeholder: gettext("Metal ammo can with the anime girl sticker"),
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :desc, "col-span-3 text-center") %>
@ -49,7 +48,6 @@
id: "container-form-location",
class: "input input-primary col-span-2",
placeholder: gettext("On the bookshelf"),
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :location, "col-span-3 text-center") %>

View File

@ -105,15 +105,14 @@ defmodule CanneryWeb.ContainerLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.container_index_path(Endpoint, :index))}
{:noreply, socket |> push_patch(to: ~p"/containers")}
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
{:noreply,
socket |> push_patch(to: Routes.container_index_path(Endpoint, :search, search_term))}
{:noreply, socket |> push_patch(to: ~p"/containers/search/#{search_term}")}
end
defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do
socket |> assign(:containers, Containers.list_containers(search, current_user))
socket |> assign(:containers, Containers.list_containers(current_user, search: search))
end
end

View File

@ -9,11 +9,11 @@
<%= display_emoji("😔") %>
</h2>
<.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "Add your first container!") %>
</.link>
<% else %>
<.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "New Container") %>
</.link>
@ -51,7 +51,7 @@
<%= if @view_table do %>
<.live_component
module={CanneryWeb.Components.ContainerTableComponent}
id="containers_index_table"
id="containers-index-table"
action={@live_action}
containers={@containers}
current_user={@current_user}
@ -59,7 +59,7 @@
<:tag_actions :let={container}>
<div class="mx-4 my-2">
<.link
patch={Routes.container_index_path(Endpoint, :edit_tags, container)}
patch={~p"/containers/edit_tags/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Tag %{container_name}", container_name: container.name)
@ -71,7 +71,7 @@
</:tag_actions>
<:actions :let={container}>
<.link
patch={Routes.container_index_path(Endpoint, :edit, container)}
patch={~p"/containers/edit/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit %{container_name}", container_name: container.name)
@ -81,7 +81,7 @@
</.link>
<.link
patch={Routes.container_index_path(Endpoint, :clone, container)}
patch={~p"/containers/clone/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Clone %{container_name}", container_name: container.name)
@ -118,7 +118,7 @@
<:tag_actions>
<div class="mx-4 my-2">
<.link
patch={Routes.container_index_path(Endpoint, :edit_tags, container)}
patch={~p"/containers/edit_tags/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Tag %{container_name}", container_name: container.name)
@ -129,7 +129,7 @@
</div>
</:tag_actions>
<.link
patch={Routes.container_index_path(Endpoint, :edit, container)}
patch={~p"/containers/edit/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit %{container_name}", container_name: container.name)
@ -139,7 +139,7 @@
</.link>
<.link
patch={Routes.container_index_path(Endpoint, :clone, container)}
patch={~p"/containers/clone/#{container}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Clone %{container_name}", container_name: container.name)
@ -173,26 +173,26 @@
<%= case @live_action do %>
<% modifying when modifying in [:new, :edit, :clone] -> %>
<.modal return_to={Routes.container_index_path(Endpoint, :index)}>
<.modal return_to={~p"/containers"}>
<.live_component
module={CanneryWeb.ContainerLive.FormComponent}
id={@container.id || :new}
title={@page_title}
action={@live_action}
container={@container}
return_to={Routes.container_index_path(Endpoint, :index)}
return_to={~p"/containers"}
current_user={@current_user}
/>
</.modal>
<% :edit_tags -> %>
<.modal return_to={Routes.container_index_path(Endpoint, :index)}>
<.modal return_to={~p"/containers"}>
<.live_component
module={CanneryWeb.ContainerLive.EditTagsComponent}
id={@container.id}
title={@page_title}
action={@live_action}
container={@container}
current_path={Routes.container_index_path(Endpoint, :edit_tags, @container)}
current_path={~p"/containers/edit_tags/#{@container}"}
current_user={@current_user}
/>
</.modal>

View File

@ -5,7 +5,6 @@ defmodule CanneryWeb.ContainerLive.Show do
use CanneryWeb, :live_view
alias Cannery.{Accounts.User, ActivityLog, Ammo, Containers, Containers.Container}
alias CanneryWeb.Endpoint
alias Ecto.Changeset
alias Phoenix.LiveView.Socket
@ -59,10 +58,7 @@ defmodule CanneryWeb.ContainerLive.Show do
|> case do
{:ok, %{name: container_name}} ->
prompt = dgettext("prompts", "%{name} has been deleted", name: container_name)
socket
|> put_flash(:info, prompt)
|> push_navigate(to: Routes.container_index_path(socket, :index))
socket |> put_flash(:info, prompt) |> push_navigate(to: ~p"/containers")
{:error, %{action: :delete, errors: [packs: _error], valid?: false} = changeset} ->
packs_error = changeset |> changeset_errors(:packs) |> Enum.join(", ")
@ -108,8 +104,10 @@ defmodule CanneryWeb.ContainerLive.Show do
id,
current_user
) do
%{name: container_name} = container = Containers.get_container!(id, current_user)
packs = Ammo.list_packs_for_container(container, class, current_user)
%{id: container_id, name: container_name} =
container = Containers.get_container!(id, current_user)
packs = Ammo.list_packs(current_user, container_id: container_id, class: class)
original_counts = packs |> Ammo.get_original_counts(current_user)
cprs = packs |> Ammo.get_cprs(current_user)
last_used_dates = packs |> ActivityLog.get_last_used_dates(current_user)
@ -124,8 +122,8 @@ defmodule CanneryWeb.ContainerLive.Show do
socket
|> assign(
container: container,
round_count: Ammo.get_round_count_for_container!(container, current_user),
packs_count: Ammo.get_packs_count_for_container!(container, current_user),
round_count: Ammo.get_round_count(current_user, container_id: container.id),
packs_count: Ammo.get_packs_count(current_user, container_id: container.id),
packs: packs,
original_counts: original_counts,
cprs: cprs,

View File

@ -30,7 +30,7 @@
<div class="flex space-x-4 justify-center items-center text-primary-600">
<.link
patch={Routes.container_show_path(Endpoint, :edit, @container)}
patch={~p"/container/edit/#{@container}"}
class="text-primary-600 link"
aria-label={dgettext("actions", "Edit %{container_name}", container_name: @container.name)}
>
@ -61,10 +61,7 @@
<%= display_emoji("😔") %>
</h2>
<.link
patch={Routes.container_show_path(Endpoint, :edit_tags, @container)}
class="btn btn-primary"
>
<.link patch={~p"/container/edit_tags/#{@container}"} class="btn btn-primary">
<%= dgettext("actions", "Why not add one?") %>
</.link>
</div>
@ -73,10 +70,7 @@
<.simple_tag_card :for={tag <- @container.tags} tag={tag} />
<div class="mx-4 my-2">
<.link
patch={Routes.container_show_path(Endpoint, :edit_tags, @container)}
class="text-primary-600 link"
>
<.link patch={~p"/container/edit_tags/#{@container}"} class="text-primary-600 link">
<i class="fa-fw fa-lg fas fa-tags"></i>
</.link>
</div>
@ -126,16 +120,31 @@
<%= if @view_table do %>
<.live_component
module={CanneryWeb.Components.PackTableComponent}
id="type-show-table"
id="pack-show-table"
packs={@packs}
current_user={@current_user}
show_used={false}
>
<:type :let={%{name: type_name} = type}>
<.link navigate={Routes.type_show_path(Endpoint, :show, type)} class="link">
<.link navigate={~p"/type/#{type}"} class="link">
<%= type_name %>
</.link>
</:type>
<:actions :let={%{count: pack_count} = pack}>
<div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
<.link
navigate={~p"/ammo/show/#{pack}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "View pack of %{pack_count} bullets",
pack_count: pack_count
)
}
>
<i class="fa-fw fa-lg fas fa-eye"></i>
</.link>
</div>
</:actions>
</.live_component>
<% else %>
<div class="flex flex-wrap justify-center items-stretch">
@ -155,27 +164,27 @@
<%= case @live_action do %>
<% :edit -> %>
<.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
<.modal return_to={~p"/container/#{@container}"}>
<.live_component
module={CanneryWeb.ContainerLive.FormComponent}
id={@container.id}
title={@page_title}
action={@live_action}
container={@container}
return_to={Routes.container_show_path(Endpoint, :show, @container)}
return_to={~p"/container/#{@container}"}
current_user={@current_user}
/>
</.modal>
<% :edit_tags -> %>
<.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
<.modal return_to={~p"/container/#{@container}"}>
<.live_component
module={CanneryWeb.ContainerLive.EditTagsComponent}
id={@container.id}
title={@page_title}
action={@live_action}
container={@container}
return_to={Routes.container_show_path(Endpoint, :show, @container)}
current_path={Routes.container_show_path(Endpoint, :edit_tags, @container)}
return_to={~p"/container/#{@container}"}
current_path={~p"/container/edit_tags/#{@container}"}
current_user={@current_user}
/>
</.modal>

View File

@ -5,7 +5,6 @@ defmodule CanneryWeb.HomeLive do
use CanneryWeb, :live_view
alias Cannery.Accounts
alias CanneryWeb.Endpoint
@version Mix.Project.config()[:version]

View File

@ -1,6 +1,6 @@
<div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl">
<img
src={Routes.static_path(Endpoint, "/images/cannery.svg")}
src={~p"/images/cannery.svg"}
alt={gettext("Cannery logo")}
class="inline-block w-32 hover:-mt-2 hover:mb-2 transition-all duration-500 ease-in-out"
title={gettext("isn't he cute >:3")}
@ -59,7 +59,7 @@
</b>
<p>
<%= if @admins |> Enum.empty?() do %>
<.link href={Routes.user_registration_path(Endpoint, :new)} class="hover:underline">
<.link href={~p"/users/register"} class="hover:underline">
<%= dgettext("prompts", "Register to setup Cannery") %>
</.link>
<% else %>

View File

@ -9,11 +9,11 @@
<%= display_emoji("😔") %>
</h1>
<.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/invites/new"} class="btn btn-primary">
<%= dgettext("actions", "Invite someone new!") %>
</.link>
<% else %>
<.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/invites/new"} class="btn btn-primary">
<%= dgettext("actions", "Create Invite") %>
</.link>
<% end %>
@ -40,7 +40,7 @@
</form>
</:code_actions>
<.link
patch={Routes.invite_index_path(Endpoint, :edit, invite)}
patch={~p"/invites/edit/#{invite}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit invite for %{invite_name}", invite_name: invite.name)
@ -149,14 +149,14 @@
<% end %>
</div>
<.modal :if={@live_action in [:new, :edit]} return_to={Routes.invite_index_path(Endpoint, :index)}>
<.modal :if={@live_action in [:new, :edit]} return_to={~p"/invites"}>
<.live_component
module={CanneryWeb.InviteLive.FormComponent}
id={@invite.id || :new}
title={@page_title}
action={@live_action}
invite={@invite}
return_to={Routes.invite_index_path(Endpoint, :index)}
return_to={~p"/invites"}
current_user={@current_user}
/>
</.modal>

View File

@ -26,7 +26,7 @@ defmodule CanneryWeb.PackLive.FormComponent do
socket =
socket
|> assign(:pack_create_limit, @pack_create_limit)
|> assign(:types, Ammo.list_types(current_user, :all))
|> assign(:types, Ammo.list_types(current_user))
|> assign_new(:containers, fn -> Containers.list_containers(current_user) end)
params =

View File

@ -39,6 +39,13 @@
) %>
<%= error_tag(f, :price_paid, "col-span-3 text-center") %>
<%= label(f, :lot_number, gettext("Lot number"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :lot_number,
class: "text-center col-span-2 input input-primary",
maxlength: 255
) %>
<%= error_tag(f, :price_paid, "col-span-3 text-center") %>
<%= label(f, :purchased_on, gettext("Purchased on"), class: "title text-lg text-primary-600") %>
<%= date_input(f, :purchased_on,
class: "input input-primary col-span-2",
@ -51,7 +58,6 @@
<%= textarea(f, :notes,
id: "pack-form-notes",
class: "text-center col-span-2 input input-primary",
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :notes, "col-span-3 text-center") %>

View File

@ -113,13 +113,11 @@ defmodule CanneryWeb.PackLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.pack_index_path(Endpoint, :index))}
{:noreply, socket |> push_patch(to: ~p"/ammo")}
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
socket = socket |> push_patch(to: Routes.pack_index_path(Endpoint, :search, search_term))
{:noreply, socket}
{:noreply, socket |> push_patch(to: ~p"/ammo/search/#{search_term}")}
end
def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do
@ -150,8 +148,8 @@ defmodule CanneryWeb.PackLive.Index do
) do
# get total number of packs to determine whether to display onboarding
# prompts
packs_count = Ammo.get_packs_count!(current_user, true)
packs = Ammo.list_packs(search, class, current_user, show_used)
packs_count = Ammo.get_packs_count(current_user, show_used: true)
packs = Ammo.list_packs(current_user, search: search, class: class, show_used: show_used)
types_count = Ammo.get_types_count!(current_user)
containers_count = Containers.get_containers_count!(current_user)

View File

@ -10,7 +10,7 @@
<%= dgettext("prompts", "You'll need to") %>
</h2>
<.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
<.link navigate={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "add a container first") %>
</.link>
</div>
@ -20,7 +20,7 @@
<%= dgettext("prompts", "You'll need to") %>
</h2>
<.link navigate={Routes.type_index_path(Endpoint, :new)} class="btn btn-primary">
<.link navigate={~p"/catalog/new"} class="btn btn-primary">
<%= dgettext("actions", "add a type first") %>
</.link>
</div>
@ -30,11 +30,11 @@
<%= display_emoji("😔") %>
</h2>
<.link patch={Routes.pack_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/ammo/new"} class="btn btn-primary">
<%= dgettext("actions", "Add your first box!") %>
</.link>
<% true -> %>
<.link patch={Routes.pack_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/ammo/new"} class="btn btn-primary">
<%= dgettext("actions", "Add Ammo") %>
</.link>
@ -103,7 +103,7 @@
show_used={@show_used}
>
<:type :let={%{name: type_name} = type}>
<.link navigate={Routes.type_show_path(Endpoint, :show, type)} class="link">
<.link navigate={~p"/type/#{type}"} class="link">
<%= type_name %>
</.link>
</:type>
@ -121,7 +121,7 @@
</button>
<.link
patch={Routes.pack_index_path(Endpoint, :add_shot_record, pack)}
patch={~p"/ammo/add_shot_record/#{pack}"}
class="mx-2 my-1 text-sm btn btn-primary"
>
<%= dgettext("actions", "Record shots") %>
@ -130,17 +130,11 @@
</:range>
<:container :let={{pack, %{name: container_name} = container}}>
<div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center">
<.link
navigate={Routes.container_show_path(Endpoint, :show, container)}
class="mx-2 my-1 link"
>
<.link navigate={~p"/container/#{container}"} class="mx-2 my-1 link">
<%= container_name %>
</.link>
<.link
patch={Routes.pack_index_path(Endpoint, :move, pack)}
class="mx-2 my-1 text-sm btn btn-primary"
>
<.link patch={~p"/ammo/move/#{pack}"} class="mx-2 my-1 text-sm btn btn-primary">
<%= dgettext("actions", "Move ammo") %>
</.link>
</div>
@ -148,27 +142,31 @@
<:actions :let={%{count: pack_count} = pack}>
<div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
<.link
navigate={Routes.pack_show_path(Endpoint, :show, pack)}
navigate={~p"/ammo/show/#{pack}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "View pack of %{pack_count} bullets", pack_count: pack_count)
dgettext("actions", "View pack of %{pack_count} bullets",
pack_count: pack_count
)
}
>
<i class="fa-fw fa-lg fas fa-eye"></i>
</.link>
<.link
patch={Routes.pack_index_path(Endpoint, :edit, pack)}
patch={~p"/ammo/edit/#{pack}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit pack of %{pack_count} bullets", pack_count: pack_count)
dgettext("actions", "Edit pack of %{pack_count} bullets",
pack_count: pack_count
)
}
>
<i class="fa-fw fa-lg fas fa-edit"></i>
</.link>
<.link
patch={Routes.pack_index_path(Endpoint, :clone, pack)}
patch={~p"/ammo/clone/#{pack}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Clone pack of %{pack_count} bullets",
@ -202,38 +200,38 @@
<%= case @live_action do %>
<% create when create in [:new, :edit, :clone] -> %>
<.modal return_to={Routes.pack_index_path(Endpoint, :index)}>
<.modal return_to={~p"/ammo"}>
<.live_component
module={CanneryWeb.PackLive.FormComponent}
id={@pack.id || :new}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_index_path(Endpoint, :index)}
return_to={~p"/ammo"}
current_user={@current_user}
/>
</.modal>
<% :add_shot_record -> %>
<.modal return_to={Routes.pack_index_path(Endpoint, :index)}>
<.modal return_to={~p"/ammo"}>
<.live_component
module={CanneryWeb.Components.AddShotRecordComponent}
id={:new}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_index_path(Endpoint, :index)}
return_to={~p"/ammo"}
current_user={@current_user}
/>
</.modal>
<% :move -> %>
<.modal return_to={Routes.pack_index_path(Endpoint, :index)}>
<.modal return_to={~p"/ammo"}>
<.live_component
module={CanneryWeb.Components.MovePackComponent}
id={@pack.id}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_index_path(Endpoint, :index)}
return_to={~p"/ammo"}
current_user={@current_user}
/>
</.modal>

View File

@ -7,7 +7,6 @@ defmodule CanneryWeb.PackLive.Show do
alias Cannery.{ActivityLog, ActivityLog.ShotRecord}
alias Cannery.{Ammo, Ammo.Pack}
alias Cannery.{ComparableDate, Containers}
alias CanneryWeb.Endpoint
alias Phoenix.LiveView.Socket
@impl true
@ -53,7 +52,7 @@ defmodule CanneryWeb.PackLive.Show do
pack |> Ammo.delete_pack!(current_user)
prompt = dgettext("prompts", "Ammo deleted succesfully")
redirect_to = Routes.pack_index_path(socket, :index)
redirect_to = ~p"/ammo"
{:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
end
@ -93,7 +92,7 @@ defmodule CanneryWeb.PackLive.Show do
%{label: gettext("Actions"), key: :actions, sortable: false}
]
shot_records = ActivityLog.list_shot_records_for_pack(pack, current_user)
shot_records = ActivityLog.list_shot_records(current_user, pack_id: pack.id)
rows =
shot_records
@ -139,7 +138,7 @@ defmodule CanneryWeb.PackLive.Show do
~H"""
<div class="px-4 py-2 space-x-4 flex justify-center items-center">
<.link
patch={Routes.pack_show_path(Endpoint, :edit_shot_record, @pack, @shot_record)}
patch={~p"/ammo/show/#{@pack}/edit/#{@shot_record}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit shot record of %{shot_record_count} shots",

View File

@ -48,15 +48,12 @@
<div class="flex flex-col justify-center items-center">
<div class="flex flex-wrap justify-center items-center text-primary-600">
<.link
navigate={Routes.type_show_path(Endpoint, :show, @pack.type)}
class="mx-4 my-2 btn btn-primary"
>
<.link navigate={~p"/type/#{@pack.type}"} class="mx-4 my-2 btn btn-primary">
<%= dgettext("actions", "View in Catalog") %>
</.link>
<.link
patch={Routes.pack_show_path(Endpoint, :edit, @pack)}
patch={~p"/ammo/show/edit/#{@pack}"}
class="mx-4 my-2 text-primary-600 link"
aria-label={
dgettext("actions", "Edit pack of %{pack_count} bullets", pack_count: @pack.count)
@ -85,14 +82,11 @@
else: dgettext("actions", "Stage for range") %>
</button>
<.link patch={Routes.pack_show_path(Endpoint, :move, @pack)} class="btn btn-primary">
<.link patch={~p"/ammo/show/move/#{@pack}"} class="btn btn-primary">
<%= dgettext("actions", "Move ammo") %>
</.link>
<.link
patch={Routes.pack_show_path(Endpoint, :add_shot_record, @pack)}
class="mx-4 my-2 btn btn-primary"
>
<.link patch={~p"/ammo/show/add_shot_record/#{@pack}"} class="mx-4 my-2 btn btn-primary">
<%= dgettext("actions", "Record shots") %>
</.link>
</div>
@ -121,7 +115,7 @@
<.live_component
module={CanneryWeb.Components.TableComponent}
id="pack_shot_records_table"
id="pack-shot-records-table"
columns={@columns}
rows={@rows}
/>
@ -130,50 +124,50 @@
<%= case @live_action do %>
<% :edit -> %>
<.modal return_to={Routes.pack_show_path(Endpoint, :show, @pack)}>
<.modal return_to={~p"/ammo/show/#{@pack}"}>
<.live_component
module={CanneryWeb.PackLive.FormComponent}
id={@pack.id}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_show_path(Endpoint, :show, @pack)}
return_to={~p"/ammo/show/#{@pack}"}
current_user={@current_user}
/>
</.modal>
<% :edit_shot_record -> %>
<.modal return_to={Routes.pack_show_path(Endpoint, :show, @pack)}>
<.modal return_to={~p"/ammo/show/#{@pack}"}>
<.live_component
module={CanneryWeb.RangeLive.FormComponent}
id={@shot_record.id}
title={@page_title}
action={@live_action}
shot_record={@shot_record}
return_to={Routes.pack_show_path(Endpoint, :show, @pack)}
return_to={~p"/ammo/show/#{@pack}"}
current_user={@current_user}
/>
</.modal>
<% :add_shot_record -> %>
<.modal return_to={Routes.pack_show_path(Endpoint, :show, @pack)}>
<.modal return_to={~p"/ammo/show/#{@pack}"}>
<.live_component
module={CanneryWeb.Components.AddShotRecordComponent}
id={:new}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_show_path(Endpoint, :show, @pack)}
return_to={~p"/ammo/show/#{@pack}"}
current_user={@current_user}
/>
</.modal>
<% :move -> %>
<.modal return_to={Routes.pack_show_path(Endpoint, :show, @pack)}>
<.modal return_to={~p"/ammo/show/#{@pack}"}>
<.live_component
module={CanneryWeb.Components.MovePackComponent}
id={@pack.id}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.pack_show_path(Endpoint, :show, @pack)}
return_to={~p"/ammo/show/#{@pack}"}
current_user={@current_user}
/>
</.modal>

View File

@ -33,7 +33,6 @@
class: "input input-primary col-span-2",
maxlength: 255,
placeholder: gettext("Really great weather"),
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :notes, "col-span-3") %>

View File

@ -5,7 +5,6 @@ defmodule CanneryWeb.RangeLive.Index do
use CanneryWeb, :live_view
alias Cannery.{ActivityLog, ActivityLog.ShotRecord, Ammo}
alias CanneryWeb.Endpoint
alias Phoenix.LiveView.Socket
@impl true
@ -45,7 +44,7 @@ defmodule CanneryWeb.RangeLive.Index do
defp apply_action(socket, :new, _params) do
socket
|> assign(
page_title: gettext("New Shot Records"),
page_title: gettext("Record Shots"),
shot_record: %ShotRecord{}
)
end
@ -53,7 +52,7 @@ defmodule CanneryWeb.RangeLive.Index do
defp apply_action(socket, :index, _params) do
socket
|> assign(
page_title: gettext("Shot Records"),
page_title: gettext("Range"),
search: nil,
shot_record: nil
)
@ -63,7 +62,7 @@ defmodule CanneryWeb.RangeLive.Index do
defp apply_action(socket, :search, %{"search" => search}) do
socket
|> assign(
page_title: gettext("Shot Records"),
page_title: gettext("Range"),
search: search,
shot_record: nil
)
@ -94,11 +93,11 @@ defmodule CanneryWeb.RangeLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :index))}
{:noreply, socket |> push_patch(to: ~p"/range")}
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
{:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :search, search_term))}
{:noreply, socket |> push_patch(to: ~p"/range/search/#{search_term}")}
end
def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do
@ -121,8 +120,8 @@ defmodule CanneryWeb.RangeLive.Index do
defp display_shot_records(
%{assigns: %{class: class, search: search, current_user: current_user}} = socket
) do
shot_records = ActivityLog.list_shot_records(search, class, current_user)
packs = Ammo.list_staged_packs(current_user)
shot_records = ActivityLog.list_shot_records(current_user, search: search, class: class)
packs = Ammo.list_packs(current_user, staged: true)
chart_data = shot_records |> get_chart_data_for_shot_record()
original_counts = packs |> Ammo.get_original_counts(current_user)
cprs = packs |> Ammo.get_cprs(current_user)

View File

@ -9,11 +9,11 @@
<%= display_emoji("😔") %>
</h1>
<.link navigate={Routes.pack_index_path(Endpoint, :index)} class="btn btn-primary">
<.link navigate={~p"/ammo"} class="btn btn-primary">
<%= dgettext("actions", "Why not get some ready to shoot?") %>
</.link>
<% else %>
<.link navigate={Routes.pack_index_path(Endpoint, :index)} class="btn btn-primary">
<.link navigate={~p"/ammo"} class="btn btn-primary">
<%= dgettext("actions", "Stage ammo") %>
</.link>
@ -38,10 +38,7 @@
else: dgettext("actions", "Stage for range") %>
</button>
<.link
patch={Routes.range_index_path(Endpoint, :add_shot_record, pack)}
class="btn btn-primary"
>
<.link patch={~p"/range/add_shot_record/#{pack}"} class="btn btn-primary">
<%= dgettext("actions", "Record shots") %>
</.link>
</.pack_card>
@ -83,7 +80,9 @@
phx-submit="change_class"
class="flex items-center"
>
<%= label(f, :class, gettext("Class"), class: "title text-primary-600 text-lg text-center") %>
<%= label(f, :class, gettext("Class"),
class: "title text-primary-600 text-lg text-center"
) %>
<%= select(
f,
@ -125,14 +124,14 @@
<% else %>
<.live_component
module={CanneryWeb.Components.ShotRecordTableComponent}
id="shot_records_index_table"
id="shot-records-index-table"
shot_records={@shot_records}
current_user={@current_user}
>
<:actions :let={shot_record}>
<div class="px-4 py-2 space-x-4 flex justify-center items-center">
<.link
patch={Routes.range_index_path(Endpoint, :edit, shot_record)}
patch={~p"/range/edit/#{shot_record}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "Edit shot record of %{shot_record_count} shots",
@ -168,26 +167,26 @@
<%= case @live_action do %>
<% :edit -> %>
<.modal return_to={Routes.range_index_path(Endpoint, :index)}>
<.modal return_to={~p"/range"}>
<.live_component
module={CanneryWeb.RangeLive.FormComponent}
id={@shot_record.id}
title={@page_title}
action={@live_action}
shot_record={@shot_record}
return_to={Routes.range_index_path(Endpoint, :index)}
return_to={~p"/range"}
current_user={@current_user}
/>
</.modal>
<% :add_shot_record -> %>
<.modal return_to={Routes.range_index_path(Endpoint, :index)}>
<.modal return_to={~p"/range"}>
<.live_component
module={CanneryWeb.Components.AddShotRecordComponent}
id={:new}
title={@page_title}
action={@live_action}
pack={@pack}
return_to={Routes.range_index_path(Endpoint, :index)}
return_to={~p"/range"}
current_user={@current_user}
/>
</.modal>

View File

@ -5,7 +5,7 @@ defmodule CanneryWeb.TagLive.Index do
use CanneryWeb, :live_view
alias Cannery.{Containers, Containers.Tag}
alias CanneryWeb.ViewHelpers
alias CanneryWeb.HTMLHelpers
@impl true
def mount(%{"search" => search}, _session, socket) do
@ -33,7 +33,7 @@ defmodule CanneryWeb.TagLive.Index do
socket
|> assign(
page_title: gettext("New Tag"),
tag: %Tag{bg_color: ViewHelpers.random_color(), text_color: "#ffffff"}
tag: %Tag{bg_color: HTMLHelpers.random_color(), text_color: "#ffffff"}
)
end
@ -67,14 +67,14 @@ defmodule CanneryWeb.TagLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.tag_index_path(Endpoint, :index))}
{:noreply, socket |> push_patch(to: ~p"/tags")}
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
{:noreply, socket |> push_patch(to: Routes.tag_index_path(Endpoint, :search, search_term))}
{:noreply, socket |> push_patch(to: ~p"/tags/search/#{search_term}")}
end
defp display_tags(%{assigns: %{search: search, current_user: current_user}} = socket) do
socket |> assign(tags: Containers.list_tags(search, current_user))
socket |> assign(tags: Containers.list_tags(current_user, search: search))
end
end

View File

@ -11,11 +11,11 @@
<%= display_emoji("😔") %>
</h2>
<.link patch={Routes.tag_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/tags/new"} class="btn btn-primary">
<%= dgettext("actions", "Make your first tag!") %>
</.link>
<% else %>
<.link patch={Routes.tag_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/tags/new"} class="btn btn-primary">
<%= dgettext("actions", "New Tag") %>
</.link>
@ -47,7 +47,7 @@
<div class="flex flex-row flex-wrap justify-center items-stretch">
<.tag_card :for={tag <- @tags} tag={tag}>
<.link
patch={Routes.tag_index_path(Endpoint, :edit, tag)}
patch={~p"/tags/edit/#{tag}"}
class="text-primary-600 link"
aria-label={dgettext("actions", "Edit %{tag_name}", tag_name: tag.name)}
>
@ -72,14 +72,14 @@
<% end %>
</div>
<.modal :if={@live_action in [:new, :edit]} return_to={Routes.tag_index_path(Endpoint, :index)}>
<.modal :if={@live_action in [:new, :edit]} return_to={~p"/tags"}>
<.live_component
module={CanneryWeb.TagLive.FormComponent}
id={@tag.id || :new}
title={@page_title}
action={@live_action}
tag={@tag}
return_to={Routes.tag_index_path(Endpoint, :index)}
return_to={~p"/tags"}
current_user={@current_user}
/>
</.modal>

View File

@ -39,7 +39,6 @@
<%= textarea(f, :desc,
id: "type-form-desc",
class: "text-center col-span-2 input input-primary",
phx_hook: "MaintainAttrs",
phx_update: "ignore"
) %>
<%= error_tag(f, :desc, "col-span-3 text-center") %>
@ -86,14 +85,18 @@
) %>
<%= error_tag(f, :unfired_length, "col-span-3 text-center") %>
<%= label(f, :brass_height, gettext("Brass height"), class: "title text-lg text-primary-600") %>
<%= label(f, :brass_height, gettext("Brass height"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :brass_height,
class: "text-center col-span-2 input input-primary",
maxlength: 255
) %>
<%= error_tag(f, :brass_height, "col-span-3 text-center") %>
<%= label(f, :chamber_size, gettext("Chamber size"), class: "title text-lg text-primary-600") %>
<%= label(f, :chamber_size, gettext("Chamber size"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :chamber_size,
class: "text-center col-span-2 input input-primary",
maxlength: 255
@ -117,24 +120,28 @@
) %>
<%= error_tag(f, :grains, "col-span-3 text-center") %>
<%= label f, :bullet_type, class: "flex title text-lg text-primary-600 space-x-2" do %>
<p><%= gettext("Bullet type") %></p>
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label f, :bullet_type, class: "flex title text-lg text-primary-600 space-x-2" do %>
<p><%= gettext("Bullet type") %></p>
<.link
href="https://shootersreference.com/reloadingdata/bullet_abbreviations/"
class="link"
target="_blank"
rel="noopener noreferrer"
>
<i class="fas fa-md fa-external-link-alt"></i>
</.link>
<.link
href="https://shootersreference.com/reloadingdata/bullet_abbreviations/"
class="link"
target="_blank"
rel="noopener noreferrer"
>
<i class="fas fa-md fa-external-link-alt"></i>
</.link>
<% end %>
<%= text_input(f, :bullet_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
placeholder: gettext("FMJ")
) %>
<%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
<% else %>
<%= hidden_input(f, :bullet_type, value: nil) %>
<% end %>
<%= text_input(f, :bullet_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
placeholder: gettext("FMJ")
) %>
<%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
<%= label(
f,
@ -164,7 +171,9 @@
<%= hidden_input(f, :jacket_type, value: nil) %>
<% end %>
<%= label(f, :case_material, gettext("Case material"), class: "title text-lg text-primary-600") %>
<%= label(f, :case_material, gettext("Case material"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :case_material,
class: "text-center col-span-2 input input-primary",
maxlength: 255,

View File

@ -78,12 +78,11 @@ defmodule CanneryWeb.TypeLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.type_index_path(Endpoint, :index))}
{:noreply, socket |> push_patch(to: ~p"/catalog")}
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
search_path = Routes.type_index_path(Endpoint, :search, search_term)
{:noreply, socket |> push_patch(to: search_path)}
{:noreply, socket |> push_patch(to: ~p"/catalog/search/#{search_term}")}
end
def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do
@ -107,7 +106,7 @@ defmodule CanneryWeb.TypeLive.Index do
) do
socket
|> assign(
types: Ammo.list_types(search, current_user, class),
types: Ammo.list_types(current_user, class: class, search: search),
types_count: Ammo.get_types_count!(current_user)
)
end

View File

@ -9,11 +9,11 @@
<%= display_emoji("😔") %>
</h2>
<.link patch={Routes.type_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/catalog/new"} class="btn btn-primary">
<%= dgettext("actions", "Add your first type!") %>
</.link>
<% else %>
<.link patch={Routes.type_index_path(Endpoint, :new)} class="btn btn-primary">
<.link patch={~p"/catalog/new"} class="btn btn-primary">
<%= dgettext("actions", "New Type") %>
</.link>
@ -26,7 +26,9 @@
phx-submit="change_class"
class="flex items-center"
>
<%= label(f, :class, gettext("Class"), class: "title text-primary-600 text-lg text-center") %>
<%= label(f, :class, gettext("Class"),
class: "title text-primary-600 text-lg text-center"
) %>
<%= select(
f,
@ -74,7 +76,7 @@
<% else %>
<.live_component
module={CanneryWeb.Components.TypeTableComponent}
id="types_index_table"
id="types-index-table"
action={@live_action}
types={@types}
current_user={@current_user}
@ -84,7 +86,7 @@
<:actions :let={type}>
<div class="px-4 py-2 space-x-4 flex justify-center items-center">
<.link
navigate={Routes.type_show_path(Endpoint, :show, type)}
navigate={~p"/type/#{type}"}
class="text-primary-600 link"
aria-label={dgettext("actions", "View %{type_name}", type_name: type.name)}
>
@ -92,7 +94,7 @@
</.link>
<.link
patch={Routes.type_index_path(Endpoint, :edit, type)}
patch={~p"/catalog/edit/#{type}"}
class="text-primary-600 link"
aria-label={dgettext("actions", "Edit %{type_name}", type_name: type.name)}
>
@ -100,7 +102,7 @@
</.link>
<.link
patch={Routes.type_index_path(Endpoint, :clone, type)}
patch={~p"/catalog/clone/#{type}"}
class="text-primary-600 link"
aria-label={dgettext("actions", "Clone %{type_name}", type_name: type.name)}
>
@ -130,17 +132,14 @@
<% end %>
</div>
<.modal
:if={@live_action in [:new, :edit, :clone]}
return_to={Routes.type_index_path(Endpoint, :index)}
>
<.modal :if={@live_action in [:new, :edit, :clone]} return_to={~p"/catalog"}>
<.live_component
module={CanneryWeb.TypeLive.FormComponent}
id={@type.id || :new}
title={@page_title}
action={@live_action}
type={@type}
return_to={Routes.type_index_path(Endpoint, :index)}
return_to={~p"/catalog"}
current_user={@current_user}
}
/>

View File

@ -5,7 +5,6 @@ defmodule CanneryWeb.TypeLive.Show do
use CanneryWeb, :live_view
alias Cannery.{ActivityLog, Ammo, Ammo.Type, Containers}
alias CanneryWeb.Endpoint
@impl true
def mount(_params, _session, socket),
@ -25,7 +24,7 @@ defmodule CanneryWeb.TypeLive.Show do
%{name: type_name} = type |> Ammo.delete_type!(current_user)
prompt = dgettext("prompts", "%{name} deleted succesfully", name: type_name)
redirect_to = Routes.type_index_path(socket, :index)
redirect_to = ~p"/catalog"
{:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
end
@ -41,7 +40,7 @@ defmodule CanneryWeb.TypeLive.Show do
defp display_type(
%{assigns: %{live_action: live_action, current_user: current_user, show_used: show_used}} =
socket,
%Type{name: type_name} = type
%Type{id: type_id, name: type_name} = type
) do
custom_fields? =
fields_to_display(type)
@ -55,7 +54,7 @@ defmodule CanneryWeb.TypeLive.Show do
type |> Map.get(field) != default_value
end)
packs = type |> Ammo.list_packs_for_type(current_user, show_used)
packs = Ammo.list_packs(current_user, type_id: type_id, show_used: show_used)
[
original_counts,
@ -67,10 +66,10 @@ defmodule CanneryWeb.TypeLive.Show do
if show_used do
[
packs |> Ammo.get_original_counts(current_user),
type |> Ammo.get_used_packs_count_for_type(current_user),
type |> Ammo.get_packs_count_for_type(current_user, true),
type |> ActivityLog.get_used_count_for_type(current_user),
type |> Ammo.get_historical_count_for_type(current_user)
Ammo.get_packs_count(current_user, type_id: type.id, show_used: :only_used),
Ammo.get_packs_count(current_user, type_id: type.id, show_used: true),
ActivityLog.get_used_count(current_user, type_id: type.id),
Ammo.get_historical_count(type, current_user)
]
else
[nil, nil, nil, nil, nil]
@ -95,12 +94,12 @@ defmodule CanneryWeb.TypeLive.Show do
containers: containers,
cprs: packs |> Ammo.get_cprs(current_user),
last_used_dates: packs |> ActivityLog.get_last_used_dates(current_user),
avg_cost_per_round: type |> Ammo.get_average_cost_for_type(current_user),
rounds: type |> Ammo.get_round_count_for_type(current_user),
avg_cost_per_round: Ammo.get_average_cost(type, current_user),
rounds: Ammo.get_round_count(current_user, type_id: type.id),
original_counts: original_counts,
used_rounds: used_rounds,
historical_round_count: historical_round_count,
packs_count: type |> Ammo.get_packs_count_for_type(current_user),
packs_count: Ammo.get_packs_count(current_user, type_id: type.id),
used_packs_count: used_packs_count,
historical_packs_count: historical_packs_count,
fields_to_display: fields_to_display(type),

View File

@ -14,7 +14,7 @@
<div class="flex space-x-4 justify-center items-center text-primary-600">
<.link
patch={Routes.type_show_path(Endpoint, :edit, @type)}
patch={~p"/type/#{@type}/edit"}
class="text-primary-600 link"
aria-label={dgettext("actions", "Edit %{type_name}", type_name: @type.name)}
>
@ -188,20 +188,19 @@
show_used={@show_used}
>
<:container :let={{_pack, %{name: container_name} = container}}>
<.link
navigate={Routes.container_show_path(Endpoint, :show, container)}
class="mx-2 my-1 link"
>
<.link navigate={~p"/container/#{container}"} class="mx-2 my-1 link">
<%= container_name %>
</.link>
</:container>
<:actions :let={%{count: pack_count} = pack}>
<div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
<.link
navigate={Routes.pack_show_path(Endpoint, :show, pack)}
navigate={~p"/ammo/show/#{pack}"}
class="text-primary-600 link"
aria-label={
dgettext("actions", "View pack of %{pack_count} bullets", pack_count: pack_count)
dgettext("actions", "View pack of %{pack_count} bullets",
pack_count: pack_count
)
}
>
<i class="fa-fw fa-lg fas fa-eye"></i>
@ -226,14 +225,14 @@
</div>
</div>
<.modal :if={@live_action == :edit} return_to={Routes.type_show_path(Endpoint, :show, @type)}>
<.modal :if={@live_action == :edit} return_to={~p"/type/#{@type}"}>
<.live_component
module={CanneryWeb.TypeLive.FormComponent}
id={@type.id}
title={@page_title}
action={@live_action}
type={@type}
return_to={Routes.type_show_path(Endpoint, :show, @type)}
return_to={~p"/type/#{@type}"}
current_user={@current_user}
/>
</.modal>

View File

@ -7,25 +7,13 @@ defmodule CanneryWeb.Router do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {CanneryWeb.LayoutView, :root}
plug :put_root_layout, {CanneryWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
plug :put_user_locale
end
defp put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
default = Application.fetch_env!(:gettext, :default_locale)
Gettext.put_locale(locale || default)
conn |> put_session(:locale, locale || default)
end
defp put_user_locale(conn, _opts) do
default = Application.fetch_env!(:gettext, :default_locale)
Gettext.put_locale(default)
conn |> put_session(:locale, default)
end
pipeline :require_admin do
plug :require_role, role: :admin
end
@ -34,14 +22,6 @@ defmodule CanneryWeb.Router do
plug :accepts, ["json"]
end
scope "/", CanneryWeb do
pipe_through :browser
live "/", HomeLive
end
## Authentication routes
scope "/", CanneryWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
@ -64,59 +44,51 @@ defmodule CanneryWeb.Router do
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
get "/export/:mode", ExportController, :export
live "/tags", TagLive.Index, :index
live "/tags/new", TagLive.Index, :new
live "/tags/edit/:id", TagLive.Index, :edit
live "/tags/search/:search", TagLive.Index, :search
live_session :default, on_mount: [{CanneryWeb.UserAuth, :ensure_authenticated}] do
live "/tags", TagLive.Index, :index
live "/tags/new", TagLive.Index, :new
live "/tags/edit/:id", TagLive.Index, :edit
live "/tags/search/:search", TagLive.Index, :search
live "/catalog", TypeLive.Index, :index
live "/catalog/new", TypeLive.Index, :new
live "/catalog/clone/:id", TypeLive.Index, :clone
live "/catalog/edit/:id", TypeLive.Index, :edit
live "/catalog/search/:search", TypeLive.Index, :search
live "/catalog", TypeLive.Index, :index
live "/catalog/new", TypeLive.Index, :new
live "/catalog/clone/:id", TypeLive.Index, :clone
live "/catalog/edit/:id", TypeLive.Index, :edit
live "/catalog/search/:search", TypeLive.Index, :search
live "/type/:id", TypeLive.Show, :show
live "/type/:id/edit", TypeLive.Show, :edit
live "/type/:id", TypeLive.Show, :show
live "/type/:id/edit", TypeLive.Show, :edit
live "/containers", ContainerLive.Index, :index
live "/containers/new", ContainerLive.Index, :new
live "/containers/edit/:id", ContainerLive.Index, :edit
live "/containers/clone/:id", ContainerLive.Index, :clone
live "/containers/edit_tags/:id", ContainerLive.Index, :edit_tags
live "/containers/search/:search", ContainerLive.Index, :search
live "/containers", ContainerLive.Index, :index
live "/containers/new", ContainerLive.Index, :new
live "/containers/edit/:id", ContainerLive.Index, :edit
live "/containers/clone/:id", ContainerLive.Index, :clone
live "/containers/edit_tags/:id", ContainerLive.Index, :edit_tags
live "/containers/search/:search", ContainerLive.Index, :search
live "/container/:id", ContainerLive.Show, :show
live "/container/edit/:id", ContainerLive.Show, :edit
live "/container/edit_tags/:id", ContainerLive.Show, :edit_tags
live "/container/:id", ContainerLive.Show, :show
live "/container/edit/:id", ContainerLive.Show, :edit
live "/container/edit_tags/:id", ContainerLive.Show, :edit_tags
live "/ammo", PackLive.Index, :index
live "/ammo/new", PackLive.Index, :new
live "/ammo/edit/:id", PackLive.Index, :edit
live "/ammo/clone/:id", PackLive.Index, :clone
live "/ammo/add_shot_record/:id", PackLive.Index, :add_shot_record
live "/ammo/move/:id", PackLive.Index, :move
live "/ammo/search/:search", PackLive.Index, :search
live "/ammo", PackLive.Index, :index
live "/ammo/new", PackLive.Index, :new
live "/ammo/edit/:id", PackLive.Index, :edit
live "/ammo/clone/:id", PackLive.Index, :clone
live "/ammo/add_shot_record/:id", PackLive.Index, :add_shot_record
live "/ammo/move/:id", PackLive.Index, :move
live "/ammo/search/:search", PackLive.Index, :search
live "/ammo/show/:id", PackLive.Show, :show
live "/ammo/show/edit/:id", PackLive.Show, :edit
live "/ammo/show/add_shot_record/:id", PackLive.Show, :add_shot_record
live "/ammo/show/move/:id", PackLive.Show, :move
live "/ammo/show/:id/edit/:shot_record_id", PackLive.Show, :edit_shot_record
live "/ammo/show/:id", PackLive.Show, :show
live "/ammo/show/edit/:id", PackLive.Show, :edit
live "/ammo/show/add_shot_record/:id", PackLive.Show, :add_shot_record
live "/ammo/show/move/:id", PackLive.Show, :move
live "/ammo/show/:id/edit/:shot_record_id", PackLive.Show, :edit_shot_record
live "/range", RangeLive.Index, :index
live "/range/edit/:id", RangeLive.Index, :edit
live "/range/add_shot_record/:id", RangeLive.Index, :add_shot_record
live "/range/search/:search", RangeLive.Index, :search
end
scope "/", CanneryWeb do
pipe_through [:browser, :require_authenticated_user, :require_admin]
live_dashboard "/dashboard", metrics: CanneryWeb.Telemetry, ecto_repos: [Cannery.Repo]
live "/invites", InviteLive.Index, :index
live "/invites/new", InviteLive.Index, :new
live "/invites/:id/edit", InviteLive.Index, :edit
live "/range", RangeLive.Index, :index
live "/range/edit/:id", RangeLive.Index, :edit
live "/range/add_shot_record/:id", RangeLive.Index, :add_shot_record
live "/range/search/:search", RangeLive.Index, :search
end
end
scope "/", CanneryWeb do
@ -126,6 +98,22 @@ defmodule CanneryWeb.Router do
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
live_session :public, on_mount: [{CanneryWeb.UserAuth, :mount_current_user}] do
live "/", HomeLive
end
end
scope "/", CanneryWeb do
pipe_through [:browser, :require_authenticated_user, :require_admin]
live_dashboard "/dashboard", metrics: CanneryWeb.Telemetry, ecto_repos: [Cannery.Repo]
live_session :admin, on_mount: [{CanneryWeb.UserAuth, :ensure_admin}] do
live "/invites", InviteLive.Index, :index
live "/invites/new", InviteLive.Index, :new
live "/invites/edit/:id", InviteLive.Index, :edit
end
end
# Enables the Swoosh mailbox preview in development.
@ -137,9 +125,6 @@ defmodule CanneryWeb.Router do
pipe_through :browser
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
scope "/dev" do
get "/preview/:id", CanneryWeb.EmailController, :preview
end
end

View File

@ -1,18 +0,0 @@
<main role="main" class="min-h-full min-w-full">
<header>
<.topbar current_user={assigns[:current_user]} />
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
<p :if={get_flash(@conn, :info)} class="alert alert-info" role="alert">
<%= get_flash(@conn, :info) %>
</p>
<p :if={get_flash(@conn, :error)} class="alert alert-danger" role="alert">
<%= get_flash(@conn, :error) %>
</p>
</div>
</header>
<div class="mx-4 sm:mx-8 md:mx-16">
<%= @inner_content %>
</div>
</main>

Some files were not shown because too many files have changed in this diff Show More