27 Commits

Author SHA1 Message Date
05b1630a96 fix dialyzer for otp 28
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-05 23:25:44 +00:00
1fd3a1bebe bump version
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-05 23:10:12 +00:00
6e9bc94f35 add better reverse proxy instructions
Some checks are pending
continuous-integration/drone/push Build is running
2025-07-05 23:07:21 +00:00
4e1cf81242 add database upgrade instructions 2025-07-05 23:06:47 +00:00
09e59c94f0 update deps 2025-07-05 22:55:21 +00:00
859e5756ae add new language note to contributing guide
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-13 19:09:01 +00:00
45b46b761d update version
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-05 04:20:07 +00:00
bda051ebc8 fix cannery logo on home page
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-05 04:12:37 +00:00
01fa306429 eliminate possible style conflicts
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 04:05:10 +00:00
5cff5d8280 update deps again
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-05 03:04:45 +00:00
71778d12a6 shorten config 2025-04-05 02:54:34 +00:00
9b721a170b fix migrations
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 02:46:59 +00:00
366a6d160d format migration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-05 01:20:35 +00:00
926d2fe6c2 fix cast_datetime
Some checks are pending
continuous-integration/drone/push Build is running
2025-04-05 01:19:20 +00:00
c7bd7238c6 improve accuracy of timestamps
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-05 01:13:00 +00:00
e2c17b6b51 fix drone
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-05 00:33:09 +00:00
20988ac1ec fix dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-05 00:31:35 +00:00
37d101a71e update deps
Some checks failed
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is failing
2025-04-05 00:13:01 +00:00
449a92e4b7 remove npm engine requirement
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-15 02:30:28 +00:00
5d17ee0a11 fix broken install step
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-13 22:02:18 +00:00
b6b6cecc0a update deps
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-02-01 02:12:41 -05:00
08916a504f add button to resend email verification email 2025-02-01 02:12:41 -05:00
3eda522903 move staging to container 2025-02-01 02:12:41 -05:00
2e6e26006d fix style issues 2025-02-01 02:12:41 -05:00
b66d0ea8a1 add date range to range page 2025-02-01 02:12:41 -05:00
839e1d7124 fix dates not displaying properly 2025-01-31 22:33:07 -05:00
76834845a3 update deps 2025-01-12 16:51:27 -05:00
155 changed files with 3120 additions and 12955 deletions

View File

@ -17,7 +17,7 @@ steps:
- .mix
- name: test
image: elixir:1.17.3-otp-27-alpine
image: elixir:1.18.4-otp-28-alpine
environment:
TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
HOST: testing.example.tld
@ -31,8 +31,7 @@ steps:
- mix deps.get
- npm set cache .npm
- npm --prefix ./assets ci --no-audit --prefer-offline
- npm run --prefix ./assets deploy
- mix do phx.digest, gettext.extract
- mix do phx.digest, gettext.extract, assets.deploy
- mix test.all
- name: build and publish stable

9
.gitignore vendored
View File

@ -28,10 +28,11 @@ npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
# Ignore assets that are produced by build tools.
/priv/static/assets/
# Ignore digested assets cache.
/priv/static/cache_manifest.json
# vs code
.elixir_ls/

View File

@ -1,3 +1,3 @@
elixir 1.17.3-otp-27
erlang 27.1.2
nodejs 23.0.0
elixir 1.18.4-otp-28
erlang 28.0.1
nodejs 24.3.0

View File

@ -1,3 +1,19 @@
# v0.9.15
- Update deps
- Add better reverse proxy and database upgrade instructions
# v0.9.14
- Update deps
- Fix wrapping issues with search bars
- Improve accuracy of timestamps
# v0.9.13
- Add button to resend email verification email
- Move staging to container, rather than ammo
- Add date restriction dropdown to range page
- Fix dates not rendering properly in table
- Update deps
# v0.9.12
- Allow filtering ammo types when creating new packs
- Add SlimSelect to select elements with user content

View File

@ -10,6 +10,9 @@ status](https://weblate.bubbletea.dev/widgets/cannery/-/287x66-black.png)](https
If you're multilingual, this project can use your translations! Visit
[weblate](https://weblate.bubbletea.dev/engage/cannery/) for more information.
Also, if your language isn't displayed here, I'd love to add that language so
you can start! Please contact me at
(shibao@bubbletea.dev)[mailto:shibao@bubbletea.dev] and let me know!
## Style Tips
@ -127,7 +130,7 @@ In `test` mode (or in the Docker container), Cannery will listen for the same en
In `prod` mode (or in the Docker container), Cannery will listen for the same environment variables as dev mode, but also include the following at runtime:
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
with `docker run -it shibaobun/cannery mix phx.gen.secret` and set for server to start.
with `docker run -it shibaobun/cannery priv/random.sh` and set for server to start.
- `SMTP_HOST`: The url for your SMTP email provider. Must be set
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!

View File

@ -1,4 +1,4 @@
FROM elixir:1.17.3-otp-27-alpine AS build
FROM elixir:1.18.4-otp-28-alpine AS build
# install build dependencies
RUN apk add --no-cache build-base npm git python3
@ -25,13 +25,12 @@ RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
COPY lib lib
COPY priv priv
COPY assets assets
RUN npm run --prefix ./assets deploy
RUN mix do phx.digest, gettext.extract
# compile and build release
# uncomment COPY if rel/ exists
# COPY rel rel
RUN mix do compile, release
RUN mix do assets.deploy, compile, release
# prepare release image
FROM alpine:latest AS app
@ -43,6 +42,8 @@ WORKDIR /app
RUN chown nobody:nobody /app
ENV MIX_ENV=prod
USER nobody:nobody
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/cannery ./

View File

@ -46,6 +46,16 @@ ports:
```
and reverse proxy to `http://localhost:4000`.
If you don't already have a reverse proxy on the machine, I recommend installing
[Nginx Proxy Manager](https://nginxproxymanager.com/), which is a GUI for Nginx
that makes it easy to configure and modify as your hosting needs change. By
adding NPM to cannery's `docker-compose.yml`, you can avoid needing to bind any
ports to your machine and have all the internal traffic routed through the
generated docker network, which can be a bit more secure. The example
configuration is commented out in the `docker-compose.yml` file, and more
information can be found on their documentation
[here](https://nginxproxymanager.com/setup/).
# Configuration
You can use the following environment variables to configure Cannery in
@ -60,7 +70,7 @@ You can use the following environment variables to configure Cannery in
Defaults to `false`.
- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`.
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
with `docker run -it shibaobun/cannery mix phx.gen.secret` and set for server to start.
with `docker run -it shibaobun/cannery priv/random.sh` and set for server to start.
- `REGISTRATION`: Controls if user sign-up should be invite only or set to
public. Set to `public` to enable public registration. Defaults to `invite`.
- `LOCALE`: Sets a custom default locale. Defaults to `en_US`
@ -74,6 +84,20 @@ You can use the following environment variables to configure Cannery in
`no-reply@HOST` where `HOST` was previously defined.
- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Cannery".
# Upgrading the Database
Typically, PostgreSQL updates can improve the performance of the database, and
the cannery app. However, these require some additional maintenance. While the
typical method is to manually dump and restore the database using the `pg_dump`
tool, I recommend using the
[pgautoupgrade tool](https://github.com/pgautoupgrade/docker-pgautoupgrade),
which can perform this for you automatically. In the `docker-compose.yml` file,
you can do this easily by switching the `image:` value from for example,
`postgres:13` to `pgautoupgrade/pgautoupgrade:17-alpine` and rerun
`docker-compose up -d`. This will automatically migrate your database to
Postgres 17, and then you can switch back to the original `postgres:17` image
for additional performance, or keep using the upgrade image if you'd like.
# Contribution
Contributions are greatly appreciated, no ability to code needed! You can browse

View File

@ -1,5 +0,0 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@ -1,57 +0,0 @@
@layer components {
.input {
@apply rounded-lg px-4 py-2 border focus:outline-none;
@apply shadow-sm focus:shadow-lg;
@apply transition-all duration-300 ease-in-out;
}
.input-primary {
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
@apply text-black;
}
.checkbox {
-ms-transform: scale(1.5);
-moz-transform: scale(1.5);
-webkit-transform: scale(1.5);
-o-transform: scale(1.5);
transform: scale(1.5);
padding: 10px;
margin: 1em auto;
}
.title {
@apply leading-5 tracking-wide;
}
.btn {
@apply focus:outline-none px-4 py-2 rounded-lg;
@apply shadow-sm focus:shadow-lg;
@apply transition-all duration-300 ease-in-out;
}
.btn-primary {
@apply bg-primary-500 focus:bg-primary-600 active:bg-primary-600;
@apply border-primary-500 focus:border-primary-600 active:border-primary-600;
@apply text-white;
}
.btn-alert {
@apply bg-red-600 focus:bg-red-700 active:bg-red-800;
@apply border-red-600 focus:border-red-700 active:border-red-800;
@apply text-white;
}
.hr {
@apply border border-primary-300 w-full max-w-2xl;
}
.hr-light {
@apply border border-white w-full max-w-2xl;
}
.link {
@apply hover:underline;
@apply transition-colors duration-500 ease-in-out;
}
}

View File

@ -1,16 +1,26 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "tailwindcss" source("../..");
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/regular";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/brands";
@theme {
--color-primary-50: oklch(0.985 0.002 247.839);
--color-primary-100: oklch(0.967 0.003 264.542);
--color-primary-200: oklch(0.928 0.006 264.531);
--color-primary-300: oklch(0.872 0.01 258.338);
--color-primary-400: oklch(0.707 0.022 261.325);
--color-primary-500: oklch(0.551 0.027 264.364);
--color-primary-600: oklch(0.446 0.03 256.802);
--color-primary-700: oklch(0.373 0.034 259.733);
--color-primary-800: oklch(0.278 0.033 256.848);
--color-primary-900: oklch(0.21 0.034 264.665);
--color-primary-950: oklch(0.13 0.028 261.692);
@import "slim-select/styles";
}
@import "components";
@import "@fortawesome/fontawesome-free/css/fontawesome" source("../..");
@import "@fortawesome/fontawesome-free/css/regular" source("../..");
@import "@fortawesome/fontawesome-free/css/solid" source("../..");
@import "@fortawesome/fontawesome-free/css/brands" source("../..");
@import "slim-select/styles" source("../..");
/* fix firefox scrollbars */
* {
@ -27,7 +37,7 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
100% { scale: 1.0; opacity: 1; }
}
// disconnect toast
/* disconnect toast */
.phx-connected > #disconnect {
opacity: 0 !important;
pointer-events: none;
@ -155,18 +165,68 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
100% { opacity: 0; }
}
.ss-main {
@apply input;
/* components */
.input, .ss-main, .ss-content, .ss-search input[type="search"] {
@apply px-4 py-2 rounded-lg border focus:outline-hidden;
@apply shadow-sm focus:shadow-lg;
@apply transition-all duration-300 ease-in-out;
}
.input-primary {
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
@apply text-black;
}
.checkbox {
-ms-transform: scale(1.5);
-moz-transform: scale(1.5);
-webkit-transform: scale(1.5);
-o-transform: scale(1.5);
transform: scale(1.5);
padding: 10px;
margin: 1em auto;
}
.title {
@apply tracking-wide leading-5;
}
.btn {
@apply px-4 py-2 rounded-lg focus:outline-hidden;
@apply shadow-sm focus:shadow-lg;
@apply transition-all duration-300 ease-in-out;
}
.btn-primary {
@apply bg-primary-500 focus:bg-primary-600 active:bg-primary-600;
@apply border-primary-500 focus:border-primary-600 active:border-primary-600;
@apply text-white;
}
.btn-alert {
@apply bg-rose-600 focus:bg-rose-700 active:bg-rose-800;
@apply border-rose-600 focus:border-rose-700 active:border-rose-800;
@apply text-white;
}
.hr {
@apply w-full max-w-2xl border border-primary-300;
}
.hr-light {
@apply w-full max-w-2xl border border-white;
}
.link {
@apply hover:underline;
@apply transition-colors duration-500 ease-in-out;
}
/* slim select */
.ss-main.input-primary {
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
}
.ss-content {
@apply input;
}
.ss-content.input-primary {
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
}
@ -181,10 +241,6 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
border-top-right-radius: 0px;
}
.ss-search input[type="search"] {
@apply input;
}
.ss-content.input-primary .ss-search input[type="search"] {
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
}

View File

@ -1,7 +1,3 @@
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import '../css/app.scss'
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

View File

@ -1,7 +1,7 @@
export default {
displayDate (el) {
const date =
Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' })
Intl.DateTimeFormat([], { timeZone: 'UTC', dateStyle: 'short' })
.format(new Date(el.dateTime))
el.innerText = date

View File

@ -1,7 +1,7 @@
export default {
displayDateTime (el) {
const date =
Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'long' })
Intl.DateTimeFormat([], { timeZone: 'UTC', dateStyle: 'short', timeStyle: 'long' })
.format(new Date(el.dateTime))
el.innerText = date

View File

@ -3,7 +3,12 @@ import SlimSelect from 'slim-select'
export default {
initalizeSlimSelect (el) {
// eslint-disable-next-line no-new
el.slimselect = new SlimSelect({ select: el })
el.slimselect = new SlimSelect({
select: el,
settings: {
contentPosition: 'fixed'
}
})
const main = document.querySelector(`.ss-main[data-id="${el.dataset.id}"]`)
main.setAttribute('id', `${el.dataset.id}-main`)

10291
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,22 @@
{
"repository": {},
"description": " ",
"license": "MIT",
"engines": {
"node": "v23.0.0",
"npm": "10.9.0"
"node": "v24.3.0",
"npm": "11.4.2"
},
"scripts": {
"deploy": "NODE_ENV=production webpack --mode production",
"watch": "webpack --mode development --watch --watch-options-stdin",
"format": "standard --fix",
"test": "standard"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"chart.js": "^4.4.5",
"@fortawesome/fontawesome-free": "^6.7.2",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"slim-select": "^2.9.2",
"slim-select": "^2.12.1",
"topbar": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"autoprefixer": "^10.4.20",
"babel-loader": "^9.2.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"file-loader": "^6.2.0",
"glob": "^11.0.0",
"mini-css-extract-plugin": "^2.9.1",
"npm-check-updates": "^17.1.6",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.0.8",
"sass": "^1.80.4",
"sass-loader": "^16.0.2",
"standard": "^17.1.2",
"tailwindcss": "^3.4.14",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
"npm-check-updates": "^18.0.1",
"standard": "^17.1.2"
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,43 +0,0 @@
const colors = require('tailwindcss/colors')
module.exports = {
content: [
'../lib/**/*.{ex,heex,leex,eex}',
'./js/**/*.js'
],
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
primary: colors.gray,
black: colors.black,
white: colors.white,
gray: colors.neutral,
indigo: colors.indigo,
red: colors.rose,
yellow: colors.amber
},
extend: {
spacing: {
128: '32rem',
192: '48rem',
256: '64rem'
},
minWidth: {
4: '1rem',
8: '2rem',
12: '3rem',
16: '4rem',
20: '8rem'
},
maxWidth: {
4: '1rem',
8: '2rem',
12: '3rem',
16: '4rem',
20: '8rem'
}
}
},
plugins: []
}

View File

@ -1,57 +0,0 @@
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, options) => {
const devMode = options.mode !== 'production'
return {
optimization: {
minimizer: [
new TerserPlugin({ parallel: true, extractComments: true }),
new CssMinimizerPlugin({})
]
},
entry: {
app: glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../priv/static/js'),
publicPath: '/js/'
},
devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.s?css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
},
{
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
type: 'asset/resource',
generator: { filename: 'fonts/[name].[ext]' }
}
]
},
plugins: [
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
new CopyWebpackPlugin({ patterns: [{ from: 'static/', to: '../' }] })
]
}
}

View File

@ -8,13 +8,14 @@
import Config
config :cannery,
ecto_repos: [Cannery.Repo],
generators: [binary_id: true]
env: :dev,
ecto_repos: [Cannery.Repo]
config :cannery, Cannery.Accounts, registration: System.get_env("REGISTRATION", "invite")
# Configures the endpoint
config :cannery, CanneryWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,
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",
@ -28,9 +29,10 @@ config :cannery, CanneryWeb.Endpoint,
config :cannery, Cannery.Application, automigrate: false
config :cannery, :generators,
migration: true,
binary_id: true,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
migration: true,
sample_binary_id: "11111111-1111-1111-1111-111111111111",
timestamp_type: :utc_datetime_usec
# Configures the mailer
#
@ -54,14 +56,25 @@ config :cannery, Oban,
queues: [default: 10, mailers: 20]
# Configure esbuild (the version is required)
# config :esbuild,
# version: "0.14.0",
# default: [
# args:
# ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
# cd: Path.expand("../assets", __DIR__),
# env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
# ]
config :esbuild,
version: "0.17.11",
cannery: [
args:
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
# Configure tailwind (the version is required)
config :tailwind,
version: "4.0.0",
cannery: [
args: ~w(
--input=css/style.css
--output=../priv/static/assets/style.css
),
cd: Path.expand("../assets", __DIR__)
]
# Configures Elixir's Logger
config :logger, :console,

View File

@ -2,6 +2,7 @@ import Config
# Configure your database
config :cannery, Cannery.Repo,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
@ -12,21 +13,14 @@ config :cannery, Cannery.Repo,
# watchers to your application. For example, we use it
# with esbuild to bundle .js and .css sources.
config :cannery, CanneryWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
watchers: [
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
# esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
"development",
"--watch",
"--watch-options-stdin",
cd: Path.expand("../assets", __DIR__)
]
esbuild: {Esbuild, :install_and_run, [:cannery, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:cannery, ~w(--watch)]}
]
# ## SSL Support
@ -57,7 +51,7 @@ config :cannery, CanneryWeb.Endpoint,
config :cannery, CanneryWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/cannery_web/*/.*(ex)$"
]
@ -73,3 +67,9 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
# Include HEEx debug annotations as HTML comments in rendered markup
debug_heex_annotations: true,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true

View File

@ -14,6 +14,8 @@ config :cannery, CanneryWeb.Endpoint, cache_static_manifest: "priv/static/cache_
# Do not print debug messages in production
config :logger, level: :info
config :cannery, env: :prod
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key

View File

@ -7,13 +7,23 @@ import Config
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
# Start the phoenix server if environment is set and running in a release
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it:
#
# PHX_SERVER=true bin/cannery start
#
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do
config :cannery, CanneryWeb.Endpoint, server: true
end
config :cannery, CanneryWeb.HTMLHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
config :cannery, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# Set default locale
config :gettext, :default_locale, System.get_env("LOCALE", "en_US")
@ -68,7 +78,7 @@ if config_env() == :prod do
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
You can generate one by calling: priv/random.sh
"""
config :cannery, CanneryWeb.Endpoint, secret_key_base: secret_key_base

View File

@ -20,6 +20,8 @@ config :cannery, CanneryWeb.Endpoint,
secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
server: false
config :cannery, env: :test
# In test we don't send emails.
config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test

View File

@ -32,7 +32,12 @@ services:
- cannery-db
cannery-db:
image: postgres:13
image: postgres:17
# To upgrade your database from a previous version, replace the existing
# image with the following line. Once the container has been booted and your
# database updated, you can revert back to the original `postgres:17` image
# if you'd like
# image: pgautoupgrade/pgautoupgrade:17-alpine
container_name: cannery-db
environment:
- POSTGRES_USER="postgres"
@ -40,4 +45,47 @@ services:
- POSTGRES_DB="cannery"
restart: always
volumes:
- ./data:/var/lib/postgresql/data
- ./cannery/data:/var/lib/postgresql/data
# Optional, if you don't have a reverse proxy set up already. You can replace
# this with any other reverse proxy like nginx, apache or caddy if you would
# like something more minimalist.
# nginx:
# image: jc21/nginx-proxy-manager
# container_name: nginx
# restart: on-failure
# ports:
# - 443:443
# - 80:80
# # Access via http://localhost:81, although once you access this, you can
# # reverse proxy itself to a domain name to control it from anywhere if
# # you'd like, and then you can remove the following line
# - 127.0.0.1:81:81
# # expose:
# # - 81
# environment:
# # DISABLE_IPV6: 'true'
# DB_MYSQL_HOST: "nginx-db"
# DB_MYSQL_NAME: "nginx"
# DB_MYSQL_PASSWORD: "my-password"
# DB_MYSQL_PORT: 3306
# DB_MYSQL_USER: "nginx"
# PGID: 9000
# PUID: 9000
# volumes:
# - /etc/letsencrypt:/etc/letsencrypt
# - ./nginx/data:/data
# - /tmp:/tmp
# nginx-db:
# image: 'jc21/mariadb-aria:latest'
# container_name: nginx-db
# restart: on-failure
# environment:
# MYSQL_DATABASE: 'nginx'
# MYSQL_PASSWORD: 'my-password'
# MYSQL_ROOT_PASSWORD: 'my-root-password'
# MYSQL_USER: 'nginx'
# volumes:
# - ./nginx/mysql:/var/lib/mysql

View File

@ -27,6 +27,7 @@ defmodule Cannery do
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts [type: :utc_datetime_usec]
end
end

View File

@ -218,7 +218,7 @@ defmodule Cannery.Accounts do
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
%UserToken{sent_to: email} <- Repo.one(query),
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
{:ok, _result} <- Repo.transaction(user_email_multi(user, email, context)) do
:ok
else
_error_tuple -> :error

View File

@ -11,13 +11,13 @@ defmodule Cannery.Accounts.Invite do
field :name, :string
field :token, :string
field :uses_left, :integer, default: nil
field :disabled_at, :naive_datetime
field :disabled_at, :utc_datetime_usec
belongs_to :created_by, User
has_many :users, User
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
@ -25,12 +25,12 @@ defmodule Cannery.Accounts.Invite do
name: String.t(),
token: token(),
uses_left: integer() | nil,
disabled_at: NaiveDateTime.t(),
disabled_at: DateTime.t(),
created_by: User.t() | nil | Association.NotLoaded.t(),
created_by_id: User.id() | nil,
users: [User.t()] | Association.NotLoaded.t(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_invite :: %__MODULE__{}
@type id :: UUID.t()

View File

@ -123,7 +123,7 @@ defmodule Cannery.Accounts.Invites do
end
defp decrement_invite_changeset(%Invite{uses_left: 1} = invite) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
now = DateTime.utc_now()
invite |> Invite.update_changeset(%{uses_left: 0, disabled_at: now})
end

View File

@ -21,7 +21,8 @@ defmodule Cannery.Accounts.User do
field :email, :string
field :password, :string, virtual: true
field :hashed_password, :string
field :confirmed_at, :naive_datetime
field :current_password, :string, virtual: true, redact: true
field :confirmed_at, :utc_datetime_usec
field :role, Ecto.Enum, values: [:admin, :user], default: :user
field :locale, :string
@ -29,7 +30,7 @@ defmodule Cannery.Accounts.User do
belongs_to :invite, Invite
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %User{
@ -37,14 +38,14 @@ defmodule Cannery.Accounts.User do
email: String.t(),
password: String.t(),
hashed_password: String.t(),
confirmed_at: NaiveDateTime.t(),
confirmed_at: DateTime.t(),
role: role(),
locale: String.t() | nil,
created_invites: [Invite.t()] | Association.NotLoaded.t(),
invite: Invite.t() | nil | Association.NotLoaded.t(),
invite_id: Invite.id() | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_user :: %User{}
@type id :: UUID.t()
@ -136,7 +137,7 @@ defmodule Cannery.Accounts.User do
|> cast(attrs, [:email])
|> validate_email()
|> case do
%{changes: %{email: _}} = changeset -> changeset
%{changes: %{email: _email}} = changeset -> changeset
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
end
end
@ -167,7 +168,7 @@ defmodule Cannery.Accounts.User do
"""
@spec confirm_changeset(t() | changeset()) :: changeset()
def confirm_changeset(user_or_changeset) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
now = DateTime.utc_now()
user_or_changeset |> change(confirmed_at: now)
end
@ -193,6 +194,8 @@ defmodule Cannery.Accounts.User do
"""
@spec validate_current_password(changeset(), String.t()) :: changeset()
def validate_current_password(changeset, password) do
changeset = cast(changeset, %{current_password: password}, [:current_password])
if valid_password?(changeset.data, password),
do: changeset,
else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))

View File

@ -32,7 +32,7 @@ defmodule Cannery.Accounts.UserToken do
sent_to: String.t(),
user: User.t() | Association.NotLoaded.t(),
user_id: User.id() | nil,
inserted_at: NaiveDateTime.t()
inserted_at: DateTime.t()
}
@type new_user_token :: %__MODULE__{}
@type id :: UUID.t()
@ -150,7 +150,7 @@ defmodule Cannery.Accounts.UserToken do
from t in __MODULE__, where: t.user_id == ^user.id
end
def user_and_contexts_query(user, [_ | _] = contexts) do
def user_and_contexts_query(user, [_first | _rest] = contexts) do
from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
end
end

View File

@ -9,6 +9,8 @@ defmodule Cannery.ActivityLog do
@type list_shot_records_option ::
{:search, String.t() | nil}
| {:class, Type.class() | :all | nil}
| {:start_date, String.t() | nil}
| {:end_date, String.t() | nil}
| {:pack_id, Pack.id() | nil}
@type list_shot_records_options :: [list_shot_records_option()]
@ -49,6 +51,8 @@ defmodule Cannery.ActivityLog do
|> 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))
|> list_shot_records_start_date(Keyword.get(opts, :start_date))
|> list_shot_records_end_date(Keyword.get(opts, :end_date))
|> Repo.all()
end
@ -100,6 +104,20 @@ defmodule Cannery.ActivityLog do
defp list_shot_records_pack_id(query, _all), do: query
@spec list_shot_records_start_date(Queryable.t(), String.t() | nil) :: Queryable.t()
defp list_shot_records_start_date(query, start_date) when start_date |> is_binary() do
query |> where([sr: sr], sr.date >= ^Date.from_iso8601!(start_date))
end
defp list_shot_records_start_date(query, _all), do: query
@spec list_shot_records_end_date(Queryable.t(), String.t() | nil) :: Queryable.t()
defp list_shot_records_end_date(query, end_date) when end_date |> is_binary() do
query |> where([sr: sr], sr.date <= ^Date.from_iso8601!(end_date))
end
defp list_shot_records_end_date(query, _all), do: query
@doc """
Returns a count of shot records.

View File

@ -24,7 +24,7 @@ defmodule Cannery.ActivityLog.ShotRecord do
field :user_id, :binary_id
field :pack_id, :binary_id
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
@ -34,8 +34,8 @@ defmodule Cannery.ActivityLog.ShotRecord do
date: Date.t() | nil,
pack_id: Pack.id(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_shot_record :: %__MODULE__{}
@type id :: UUID.t()

View File

@ -547,7 +547,7 @@ defmodule Cannery.Ammo do
@spec list_packs_staged(Queryable.t(), staged :: boolean() | nil) :: Queryable.t()
defp list_packs_staged(query, staged) when staged |> is_boolean(),
do: query |> where([p: p], p.staged == ^staged)
do: query |> where([c: c], c.staged == ^staged)
defp list_packs_staged(query, _nil), do: query
@ -920,7 +920,7 @@ defmodule Cannery.Ammo do
multiplier <= @pack_create_limit and
type_id |> is_binary() and
container_id |> is_binary() do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
now = DateTime.utc_now()
changesets =
Enum.map(1..multiplier, fn _count ->

View File

@ -11,44 +11,41 @@ defmodule Cannery.Ammo.Pack do
@derive {Jason.Encoder,
only: [
:id,
:container_id,
:count,
:id,
:lot_number,
:notes,
:price_paid,
:lot_number,
:staged,
:type_id,
:container_id
:type_id
]}
schema "packs" do
field :count, :integer
field :lot_number, :string
field :notes, :string
field :price_paid, :float
field :staged, :boolean, default: false
field :lot_number, :string
field :purchased_on, :date
belongs_to :type, Type
field :container_id, :binary_id
field :user_id, :binary_id
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
id: id(),
count: integer,
id: id(),
lot_number: String.t() | nil,
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(),
container_id: Container.id(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_pack :: %__MODULE__{}
@type id :: UUID.t()
@ -92,7 +89,13 @@ defmodule Cannery.Ammo.Pack do
|> change(type_id: type_id)
|> change(container_id: container_id)
|> change(user_id: user_id)
|> cast(attrs, [:count, :lot_number, :notes, :price_paid, :purchased_on, :staged])
|> cast(attrs, [
:count,
:lot_number,
:notes,
:price_paid,
:purchased_on
])
|> validate_required(:type_id, message: dgettext("errors", "Please select a valid type"))
|> validate_required(:container_id,
message: dgettext("errors", "Please select a valid container")
@ -100,7 +103,13 @@ defmodule Cannery.Ammo.Pack do
|> 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])
|> validate_required([
:container_id,
:count,
:purchased_on,
:type_id,
:user_id
])
end
@doc false
@ -108,19 +117,22 @@ defmodule Cannery.Ammo.Pack do
def update_changeset(pack, attrs, user) do
pack
|> cast(attrs, [
:container_id,
:count,
:price_paid,
:notes,
:staged,
:purchased_on,
:lot_number,
:container_id
:notes,
:price_paid,
:purchased_on
])
|> 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])
|> validate_required([
:container_id,
:count,
:purchased_on
])
end
defp validate_container_id(changeset, user) do
@ -140,7 +152,7 @@ defmodule Cannery.Ammo.Pack do
@spec range_changeset(t() | new_pack(), attrs :: map()) :: changeset()
def range_changeset(pack, attrs) do
pack
|> cast(attrs, [:count, :staged])
|> validate_required([:count, :staged])
|> cast(attrs, [:count])
|> validate_required([:count])
end
end

View File

@ -90,7 +90,7 @@ defmodule Cannery.Ammo.Type do
field :user_id, :binary_id
has_many :packs, Pack
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
@ -129,8 +129,8 @@ defmodule Cannery.Ammo.Type do
dram_equivalent: String.t() | nil,
user_id: User.id(),
packs: [Pack.t()] | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_type :: %__MODULE__{}
@type id :: UUID.t()

View File

@ -15,6 +15,7 @@ defmodule Cannery.Application do
CanneryWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Cannery.PubSub},
{DNSCluster, query: Application.get_env(:cannery, :dns_cluster_query) || :ignore},
# Start the Endpoint (http/https)
CanneryWeb.Endpoint,
# Add Oban

View File

@ -9,7 +9,9 @@ defmodule Cannery.Containers do
@container_preloads [:tags]
@type list_containers_option :: {:search, String.t() | nil}
@type list_containers_option ::
{:search, String.t() | nil}
| {:staged, boolean() | nil}
@type list_containers_options :: [list_containers_option()]
@doc """
@ -20,7 +22,10 @@ defmodule Cannery.Containers do
iex> list_containers(%User{id: 123})
[%Container{}, ...]
iex> list_containers(%User{id: 123}, search: "cool")
iex> list_containers(%User{id: 123},
...> search: "cool",
...> staged: true
...> )
[%Container{name: "my cool container"}, ...]
"""
@ -37,9 +42,16 @@ defmodule Cannery.Containers do
preload: ^@container_preloads
)
|> list_containers_search(Keyword.get(opts, :search))
|> list_containers_staged(Keyword.get(opts, :staged))
|> Repo.all()
end
@spec list_containers_staged(Queryable.t(), staged :: boolean() | nil) :: Queryable.t()
defp list_containers_staged(query, staged) when staged |> is_boolean(),
do: query |> where([c: c], c.staged == ^staged)
defp list_containers_staged(query, _nil), 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)

View File

@ -8,36 +8,39 @@ defmodule Cannery.Containers.Container do
@derive {Jason.Encoder,
only: [
:id,
:name,
:desc,
:id,
:location,
:type,
:tags
:name,
:staged,
:tags,
:type
]}
schema "containers" do
field :name, :string
field :desc, :string
field :location, :string
field :name, :string
field :staged, :boolean, default: false
field :type, :string
field :user_id, :binary_id
many_to_many :tags, Tag, join_through: ContainerTag
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
id: id(),
name: String.t(),
desc: String.t(),
id: id(),
location: String.t(),
name: String.t(),
staged: boolean(),
type: String.t(),
user_id: User.id(),
tags: [Tag.t()] | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_container :: %__MODULE__{}
@type id :: UUID.t()
@ -48,19 +51,40 @@ defmodule Cannery.Containers.Container do
def create_changeset(container, %User{id: user_id}, attrs) do
container
|> change(user_id: user_id)
|> cast(attrs, [:name, :desc, :type, :location])
|> cast(attrs, [
:desc,
:location,
:name,
:staged,
:type
])
|> validate_length(:name, max: 255)
|> validate_length(:type, max: 255)
|> validate_required([:name, :type, :user_id])
|> validate_required([
:name,
:staged,
:type,
:user_id
])
end
@doc false
@spec update_changeset(t() | new_container(), attrs :: map()) :: changeset()
def update_changeset(container, attrs) do
container
|> cast(attrs, [:name, :desc, :type, :location])
|> cast(attrs, [
:desc,
:location,
:name,
:staged,
:type
])
|> validate_length(:name, max: 255)
|> validate_length(:type, max: 255)
|> validate_required([:name, :type])
|> validate_required([
:name,
:staged,
:type
])
end
end

View File

@ -11,7 +11,7 @@ defmodule Cannery.Containers.ContainerTag do
belongs_to :container, Container
belongs_to :tag, Tag
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
@ -20,8 +20,8 @@ defmodule Cannery.Containers.ContainerTag do
container_id: Container.id(),
tag: Tag.t(),
tag_id: Tag.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_container_tag :: %__MODULE__{}
@type id :: UUID.t()

View File

@ -20,7 +20,7 @@ defmodule Cannery.Containers.Tag do
field :user_id, :binary_id
timestamps()
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
@ -29,8 +29,8 @@ defmodule Cannery.Containers.Tag do
bg_color: String.t(),
text_color: String.t(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@type new_tag() :: %__MODULE__{}
@type id() :: UUID.t()

View File

@ -17,7 +17,7 @@ defmodule CanneryWeb do
those modules here.
"""
def static_paths, do: ~w(css js fonts images favicon.ico robots.txt)
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt webfonts)
def router do
quote do

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= gettext("Record shots") %>
{gettext("Record shots")}
</h2>
<.form
@ -16,47 +16,47 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :ammo_left,
{label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600")}
{number_input(f, :ammo_left,
min: 0,
max: @pack.count - 1,
placeholder: gettext("Rounds left"),
class: "input input-primary"
) %>
)}
<button
type="button"
class="mx-2 my-1 text-sm btn btn-primary"
phx-click={JS.dispatch("cannery:set-zero", to: "#shot-record-form_ammo_left")}
>
<%= gettext("Used up!") %>
{gettext("Used up!")}
</button>
<%= error_tag(f, :ammo_left, "col-span-3") %>
{error_tag(f, :ammo_left, "col-span-3")}
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
<%= textarea(f, :notes,
{label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600")}
{textarea(f, :notes,
class: "input input-primary col-span-2",
id: "add-shot-record-form-notes",
maxlength: 255,
phx_debounce: 300,
phx_update: "ignore",
placeholder: gettext("Really great weather")
) %>
<%= error_tag(f, :notes, "col-span-3") %>
)}
{error_tag(f, :notes, "col-span-3")}
<%= label(f, :date, gettext("Date"), class: "title text-lg text-primary-600") %>
<%= date_input(f, :date,
{label(f, :date, gettext("Date"), class: "title text-lg text-primary-600")}
{date_input(f, :date,
class: "input input-primary col-span-2",
phx_update: "ignore",
value: Date.utc_today()
) %>
<%= error_tag(f, :notes, "col-span-3") %>
)}
{error_tag(f, :notes, "col-span-3")}
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
class: "mx-auto btn btn-primary col-span-3",
phx_disable_with: dgettext("prompts", "Saving...")
) %>
)}
</.form>
</div>

View File

@ -4,6 +4,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
"""
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Ammo, Containers.Container}
alias CanneryWeb.Components.TableComponent
alias Ecto.UUID
alias Phoenix.LiveView.{Rendered, Socket}
@ -13,6 +14,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
required(:id) => UUID.t(),
required(:current_user) => User.t(),
optional(:containers) => [Container.t()],
optional(:range) => Rendered.t(),
optional(:tag_actions) => Rendered.t(),
optional(:actions) => Rendered.t(),
optional(any()) => any()
@ -23,6 +25,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
socket =
socket
|> assign(assigns)
|> assign_new(:range, fn -> [] end)
|> assign_new(:tag_actions, fn -> [] end)
|> assign_new(:actions, fn -> [] end)
|> display_containers()
@ -35,6 +38,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
assigns: %{
containers: containers,
current_user: current_user,
range: range,
tag_actions: tag_actions,
actions: actions
}
@ -62,13 +66,22 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
end)
|> Enum.concat([
%{label: gettext("Packs"), key: :packs, type: :integer},
%{label: gettext("Rounds"), key: :rounds, type: :integer},
%{label: gettext("Tags"), key: :tags, type: :tags},
%{label: gettext("Actions"), key: :actions, sortable: false, type: :actions}
%{label: gettext("Rounds"), key: :rounds, type: :integer}
])
|> Enum.concat(
[
%{label: gettext("Tags"), key: :tags, type: :tags},
%{label: gettext("Actions"), key: :actions, sortable: false, type: :actions}
]
|> TableComponent.maybe_compose_columns(
%{label: gettext("Range"), key: :range},
range != []
)
)
extra_data = %{
current_user: current_user,
range: range,
tag_actions: tag_actions,
actions: actions,
pack_count:
@ -122,7 +135,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
~H"""
<div class="flex flex-wrap justify-center items-center">
<.link navigate={~p"/container/#{@id}"} class="link">
<%= @name %>
{@name}
</.link>
</div>
"""}
@ -136,6 +149,15 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
round_count |> Map.get(container_id, 0)
end
defp get_value_for_key(:range, %{staged: staged} = container, %{range: range}) do
assigns = %{range: range, container: container}
{staged,
~H"""
{render_slot(@range, @container)}
"""}
end
defp get_value_for_key(:tags, container, %{tag_actions: tag_actions}) do
assigns = %{tag_actions: tag_actions, container: container}
@ -150,7 +172,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
<div class="flex flex-wrap justify-center items-center">
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
<%= render_slot(@tag_actions, @container) %>
{render_slot(@tag_actions, @container)}
</div>
"""}
end
@ -159,7 +181,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
assigns = %{actions: actions, container: container}
~H"""
<%= render_slot(@actions, @container) %>
{render_slot(@actions, @container)}
"""
end

View File

@ -136,14 +136,26 @@ defmodule CanneryWeb.CoreComponents do
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
@doc """
Phoenix.Component for a <time> element that renders the naivedatetime in the
Phoenix.Component for a <time> element that renders the DateTime in the
user's local timezone
"""
def datetime(assigns)
@spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
defp cast_datetime(%NaiveDateTime{} = datetime) do
datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
attr :name, :string, required: true
attr :start_date, :string,
default: Date.utc_today() |> Date.shift(year: -1) |> Date.to_iso8601()
attr :end_date, :string, default: Date.utc_today() |> Date.to_iso8601()
@doc """
Phoenix.Component for an element that generates date fields for a range
"""
def date_range(assigns)
@spec cast_datetime(DateTime.t() | nil) :: String.t()
defp cast_datetime(%DateTime{} = datetime) do
datetime |> DateTime.to_iso8601(:extended)
end
defp cast_datetime(_datetime), do: ""

View File

@ -2,40 +2,40 @@
id={"container-#{@container.id}"}
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
flex flex-col justify-around items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<.link navigate={~p"/container/#{@container}"} class="link">
<h1 class="px-4 py-2 rounded-lg title text-xl">
<%= @container.name %>
{@container.name}
</h1>
</.link>
<div class="flex flex-col justify-center items-center space-y-2">
<span :if={@container.desc} class="rounded-lg title text-lg">
<%= gettext("Description:") %>
<%= @container.desc %>
{gettext("Description:")}
{@container.desc}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Type:") %>
<%= @container.type %>
{gettext("Type:")}
{@container.type}
</span>
<span :if={@container.location} class="rounded-lg title text-lg">
<%= gettext("Location:") %>
<%= @container.location %>
{gettext("Location:")}
{@container.location}
</span>
<%= if Ammo.get_packs_count(@current_user, container_id: @container.id) != 0 do %>
<span class="rounded-lg title text-lg">
<%= gettext("Packs:") %>
<%= Ammo.get_packs_count(@current_user, container_id: @container.id) %>
{gettext("Packs:")}
{Ammo.get_packs_count(@current_user, container_id: @container.id)}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Rounds:") %>
<%= Ammo.get_round_count(@current_user, container_id: @container.id) %>
{gettext("Rounds:")}
{Ammo.get_round_count(@current_user, container_id: @container.id)}
</span>
<% end %>
@ -45,7 +45,7 @@
>
<.simple_tag_card :for={tag <- @container.tags} tag={tag} />
<%= if @tag_actions, do: render_slot(@tag_actions) %>
{if @tag_actions, do: render_slot(@tag_actions)}
</div>
</div>
@ -53,6 +53,6 @@
:if={assigns |> Map.has_key?(:inner_block)}
class="flex space-x-4 justify-center items-center"
>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</div>

View File

@ -1,3 +1,3 @@
<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
<%= Date.to_iso8601(@date, :extended) %>
{Date.to_iso8601(@date, :extended)}
</time>

View File

@ -0,0 +1,15 @@
<div class="flex items-center mx-4 my-2 space-x-1">
<input
class="w-40 text-center input input-primary"
name={"#{@name}_start"}
type="date"
value={@start_date}
/>
<span>—</span>
<input
class="w-40 text-center input input-primary"
name={"#{@name}_end"}
type="date"
value={@end_date}
/>
</div>

View File

@ -1,3 +1,3 @@
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
<%= cast_datetime(@datetime) %>
{cast_datetime(@datetime)}
</time>

View File

@ -1,24 +1,24 @@
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out">
<h1 class="title text-xl">
<%= @invite.name %>
{@invite.name}
</h1>
<%= if @invite.disabled_at |> is_nil() do %>
<h2 class="title text-md">
<%= if @invite.uses_left do %>
<%= gettext(
{gettext(
"Uses Left: %{uses_left_count}",
uses_left_count: @invite.uses_left
) %>
)}
<% else %>
<%= gettext("Uses Left: Unlimited") %>
{gettext("Uses Left: Unlimited")}
<% end %>
</h2>
<% else %>
<h2 class="title text-md">
<%= gettext("Invite Disabled") %>
{gettext("Invite Disabled")}
</h2>
<% end %>
@ -28,19 +28,19 @@
/>
<h2 :if={@use_count && @use_count != 0} class="title text-md">
<%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
{gettext("Uses: %{uses_count}", uses_count: @use_count)}
</h2>
<div class="flex flex-row flex-wrap justify-center items-center">
<code
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"
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-zinc-100 bg-primary-800"
phx-no-format
><%= url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
<%= if @code_actions, do: render_slot(@code_actions) %>
{if @code_actions, do: render_slot(@code_actions)}
</div>
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</div>

View File

@ -29,7 +29,7 @@
patch={@return_to}
id="close"
class="absolute top-8 right-10
text-gray-500 hover:text-gray-800
text-zinc-500 hover:text-zinc-800
transition-all duration-500 ease-in-out"
phx-remove={hide_modal()}
aria-label={gettext("Close modal")}
@ -38,7 +38,7 @@
</.link>
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</div>
</div>

View File

@ -2,66 +2,66 @@
id={"pack-#{@pack.id}"}
class="mx-4 my-2 px-8 py-4
flex flex-col justify-center items-center
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<.link navigate={~p"/ammo/show/#{@pack}"} class="mb-2 link">
<h1 class="title text-xl title-primary-500">
<%= @pack.type.name %>
{@pack.type.name}
</h1>
</.link>
<div class="flex flex-col justify-center items-center">
<span class="rounded-lg title text-lg">
<%= gettext("Count:") %>
<%= if @pack.count == 0, do: gettext("Empty"), else: @pack.count %>
{gettext("Count:")}
{if @pack.count == 0, do: gettext("Empty"), else: @pack.count}
</span>
<span :if={@original_count && @original_count != @pack.count} class="rounded-lg title text-lg">
<%= gettext("Original Count:") %>
<%= @original_count %>
{gettext("Original Count:")}
{@original_count}
</span>
<span :if={@pack.notes} class="rounded-lg title text-lg">
<%= gettext("Notes:") %>
<%= @pack.notes %>
{gettext("Notes:")}
{@pack.notes}
</span>
<span :if={@pack.purchased_on} class="rounded-lg title text-lg">
<%= gettext("Purchased on:") %>
{gettext("Purchased on:")}
<.date id={"#{@pack.id}-purchased-on"} date={@pack.purchased_on} />
</span>
<span :if={@last_used_date} class="rounded-lg title text-lg">
<%= gettext("Last used on:") %>
{gettext("Last used on:")}
<.date id={"#{@pack.id}-last-used-on"} date={@last_used_date} />
</span>
<span :if={@pack.price_paid} class="rounded-lg title text-lg">
<%= gettext("Price paid:") %>
<%= gettext("$%{amount}", amount: display_currency(@pack.price_paid)) %>
{gettext("Price paid:")}
{gettext("$%{amount}", amount: display_currency(@pack.price_paid))}
</span>
<span :if={@cpr} class="rounded-lg title text-lg">
<%= gettext("CPR:") %>
<%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
{gettext("CPR:")}
{gettext("$%{amount}", amount: display_currency(@cpr))}
</span>
<span :if={@pack.lot_number} class="rounded-lg title text-lg">
<%= gettext("Lot number:") %>
<%= @pack.lot_number %>
{gettext("Lot number:")}
{@pack.lot_number}
</span>
<span :if={@container} class="rounded-lg title text-lg">
<%= gettext("Container:") %>
{gettext("Container:")}
<.link navigate={~p"/container/#{@container}"} class="link">
<%= @container.name %>
{@container.name}
</.link>
</span>
</div>
<div :if={@inner_block} class="mt-4 flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</div>

View File

@ -2,5 +2,5 @@
class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
>
<%= @tag.name %>
{@tag.name}
</h1>

View File

@ -1,9 +1,9 @@
<div
id={"tag-#{@tag.id}"}
class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<.simple_tag_card tag={@tag} />
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>

View File

@ -1,4 +1,4 @@
<label for={@id || @action} class="relative inline-flex items-center cursor-pointer">
<label for={@id || @action} class="inline-flex relative items-center cursor-pointer">
<input
id={@id || @action}
type="checkbox"
@ -12,19 +12,17 @@
else: %{"phx-click": @action, "phx-value-value": @value}
}
/>
<div class="w-11 h-6 bg-gray-300 rounded-full peer
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
peer-checked:bg-gray-600
peer-checked:after:translate-x-full peer-checked:after:border-white
after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
<div class="w-11 h-6 bg-zinc-300 rounded-full peer
peer-checked:bg-zinc-600 peer-checked:after:translate-x-full peer-checked:after:border-white
after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300
after:border after:rounded-full after:h-5 after:w-5
after:transition-all after:duration-250 after:ease-in-out
transition-colors duration-250 ease-in-out">
</div>
<span
id={"#{@id || @action}-label"}
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 whitespace-nowrap"
class="ml-3 text-sm font-medium whitespace-nowrap text-zinc-900 dark:text-zinc-300"
>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</span>
</label>

View File

@ -14,7 +14,7 @@
<span class="mx-2 my-1">
|
</span>
<%= @title_content %>
{@title_content}
<% end %>
</div>
@ -25,37 +25,37 @@
<%= if @current_user do %>
<li class="mx-2 my-1">
<.link navigate={~p"/tags"} class="text-white hover:underline">
<%= gettext("Tags") %>
{gettext("Tags")}
</.link>
</li>
<li class="mx-2 my-1">
<.link navigate={~p"/containers"} class="text-white hover:underline">
<%= gettext("Containers") %>
{gettext("Containers")}
</.link>
</li>
<li class="mx-2 my-1">
<.link navigate={~p"/catalog"} class="text-white hover:underline">
<%= gettext("Catalog") %>
{gettext("Catalog")}
</.link>
</li>
<li class="mx-2 my-1">
<.link navigate={~p"/ammo"} class="text-white hover:underline">
<%= gettext("Ammo") %>
{gettext("Ammo")}
</.link>
</li>
<li class="mx-2 my-1">
<.link navigate={~p"/range"} class="text-white hover:underline">
<%= gettext("Range") %>
{gettext("Range")}
</.link>
</li>
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
<.link navigate={~p"/invites"} class="text-white hover:underline">
<%= gettext("Invites") %>
{gettext("Invites")}
</.link>
</li>
<li class="mx-2 my-1">
<.link href={~p"/users/settings"} class="text-white hover:underline truncate">
<%= @current_user.email %>
{@current_user.email}
</.link>
</li>
<li class="mx-2 my-1">
@ -86,12 +86,12 @@
<% else %>
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
<.link href={~p"/users/register"} class="text-white hover:underline truncate">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</.link>
</li>
<li class="mx-2 my-1">
<.link href={~p"/users/log_in"} class="text-white hover:underline truncate">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</.link>
</li>
<% end %>

View File

@ -1,36 +1,36 @@
<div
id={"user-#{@user.id}"}
class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
<%= @user.email %>
{@user.email}
</h1>
<h3 class="px-4 py-2 rounded-lg title text-lg">
<p>
<%= if @user.confirmed_at do %>
<%= gettext(
{gettext(
"User was confirmed at%{confirmed_datetime}",
confirmed_datetime: ""
) %>
)}
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
<% else %>
<%= gettext("Email unconfirmed") %>
{gettext("Email unconfirmed")}
<% end %>
</p>
<p>
<%= gettext(
{gettext(
"User registered on%{registered_datetime}",
registered_datetime: ""
) %>
)}
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
</p>
</h3>
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</div>

View File

@ -1,23 +1,23 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.75em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
{dgettext("emails", "Hi %{email},", email: @user.email)}
</span>
<br />
<span style="margin-bottom: 1em; font-size: 1.25em;">
<%= dgettext("emails", "Welcome to Cannery") %>
{dgettext("emails", "Welcome to Cannery")}
</span>
<br />
<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %>
{dgettext("emails", "You can confirm your account by visiting the URL below:")}
<br />
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}>{@url}</a>
<br />
<%= dgettext("emails", "If you didn't create an account at Cannery, please ignore this.") %>
{dgettext("emails", "If you didn't create an account at Cannery, please ignore this.")}
</div>

View File

@ -1,17 +1,17 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
{dgettext("emails", "Hi %{email},", email: @user.email)}
</span>
<br />
<%= dgettext("emails", "You can reset your password by visiting the URL below:") %>
{dgettext("emails", "You can reset your password by visiting the URL below:")}
<br />
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}>{@url}</a>
<br />
<%= dgettext("emails", "If you didn't request this change from Cannery, please ignore this.") %>
{dgettext("emails", "If you didn't request this change from Cannery, please ignore this.")}
</div>

View File

@ -1,20 +1,20 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
{dgettext("emails", "Hi %{email},", email: @user.email)}
</span>
<br />
<%= dgettext("emails", "You can change your email by visiting the URL below:") %>
{dgettext("emails", "You can change your email by visiting the URL below:")}
<br />
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}>{@url}</a>
<br />
<%= dgettext(
{dgettext(
"emails",
"If you didn't request this change from Cannery, please ignore this."
) %>
)}
</div>

View File

@ -10,7 +10,7 @@
phx-click="lv:clear-flash"
phx-value-key="info"
>
<%= Phoenix.Flash.get(@flash, :info) %>
{Phoenix.Flash.get(@flash, :info)}
</p>
<p
@ -20,13 +20,13 @@
phx-click="lv:clear-flash"
phx-value-key="error"
>
<%= Phoenix.Flash.get(@flash, :error) %>
{Phoenix.Flash.get(@flash, :error)}
</p>
</div>
</header>
<div class="mx-4 sm:mx-8 md:mx-16 flex flex-col justify-center items-stretch">
<%= @inner_content %>
{@inner_content}
</div>
</main>
@ -40,6 +40,6 @@
<i class="fas fa-fade text-md fa-satellite-dish"></i>
<h1 class="title text-md title-primary-500">
<%= gettext("Reconnecting...") %>
{gettext("Reconnecting...")}
</h1>
</div>

View File

@ -1,19 +1,19 @@
<html>
<head>
<title>
<%= @email.subject %>
{@email.subject}
</title>
</head>
<body style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;">
<%= @inner_content %>
{@inner_content}
<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={~p"/"}>
<%= dgettext(
{dgettext(
"emails",
"This email was sent from Cannery, the self-hosted firearm tracker website."
) %>
)}
</a>
</body>
</html>

View File

@ -1 +1 @@
<%= @inner_block %>
{@inner_block}

View File

@ -1,20 +1,19 @@
<!DOCTYPE html>
<html lang="en" class="m-0 p-0 w-full h-full bg-white">
<html lang="en" class="p-0 m-0 w-full h-full bg-white [scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<link rel="shortcut icon" type="image/jpg" href={~p"/images/cannery.svg"} />
<.live_title suffix={" | #{gettext("Cannery")}"}>
<%= assigns[:page_title] || gettext("Cannery") %>
{assigns[:page_title] || gettext("Cannery")}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
<link phx-track-static rel="stylesheet" href={~p"/assets/style.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="m-0 p-0 w-full h-full subpixel-antialiased">
<%= @inner_content %>
<body class="p-0 m-0 w-full h-full subpixel-antialiased">
{@inner_content}
</body>
</html>

View File

@ -74,17 +74,17 @@ defmodule CanneryWeb.Components.MovePackComponent do
~H"""
<div class="w-full flex flex-col space-y-8 justify-center items-center">
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= dgettext("actions", "Move ammo") %>
{dgettext("actions", "Move ammo")}
</h2>
<%= if @containers |> Enum.empty?() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No other containers") %>
<%= display_emoji("😔") %>
{gettext("No other containers")}
{display_emoji("😔")}
</h2>
<.link navigate={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "Add another container!") %>
{dgettext("actions", "Add another container!")}
</.link>
<% else %>
<.live_component
@ -120,7 +120,7 @@ defmodule CanneryWeb.Components.MovePackComponent do
phx-target={@myself}
phx-value-container_id={@container.id}
>
<%= dgettext("actions", "Select") %>
{dgettext("actions", "Select")}
</button>
</div>
"""

View File

@ -170,7 +170,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
{type_name,
~H"""
<%= render_slot(@type_block, @type) %>
{render_slot(@type_block, @type)}
"""}
end
@ -196,18 +196,17 @@ defmodule CanneryWeb.Components.PackTableComponent do
<%= if @last_used_date do %>
<.date id={"#{@id}-last-used-date"} date={@last_used_date} />
<% else %>
<%= gettext("Never used") %>
{gettext("Never used")}
<% end %>
"""}
end
defp get_value_for_key(:range, %{staged: staged} = pack, %{range: range}) do
defp get_value_for_key(:range, pack, %{range: range}) do
assigns = %{range: range, pack: pack}
{staged,
~H"""
<%= render_slot(@range, @pack) %>
"""}
~H"""
{render_slot(@range, @pack)}
"""
end
defp get_value_for_key(
@ -223,7 +222,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
assigns = %{actions: actions, pack: pack}
~H"""
<%= render_slot(@actions, @pack) %>
{render_slot(@actions, @pack)}
"""
end
@ -244,7 +243,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
{container_name,
~H"""
<%= render_slot(@container_block, {@pack, @container}) %>
{render_slot(@container_block, {@pack, @container})}
"""}
end

View File

@ -99,7 +99,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
{pack.type.name,
~H"""
<.link navigate={~p"/ammo/show/#{@pack}"} class="link">
<%= @pack.type.name %>
{@pack.type.name}
</.link>
"""}
end
@ -115,7 +115,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
assigns = %{actions: actions, shot_record: shot_record}
~H"""
<%= render_slot(@actions, @shot_record) %>
{render_slot(@actions, @shot_record)}
"""
end

View File

@ -20,6 +20,7 @@ defmodule CanneryWeb.Components.TableComponent do
"""
use CanneryWeb, :live_component
alias Cannery.{ComparableDate, ComparableDateTime}
alias Phoenix.LiveView.Socket
require Integer
@ -75,7 +76,7 @@ defmodule CanneryWeb.Components.TableComponent do
sort_mode: initial_sort_mode
)
|> assign_new(:row_class, fn -> "bg-white" end)
|> assign_new(:alternate_row_class, fn -> "bg-gray-200" end)
|> assign_new(:alternate_row_class, fn -> "bg-zinc-200" end)
{:ok, socket}
end
@ -110,7 +111,7 @@ defmodule CanneryWeb.Components.TableComponent do
end
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
when type in [Date, DateTime] do
when type in [ComparableDate, ComparableDateTime, Date, DateTime] do
rows
|> Enum.sort_by(
fn row ->

View File

@ -1,4 +1,4 @@
<div id={@id} class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-white">
<div id={@id} class="w-full overflow-x-auto border border-zinc-600 rounded-lg shadow-lg bg-white">
<table class="min-w-full table-auto text-center bg-white">
<thead class="border-b border-primary-600">
<tr>
@ -12,7 +12,7 @@
phx-target={@myself}
>
<i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
<span class={if @last_sort_key == key, do: "underline"}><%= label %></span>
<span class={if @last_sort_key == key, do: "underline"}>{label}</span>
<%= if @last_sort_key == key do %>
<%= case @sort_mode do %>
<% :asc -> %>
@ -27,7 +27,7 @@
</th>
<% else %>
<th class={["p-2 cursor-not-allowed", column[:class]]}>
<%= label %>
{label}
</th>
<% end %>
<% end %>
@ -41,9 +41,9 @@
<td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
<%= case values |> Map.get(key) do %>
<% {_custom_sort_value, value} -> %>
<%= value %>
{value}
<% value -> %>
<%= value %>
{value}
<% end %>
</td>
</tr>

View File

@ -278,7 +278,7 @@ defmodule CanneryWeb.Components.TypeTableComponent do
{type_name,
~H"""
<.link navigate={~p"/type/#{@id}"} class="link">
<%= @name %>
{@name}
</.link>
"""}
end
@ -287,7 +287,7 @@ defmodule CanneryWeb.Components.TypeTableComponent do
assigns = %{actions: actions, type: type}
~H"""
<%= render_slot(@actions, @type) %>
{render_slot(@actions, @type)}
"""
end

View File

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
<%= dgettext("errors", "Error") %> | <%= gettext("Cannery") %>
{dgettext("errors", "Error")} | {gettext("Cannery")}
</title>
<link rel="stylesheet" href="/css/app.css" />
<script defer type="text/javascript" src="/js/app.js">
@ -19,13 +19,13 @@
<div class="pb-8 w-full flex flex-col justify-center items-center text-center">
<div class="p-8 sm:p-16 w-full flex flex-col justify-center items-center space-y-4 max-w-3xl">
<h1 class="title text-primary-600 text-3xl">
<%= @error_string %>
{@error_string}
</h1>
<hr class="w-full hr" />
<.link href={~p"/"} class="link title text-primary-600 text-lg">
<%= dgettext("errors", "Go back home") %>
{dgettext("errors", "Go back home")}
</.link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-600 text-xl">
<%= dgettext("actions", "Resend confirmation instructions") %>
{dgettext("actions", "Resend confirmation instructions")}
</h1>
<.form
@ -10,22 +10,22 @@
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") %>
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
{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")}
<%= submit(dgettext("actions", "Resend confirmation instructions"),
{submit(dgettext("actions", "Resend confirmation instructions"),
class: "mx-auto btn btn-primary col-span-3"
) %>
)}
</.form>
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</.link>
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</.link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-600 text-xl">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</h1>
<.form
@ -10,23 +10,23 @@
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">
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</p>
<%= if @invite_token do %>
<%= hidden_input(f, :invite_token, value: @invite_token) %>
{hidden_input(f, :invite_token, value: @invite_token)}
<% end %>
<%= 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") %>
<%= error_tag(f, :email, "col-span-3") %>
{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")}
{error_tag(f, :email, "col-span-3")}
<%= 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") %>
<%= error_tag(f, :password, "col-span-3") %>
{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")}
{error_tag(f, :password, "col-span-3")}
<%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600") %>
<%= select(
{label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600")}
{select(
f,
:locale,
[
@ -36,20 +36,20 @@
{"Español", "es"}
],
class: "input input-primary col-span-2"
) %>
<%= error_tag(f, :locale) %>
)}
{error_tag(f, :locale)}
<%= submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3") %>
{submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3")}
</.form>
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</.link>
<.link href={~p"/users/reset_password"} class="btn btn-primary">
<%= dgettext("actions", "Forgot your password?") %>
{dgettext("actions", "Forgot your password?")}
</.link>
</div>
</div>

View File

@ -40,7 +40,7 @@ defmodule CanneryWeb.UserResetPasswordController do
# leaked token giving the user access to the account.
def update(conn, %{"user" => user_params}) do
case Accounts.reset_user_password(conn.assigns.user, user_params) do
{:ok, _} ->
{:ok, _socket} ->
conn
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|> redirect(to: ~p"/users/log_in")

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-600 text-xl">
<%= dgettext("actions", "Reset password") %>
{dgettext("actions", "Reset password")}
</h1>
<.form
@ -10,35 +10,35 @@
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">
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</p>
<%= label(f, :password, gettext("New password"), class: "title text-lg text-primary-600") %>
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
<%= error_tag(f, :password, "col-span-3") %>
{label(f, :password, gettext("New password"), class: "title text-lg text-primary-600")}
{password_input(f, :password, required: true, class: "input input-primary col-span-2")}
{error_tag(f, :password, "col-span-3")}
<%= label(f, :password_confirmation, gettext("Confirm new password"),
{label(f, :password_confirmation, gettext("Confirm new password"),
class: "title text-lg text-primary-600"
) %>
<%= password_input(f, :password_confirmation,
)}
{password_input(f, :password_confirmation,
required: true,
class: "input input-primary col-span-2"
) %>
<%= error_tag(f, :password_confirmation, "col-span-3") %>
)}
{error_tag(f, :password_confirmation, "col-span-3")}
<%= submit(dgettext("actions", "Reset password"),
{submit(dgettext("actions", "Reset password"),
class: "mx-auto btn btn-primary col-span-3"
) %>
)}
</.form>
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</.link>
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</.link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-600 text-xl">
<%= dgettext("actions", "Forgot your password?") %>
{dgettext("actions", "Forgot your password?")}
</h1>
<.form
@ -10,22 +10,22 @@
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") %>
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
{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")}
<%= submit(dgettext("actions", "Send instructions to reset password"),
{submit(dgettext("actions", "Send instructions to reset password"),
class: "mx-auto btn btn-primary col-span-3"
) %>
)}
</.form>
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</.link>
<.link href={~p"/users/log_in"} class="btn btn-primary">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</.link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-600 text-xl">
<%= dgettext("actions", "Log in") %>
{dgettext("actions", "Log in")}
</h1>
<.form
@ -11,39 +11,39 @@
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">
<%= @error_message %>
{@error_message}
</p>
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
<%= email_input(f, :email,
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
{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,
{label(f, :password, gettext("Password"), class: "title text-lg text-primary-600")}
{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"),
{label(f, :remember_me, gettext("Keep me logged in for 60 days"),
class: "title text-lg text-primary-600"
) %>
<%= checkbox(f, :remember_me, class: "checkbox col-span-2") %>
)}
{checkbox(f, :remember_me, class: "checkbox col-span-2")}
<%= submit(dgettext("actions", "Log in"), class: "mx-auto btn btn-primary col-span-3") %>
{submit(dgettext("actions", "Log in"), class: "mx-auto btn btn-primary col-span-3")}
</.form>
<hr class="hr" />
<div class="flex flex-row justify-center items-center space-x-4">
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
<%= dgettext("actions", "Register") %>
{dgettext("actions", "Register")}
</.link>
<.link href={~p"/users/reset_password"} class="btn btn-primary">
<%= dgettext("actions", "Forgot your password?") %>
{dgettext("actions", "Forgot your password?")}
</.link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto pb-8 max-w-3xl flex flex-col justify-center items-center text-right space-y-4">
<h1 class="pb-4 title text-primary-600 text-2xl text-center">
<%= gettext("Settings") %>
<div class="flex flex-col justify-center items-center pb-8 mx-auto space-y-4 max-w-3xl text-right">
<h1 class="pb-4 text-2xl text-center title text-primary-600">
{gettext("Settings")}
</h1>
<hr class="hr" />
@ -9,40 +9,40 @@
:let={f}
for={@email_changeset}
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"
class="flex flex-col justify-center items-center space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4"
>
<h3 class="title text-primary-600 text-lg text-center col-span-3">
<%= dgettext("actions", "Change email") %>
<h3 class="col-span-3 text-lg text-center title text-primary-600">
{dgettext("actions", "Change email")}
</h3>
<div
:if={@email_changeset.action && not @email_changeset.valid?}
class="alert alert-danger col-span-3"
class="col-span-3 alert alert-danger"
>
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</div>
<%= hidden_input(f, :action, name: "action", value: "update_email") %>
{hidden_input(f, :action, name: "action", value: "update_email")}
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
<%= email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2") %>
<%= error_tag(f, :email, "col-span-3") %>
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
{email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2")}
{error_tag(f, :email, "col-span-3")}
<%= label(f, :current_password, gettext("Current password"),
{label(f, :current_password, gettext("Current password"),
for: "current_password_for_email",
class: "mx-2 my-1 title text-lg text-primary-600"
) %>
<%= password_input(f, :current_password,
)}
{password_input(f, :current_password,
required: true,
name: "current_password",
id: "current_password_for_email",
class: "mx-2 my-1 input input-primary col-span-2"
) %>
<%= error_tag(f, :current_password, "col-span-3") %>
)}
{error_tag(f, :current_password, "col-span-3")}
<%= submit(dgettext("actions", "Change email"),
{submit(dgettext("actions", "Change email"),
class: "mx-auto btn btn-primary col-span-3"
) %>
)}
</.form>
<hr class="hr" />
@ -51,52 +51,52 @@
:let={f}
for={@password_changeset}
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"
class="flex flex-col justify-center items-center space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4"
>
<h3 class="title text-primary-600 text-lg text-center col-span-3">
<%= dgettext("actions", "Change password") %>
<h3 class="col-span-3 text-lg text-center title text-primary-600">
{dgettext("actions", "Change password")}
</h3>
<div
:if={@password_changeset.action && not @password_changeset.valid?}
class="alert alert-danger col-span-3"
class="col-span-3 alert alert-danger"
>
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</div>
<%= hidden_input(f, :action, name: "action", value: "update_password") %>
{hidden_input(f, :action, name: "action", value: "update_password")}
<%= label(f, :password, gettext("New password"), class: "title text-lg text-primary-600") %>
<%= password_input(f, :password,
{label(f, :password, gettext("New password"), class: "title text-lg text-primary-600")}
{password_input(f, :password,
required: true,
class: "mx-2 my-1 input input-primary col-span-2"
) %>
<%= error_tag(f, :password, "col-span-3") %>
)}
{error_tag(f, :password, "col-span-3")}
<%= label(f, :password_confirmation, gettext("Confirm new password"),
{label(f, :password_confirmation, gettext("Confirm new password"),
class: "title text-lg text-primary-600"
) %>
<%= password_input(f, :password_confirmation,
)}
{password_input(f, :password_confirmation,
required: true,
class: "mx-2 my-1 input input-primary col-span-2"
) %>
<%= error_tag(f, :password_confirmation, "col-span-3") %>
)}
{error_tag(f, :password_confirmation, "col-span-3")}
<%= label(f, :current_password, gettext("Current password"),
{label(f, :current_password, gettext("Current password"),
for: "current_password_for_password",
class: "title text-lg text-primary-600"
) %>
<%= password_input(f, :current_password,
)}
{password_input(f, :current_password,
required: true,
name: "current_password",
id: "current_password_for_password",
class: "mx-2 my-1 input input-primary col-span-2"
) %>
<%= error_tag(f, :current_password, "col-span-3") %>
)}
{error_tag(f, :current_password, "col-span-3")}
<%= submit(dgettext("actions", "Change password"),
{submit(dgettext("actions", "Change password"),
class: "mx-auto btn btn-primary col-span-3"
) %>
)}
</.form>
<hr class="hr" />
@ -105,22 +105,22 @@
:let={f}
for={@locale_changeset}
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"
class="flex flex-col justify-center items-center space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4"
>
<%= label(f, :locale, dgettext("actions", "Change Language"),
{label(f, :locale, dgettext("actions", "Change Language"),
class: "title text-primary-600 text-lg text-center col-span-3"
) %>
)}
<div
:if={@locale_changeset.action && not @locale_changeset.valid?}
class="alert alert-danger col-span-3"
class="col-span-3 alert alert-danger"
>
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</div>
<%= hidden_input(f, :action, name: "action", value: "update_locale") %>
{hidden_input(f, :action, name: "action", value: "update_locale")}
<%= select(
{select(
f,
:locale,
[
@ -129,21 +129,21 @@
{"Français", "fr"},
{"Español", "es"}
],
class: "my-1 min-w-md input input-primary col-span-3"
) %>
<%= error_tag(f, :locale, "col-span-3") %>
class: "my-1 min-w-20 input input-primary col-span-3"
)}
{error_tag(f, :locale, "col-span-3")}
<%= submit(dgettext("actions", "Change language"),
{submit(dgettext("actions", "Change language"),
class: "whitespace-nowrap mx-auto btn btn-primary col-span-3",
data: [qa: dgettext("prompts", "Are you sure you want to change your language?")]
) %>
)}
</.form>
<hr class="hr" />
<div class="flex justify-center items-center">
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
<%= dgettext("actions", "Export Data as JSON") %>
{dgettext("actions", "Export Data as JSON")}
</.link>
<.link
@ -152,7 +152,7 @@
class="mx-4 my-2 btn btn-alert"
data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")}
>
<%= dgettext("actions", "Delete User") %>
{dgettext("actions", "Delete User")}
</.link>
</div>
</div>

View File

@ -4,7 +4,7 @@ defmodule CanneryWeb.ErrorHelpers do
"""
use PhoenixHTMLHelpers
import Phoenix.{Component, HTML.Form}
import Phoenix.Component
alias Ecto.Changeset
alias Phoenix.{HTML.Form, LiveView.Rendered}
@ -19,10 +19,10 @@ defmodule CanneryWeb.ErrorHelpers do
~H"""
<span
:for={error <- Keyword.get_values(@form.errors, @field)}
:if={used_input?(@form[@field])}
class={["invalid-feedback", @extra_class]}
phx-feedback-for={input_name(@form, @field)}
>
<%= translate_error(error) %>
{translate_error(error)}
</span>
"""
end

View File

@ -1,6 +1,6 @@
<div class="flex flex-col justify-center items-center text-center space-y-8">
<h2 class="title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<div class="flex flex-wrap justify-center items-center">
@ -21,13 +21,13 @@
)
}
>
<%= tag.name %>
{tag.name}
<i class="fa-fw fa-sm fas fa-trash"></i>
</.link>
<h2 :if={@container.tags |> Enum.empty?()} class="title text-xl text-primary-600">
<%= gettext("No tags") %>
<%= display_emoji("😔") %>
{gettext("No tags")}
{display_emoji("😔")}
</h2>
</div>
@ -43,17 +43,17 @@
phx-target={@myself}
phx-submit="save"
>
<%= select(f, :tag_id, tag_options(@tags, @container),
{select(f, :tag_id, tag_options(@tags, @container),
class: "text-center col-span-2 input input-primary",
id: "#{@id}-tag-select",
phx_hook: "SlimSelect"
) %>
<%= error_tag(f, :tag_id, "col-span-3 text-center") %>
)}
{error_tag(f, :tag_id, "col-span-3 text-center")}
<%= submit(dgettext("actions", "Add"),
{submit(dgettext("actions", "Add"),
class: "mx-auto btn btn-primary",
phx_disable_with: dgettext("prompts", "Adding...")
) %>
)}
</.form>
<% end %>
</div>

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<.form
:let={f}
@ -15,50 +15,50 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :name,
{label(f, :name, gettext("Name"), class: "title text-lg text-primary-600")}
{text_input(f, :name,
class: "input input-primary col-span-2",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("My cool ammo can")
) %>
<%= error_tag(f, :name, "col-span-3 text-center") %>
)}
{error_tag(f, :name, "col-span-3 text-center")}
<%= label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600") %>
<%= textarea(f, :desc,
{label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600")}
{textarea(f, :desc,
class: "input input-primary col-span-2",
id: "container-form-desc",
phx_debounce: 300,
phx_update: "ignore",
placeholder: gettext("Metal ammo can with the anime girl sticker")
) %>
<%= error_tag(f, :desc, "col-span-3 text-center") %>
)}
{error_tag(f, :desc, "col-span-3 text-center")}
<%= label(f, :type, gettext("Type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :type,
{label(f, :type, gettext("Type"), class: "title text-lg text-primary-600")}
{text_input(f, :type,
class: "input input-primary col-span-2",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Magazine, Clip, Ammo Box, etc")
) %>
<%= error_tag(f, :type, "col-span-3 text-center") %>
)}
{error_tag(f, :type, "col-span-3 text-center")}
<%= label(f, :location, gettext("Location"), class: "title text-lg text-primary-600") %>
<%= textarea(f, :location,
{label(f, :location, gettext("Location"), class: "title text-lg text-primary-600")}
{textarea(f, :location,
class: "input input-primary col-span-2",
id: "container-form-location",
phx_debounce: 300,
phx_update: "ignore",
placeholder: gettext("On the bookshelf")
) %>
<%= error_tag(f, :location, "col-span-3 text-center") %>
)}
{error_tag(f, :location, "col-span-3 text-center")}
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
class: "mx-auto btn btn-primary col-span-3",
phx_disable_with: dgettext("prompts", "Saving...")
) %>
)}
</.form>
</div>

View File

@ -112,6 +112,20 @@ defmodule CanneryWeb.ContainerLive.Index do
{:noreply, socket |> push_patch(to: ~p"/containers/search/#{search_term}")}
end
def handle_event(
"toggle_staged",
%{"container_id" => id},
%{assigns: %{current_user: current_user}} = socket
) do
container = Containers.get_container!(id, current_user)
{:ok, _container} =
container
|> Containers.update_container(current_user, %{"staged" => !container.staged})
{:noreply, socket |> display_containers()}
end
defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do
socket |> assign(:containers, Containers.list_containers(current_user, search: search))
end

View File

@ -1,51 +1,51 @@
<div class="flex flex-col space-y-8 justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= gettext("Containers") %>
<div class="flex flex-col justify-center items-center space-y-8">
<h1 class="text-2xl title title-primary-500">
{gettext("Containers")}
</h1>
<%= if @containers |> Enum.empty?() and @search |> is_nil() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No containers") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No containers")}
{display_emoji("😔")}
</h2>
<.link patch={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "Add your first container!") %>
{dgettext("actions", "Add your first container!")}
</.link>
<% else %>
<.link patch={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "New Container") %>
{dgettext("actions", "New Container")}
</.link>
<div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl">
<div class="flex flex-col flex-wrap justify-center items-center space-y-4 w-full sm:flex-row sm:space-y-0 sm:space-x-4">
<.form
:let={f}
for={%{}}
as={:search}
phx-change="search"
phx-submit="search"
class="grow flex items-center"
class="flex items-center grow"
>
<%= text_input(f, :search_term,
{text_input(f, :search_term,
class: "grow input input-primary",
phx_debounce: 300,
placeholder: gettext("Search containers"),
role: "search",
value: @search
) %>
)}
</.form>
<.toggle_button action="toggle_table" value={@view_table}>
<span class="title text-lg text-primary-600">
<%= gettext("View as table") %>
<span class="text-lg title text-primary-600">
{gettext("View as table")}
</span>
</.toggle_button>
</div>
<%= if @containers |> Enum.empty?() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No containers") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No containers")}
{display_emoji("😔")}
</h2>
<% else %>
<%= if @view_table do %>
@ -56,6 +56,20 @@
containers={@containers}
current_user={@current_user}
>
<:range :let={container}>
<div class="flex flex-wrap justify-center items-center px-4 py-2 h-full min-w-20">
<button
type="button"
class="mx-2 my-1 text-sm btn btn-primary"
phx-click="toggle_staged"
phx-value-container_id={container.id}
>
{if container.staged,
do: dgettext("actions", "Unstage"),
else: dgettext("actions", "Stage")}
</button>
</div>
</:range>
<:tag_actions :let={container}>
<div class="mx-4 my-2">
<.link
@ -109,7 +123,7 @@
</:actions>
</.live_component>
<% else %>
<div class="w-full flex flex-row flex-wrap justify-center items-stretch">
<div class="flex flex-row flex-wrap justify-center items-stretch w-full">
<.container_card
:for={container <- @containers}
container={container}

View File

@ -78,6 +78,18 @@ defmodule CanneryWeb.ContainerLive.Show do
{:noreply, socket}
end
def handle_event(
"toggle_staged",
_params,
%{assigns: %{container: container, current_user: current_user}} = socket
) do
{:ok, _container} =
container
|> Containers.update_container(current_user, %{"staged" => !container.staged})
{:noreply, socket |> render_container()}
end
def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
{:noreply, socket |> assign(:view_table, !view_table) |> render_container()}
end

View File

@ -1,34 +1,34 @@
<div class="space-y-4 flex flex-col justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= @container.name %>
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="text-2xl title title-primary-500">
{@container.name}
</h1>
<span :if={@container.desc} class="rounded-lg title text-lg">
<%= gettext("Description:") %>
<%= @container.desc %>
<span :if={@container.desc} class="text-lg rounded-lg title">
{gettext("Description:")}
{@container.desc}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Type:") %>
<%= @container.type %>
<span class="text-lg rounded-lg title">
{gettext("Type:")}
{@container.type}
</span>
<span :if={@container.location} class="rounded-lg title text-lg">
<%= gettext("Location:") %>
<%= @container.location %>
<span :if={@container.location} class="text-lg rounded-lg title">
{gettext("Location:")}
{@container.location}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Packs:") %>
<%= @packs_count %>
<span class="text-lg rounded-lg title">
{gettext("Packs:")}
{@packs_count}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Rounds:") %>
<%= @round_count %>
<span class="text-lg rounded-lg title">
{gettext("Rounds:")}
{@round_count}
</span>
<div class="flex space-x-4 justify-center items-center text-primary-600">
<div class="flex justify-center items-center space-x-4 text-primary-600">
<.link
patch={~p"/container/edit/#{@container}"}
class="text-primary-600 link"
@ -52,17 +52,25 @@
</.link>
</div>
<div class="flex flex-wrap justify-center items-center text-primary-600">
<button type="button" class="mx-4 my-2 btn btn-primary" phx-click="toggle_staged">
{if @container.staged,
do: dgettext("actions", "Unstage from range"),
else: dgettext("actions", "Stage for range")}
</button>
</div>
<hr class="mb-4 hr" />
<%= if @container.tags |> Enum.empty?() do %>
<div class="flex flex-row justify-center items-center space-x-4">
<h2 class="title text-lg text-primary-600">
<%= gettext("No tags for this container") %>
<%= display_emoji("😔") %>
<h2 class="text-lg title text-primary-600">
{gettext("No tags for this container")}
{display_emoji("😔")}
</h2>
<.link patch={~p"/container/edit_tags/#{@container}"} class="btn btn-primary">
<%= dgettext("actions", "Why not add one?") %>
{dgettext("actions", "Why not add one?")}
</.link>
</div>
<% else %>
@ -88,9 +96,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(
{select(
f,
:class,
[
@ -99,22 +107,22 @@
{gettext("Shotgun"), :shotgun},
{gettext("Pistol"), :pistol}
],
class: "mx-2 my-1 min-w-md input input-primary",
class: "mx-2 my-1 min-w-20 input input-primary",
value: @class
) %>
)}
</.form>
<.toggle_button action="toggle_table" value={@view_table}>
<span class="title text-lg text-primary-600">
<%= gettext("View as table") %>
<span class="text-lg title text-primary-600">
{gettext("View as table")}
</span>
</.toggle_button>
</div>
<div class="w-full p-4">
<div class="p-4 w-full">
<%= if @packs |> Enum.empty?() do %>
<h2 class="mx-4 title text-lg text-primary-600 text-center">
<%= gettext("No ammo in this container") %>
<h2 class="mx-4 text-lg text-center title text-primary-600">
{gettext("No ammo in this container")}
</h2>
<% else %>
<%= if @view_table do %>
@ -127,11 +135,11 @@
>
<:type :let={%{name: type_name} = type}>
<.link navigate={~p"/type/#{type}"} class="link">
<%= type_name %>
{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">
<div class="flex justify-center items-center px-4 py-2 space-x-4 h-full">
<.link
navigate={~p"/ammo/show/#{pack}"}
class="text-primary-600 link"

View File

@ -1,17 +1,17 @@
<div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl">
<div class="flex flex-col justify-center items-center px-8 mx-auto space-y-4 max-w-3xl text-center sm:px-16">
<img
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"
class="inline-block pt-2 pb-0 mb-8 w-32 transition-all duration-500 ease-in-out hover:pt-0 hover:pb-2"
title={gettext("isn't he cute >:3")}
/>
<h1 class="title text-primary-600 text-2xl">
<%= gettext("Welcome to Cannery") %>
<h1 class="text-2xl title text-primary-600">
{gettext("Welcome to Cannery")}
</h1>
<h2 class="title text-primary-600 text-lg">
<%= gettext("The self-hosted firearm tracker website") %>
<h2 class="text-lg title text-primary-600">
{gettext("The self-hosted firearm tracker website")}
</h2>
<hr class="hr" />
@ -19,48 +19,48 @@
<ul class="flex flex-col space-y-4 text-center">
<li class="flex flex-col justify-center items-center space-y-2">
<b class="whitespace-nowrap">
<%= gettext("Easy to Use:") %>
{gettext("Easy to Use:")}
</b>
<p>
<%= gettext(
{gettext(
"Cannery lets you easily keep an eye on your ammo levels before and after range day"
) %>
)}
</p>
</li>
<li class="flex flex-col justify-center items-center space-y-2">
<b class="whitespace-nowrap">
<%= gettext("Secure:") %>
{gettext("Secure:")}
</b>
<p>
<%= gettext("Self-host your own instance, or use an instance from someone you trust.") %>
<%= gettext("Your data stays with you, period") %>
{gettext("Self-host your own instance, or use an instance from someone you trust.")}
{gettext("Your data stays with you, period")}
</p>
</li>
<li class="flex flex-col justify-center items-center space-y-2">
<b class="whitespace-nowrap">
<%= gettext("Simple:") %>
{gettext("Simple:")}
</b>
<p>
<%= gettext("Access from any internet-capable device") %>
{gettext("Access from any internet-capable device")}
</p>
</li>
</ul>
<hr class="hr" />
<ul class="flex flex-col space-y-2 text-center justify-center">
<h2 class="title text-primary-600 text-lg">
<%= gettext("Instance Information") %>
<ul class="flex flex-col justify-center space-y-2 text-center">
<h2 class="text-lg title text-primary-600">
{gettext("Instance Information")}
</h2>
<li class="flex flex-col justify-center space-x-2">
<b>
<%= gettext("Admins:") %>
{gettext("Admins:")}
</b>
<p>
<%= if @admins |> Enum.empty?() do %>
<.link href={~p"/users/register"} class="hover:underline">
<%= dgettext("prompts", "Register to setup Cannery") %>
{dgettext("prompts", "Register to setup Cannery")}
</.link>
<% else %>
<div class="flex flex-wrap justify-center space-x-2">
@ -69,7 +69,7 @@
class="hover:underline"
href={"mailto:#{email}"}
>
<%= email %>
{email}
</.link>
</div>
<% end %>
@ -77,17 +77,17 @@
</li>
<li class="flex flex-row justify-center space-x-2">
<b><%= gettext("Registration:") %></b>
<b>{gettext("Registration:")}</b>
<p>
<%= case Accounts.registration_mode() do
{case Accounts.registration_mode() do
:public -> gettext("Public Signups")
:invite_only -> gettext("Invite Only")
end %>
end}
</p>
</li>
<li class="flex flex-row justify-center items-center space-x-2">
<b><%= gettext("Version:") %></b>
<b>{gettext("Version:")}</b>
<.link
href="https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/CHANGELOG.md"
class="flex flex-row justify-center items-center space-x-2 hover:underline"
@ -95,7 +95,7 @@
rel="noopener noreferrer"
>
<p>
<%= @version %>
{@version}
</p>
<i class="fas fa-md fa-info-circle"></i>
</.link>
@ -104,9 +104,9 @@
<hr class="hr" />
<ul class="flex flex-col space-y-2 text-center justify-center">
<h2 class="title text-primary-600 text-lg">
<%= gettext("Get involved!") %>
<ul class="flex flex-col justify-center space-y-2 text-center">
<h2 class="text-lg title text-primary-600">
{gettext("Get involved!")}
</h2>
<li class="flex flex-col justify-center space-x-2">
@ -116,7 +116,7 @@
target="_blank"
rel="noopener noreferrer"
>
<p><%= gettext("View the source code") %></p>
<p>{gettext("View the source code")}</p>
<i class="fas fa-md fa-code"></i>
</.link>
</li>
@ -127,7 +127,7 @@
target="_blank"
rel="noopener noreferrer"
>
<p><%= gettext("Help translate") %></p>
<p>{gettext("Help translate")}</p>
<i class="fas fa-md fa-language"></i>
</.link>
</li>
@ -138,7 +138,7 @@
target="_blank"
rel="noopener noreferrer"
>
<p><%= gettext("Report bugs or request features") %></p>
<p>{gettext("Report bugs or request features")}</p>
<i class="fas fa-md fa-spider"></i>
</.link>
</li>

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<.form
:let={f}
@ -15,29 +15,29 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :name, gettext("Name"),
{label(f, :name, gettext("Name"),
class: "title text-lg text-primary-600",
maxlength: 255
) %>
<%= text_input(f, :name,
)}
{text_input(f, :name,
class: "input input-primary col-span-2",
phx_debounce: 300
) %>
<%= error_tag(f, :name, "col-span-3") %>
)}
{error_tag(f, :name, "col-span-3")}
<%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %>
<%= error_tag(f, :uses_left, "col-span-3") %>
{label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600")}
{number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2")}
{error_tag(f, :uses_left, "col-span-3")}
<span class="col-span-3 text-primary-400 italic text-center">
<%= gettext(~s/Leave "Uses left" blank to make invite unlimited/) %>
{gettext(~s/Leave "Uses left" blank to make invite unlimited/)}
</span>
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
class: "mx-auto btn btn-primary col-span-3",
phx_disable_with: dgettext("prompts", "Saving...")
) %>
)}
</.form>
</div>

View File

@ -93,7 +93,7 @@ defmodule CanneryWeb.InviteLive.Index do
%{"id" => id},
%{assigns: %{current_user: current_user}} = socket
) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
now = DateTime.utc_now()
socket =
Invites.get_invite!(id, current_user)
@ -116,6 +116,20 @@ defmodule CanneryWeb.InviteLive.Index do
{:noreply, socket |> put_flash(:info, dgettext("prompts", "Copied to clipboard"))}
end
def handle_event("resend_email_verification", %{"id" => id}, socket) do
%{email: user_email} = user = Accounts.get_user!(id)
Accounts.deliver_user_confirmation_instructions(
user,
fn token -> url(CanneryWeb.Endpoint, ~p"/users/confirm/#{token}") end
)
prompt =
dgettext("prompts", "Email resent to %{user_email} succesfully", user_email: user_email)
{:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
end
def handle_event(
"delete_user",
%{"id" => id},

View File

@ -1,20 +1,20 @@
<div class="mx-auto flex flex-col justify-center items-center space-y-4 max-w-3xl">
<h1 class="title text-2xl title-primary-500">
<%= gettext("Invites") %>
<div class="flex flex-col justify-center items-center mx-auto space-y-4 max-w-3xl">
<h1 class="text-2xl title title-primary-500">
{gettext("Invites")}
</h1>
<%= if @invites |> Enum.empty?() do %>
<h1 class="title text-xl text-primary-600">
<%= gettext("No invites") %>
<%= display_emoji("😔") %>
<h1 class="text-xl title text-primary-600">
{gettext("No invites")}
{display_emoji("😔")}
</h1>
<.link patch={~p"/invites/new"} class="btn btn-primary">
<%= dgettext("actions", "Invite someone new!") %>
{dgettext("actions", "Invite someone new!")}
</.link>
<% else %>
<.link patch={~p"/invites/new"} class="btn btn-primary">
<%= dgettext("actions", "Create Invite") %>
{dgettext("actions", "Create Invite")}
</.link>
<% end %>
@ -35,7 +35,7 @@
dgettext("actions", "Copy invite link for %{invite_name}", invite_name: invite.name)
}
>
<%= dgettext("actions", "Copy to clipboard") %>
{dgettext("actions", "Copy to clipboard")}
</button>
</form>
</:code_actions>
@ -72,7 +72,7 @@
phx-click={if invite.disabled_at, do: "enable_invite", else: "disable_invite"}
phx-value-id={invite.id}
>
<%= if invite.disabled_at, do: gettext("Enable"), else: gettext("Disable") %>
{if invite.disabled_at, do: gettext("Enable"), else: gettext("Disable")}
</.link>
<.link
@ -87,7 +87,7 @@
)
}
>
<%= dgettext("actions", "Set Unlimited") %>
{dgettext("actions", "Set Unlimited")}
</.link>
</.invite_card>
</div>
@ -95,8 +95,8 @@
<%= unless @admins |> Enum.empty?() do %>
<hr class="hr" />
<h1 class="title text-2xl text-primary-600">
<%= gettext("Admins") %>
<h1 class="text-2xl title text-primary-600">
{gettext("Admins")}
</h1>
<div class="flex flex-col justify-center items-stretch space-y-4">
@ -123,27 +123,38 @@
<%= unless @users |> Enum.empty?() do %>
<hr class="hr" />
<h1 class="title text-2xl text-primary-600">
<%= gettext("Users") %>
<h1 class="text-2xl title text-primary-600">
{gettext("Users")}
</h1>
<div class="flex flex-col justify-center items-stretch space-y-4">
<.user_card :for={user <- @users} user={user}>
<.link
href="#"
class="text-primary-600 link"
phx-click="delete_user"
phx-value-id={user.id}
data-confirm={
dgettext(
"prompts",
"Are you sure you want to delete %{email}? This action is permanent!",
email: user.email
)
}
>
<i class="fa-fw fa-lg fas fa-trash"></i>
</.link>
<div class="flex justify-center items-center space-x-2">
<.link
:if={!user.confirmed_at}
class="text-primary-600 link"
href="#"
phx-click="resend_email_verification"
phx-value-id={user.id}
>
<i class="fa-fw fa-lg fas fa-paper-plane"></i>
</.link>
<.link
class="text-primary-600 link"
data-confirm={
dgettext(
"prompts",
"Are you sure you want to delete %{email}? This action is permanent!",
email: user.email
)
}
href="#"
phx-click="delete_user"
phx-value-id={user.id}
>
<i class="fa-fw fa-lg fas fa-trash"></i>
</.link>
</div>
</.user_card>
</div>
<% end %>

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<.form
@ -16,11 +16,11 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :class, gettext("Class"), class: "title text-lg text-primary-600") %>
<%= select(
{label(f, :class, gettext("Class"), class: "title text-lg text-primary-600")}
{select(
f,
:class,
[
@ -31,86 +31,86 @@
],
class: "text-center col-span-2 input input-primary",
value: @class
) %>
<%= error_tag(f, :class, "col-span-3 text-center") %>
)}
{error_tag(f, :class, "col-span-3 text-center")}
<%= label(f, :type_id, gettext("Type"), class: "title text-lg text-primary-600") %>
<%= select(f, :type_id, type_options(@types, @class),
{label(f, :type_id, gettext("Type"), class: "title text-lg text-primary-600")}
{select(f, :type_id, type_options(@types, @class),
class: "text-center col-span-2 input input-primary",
id: "pack-form-type-select",
phx_hook: "SlimSelect"
) %>
<%= error_tag(f, :type_id, "col-span-3 text-center") %>
)}
{error_tag(f, :type_id, "col-span-3 text-center")}
<%= label(f, :count, gettext("Count"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :count,
{label(f, :count, gettext("Count"), class: "title text-lg text-primary-600")}
{number_input(f, :count,
class: "text-center col-span-2 input input-primary",
min: 0
) %>
<%= error_tag(f, :count, "col-span-3 text-center") %>
)}
{error_tag(f, :count, "col-span-3 text-center")}
<%= label(f, :price_paid, gettext("Price paid"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :price_paid,
{label(f, :price_paid, gettext("Price paid"), class: "title text-lg text-primary-600")}
{number_input(f, :price_paid,
step: 0.01,
class: "text-center col-span-2 input input-primary"
) %>
<%= error_tag(f, :price_paid, "col-span-3 text-center") %>
)}
{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,
{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,
phx_debounce: 300
) %>
<%= error_tag(f, :price_paid, "col-span-3 text-center") %>
)}
{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,
{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",
phx_update: "ignore",
value: @changeset |> Changeset.get_field(:purchased_on) || Date.utc_today()
) %>
<%= error_tag(f, :purchased_on, "col-span-3 text-center") %>
)}
{error_tag(f, :purchased_on, "col-span-3 text-center")}
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
<%= textarea(f, :notes,
{label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600")}
{textarea(f, :notes,
class: "text-center col-span-2 input input-primary",
id: "pack-form-notes",
phx_debounce: 300,
phx_update: "ignore"
) %>
<%= error_tag(f, :notes, "col-span-3 text-center") %>
)}
{error_tag(f, :notes, "col-span-3 text-center")}
<%= label(f, :container, gettext("Container"), class: "title text-lg text-primary-600") %>
<%= select(f, :container_id, container_options(@containers),
{label(f, :container, gettext("Container"), class: "title text-lg text-primary-600")}
{select(f, :container_id, container_options(@containers),
class: "text-center col-span-2 input input-primary",
id: "pack-form-container-select",
phx_hook: "SlimSelect"
) %>
<%= error_tag(f, :container_id, "col-span-3 text-center") %>
)}
{error_tag(f, :container_id, "col-span-3 text-center")}
<%= case @action do %>
<% action when action in [:new, :clone] -> %>
<hr class="hr col-span-3" />
<%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :multiplier,
{label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600")}
{number_input(f, :multiplier,
class: "text-center input input-primary",
value: 1,
phx_update: "ignore"
) %>
)}
<%= submit(dgettext("actions", "Create"),
{submit(dgettext("actions", "Create"),
phx_disable_with: dgettext("prompts", "Creating..."),
class: "mx-auto btn btn-primary"
) %>
)}
<%= error_tag(f, :multiplier, "col-span-3 text-center") %>
{error_tag(f, :multiplier, "col-span-3 text-center")}
<% :edit -> %>
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
phx_disable_with: dgettext("prompts", "Saving..."),
class: "mx-auto col-span-3 btn btn-primary"
) %>
)}
<% end %>
</.form>
</div>

View File

@ -96,18 +96,6 @@ defmodule CanneryWeb.PackLive.Index do
{:noreply, socket |> put_flash(:info, prompt) |> display_packs()}
end
def handle_event(
"toggle_staged",
%{"pack_id" => id},
%{assigns: %{current_user: current_user}} = socket
) do
pack = Ammo.get_pack!(id, current_user)
{:ok, _pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user)
{:noreply, socket |> display_packs()}
end
def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
{:noreply, socket |> assign(:show_used, !show_used) |> display_packs()}
end

View File

@ -1,44 +1,44 @@
<div class="flex flex-col space-y-8 justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= gettext("Ammo") %>
<div class="flex flex-col justify-center items-center space-y-8">
<h1 class="text-2xl title title-primary-500">
{gettext("Ammo")}
</h1>
<%= cond do %>
<% @containers_count == 0 -> %>
<div class="flex justify-center items-center">
<h2 class="m-2 title text-md text-primary-600">
<%= dgettext("prompts", "You'll need to") %>
{dgettext("prompts", "You'll need to")}
</h2>
<.link navigate={~p"/containers/new"} class="btn btn-primary">
<%= dgettext("actions", "add a container first") %>
{dgettext("actions", "add a container first")}
</.link>
</div>
<% @types_count == 0 -> %>
<div class="flex justify-center items-center">
<h2 class="m-2 title text-md text-primary-600">
<%= dgettext("prompts", "You'll need to") %>
{dgettext("prompts", "You'll need to")}
</h2>
<.link navigate={~p"/catalog/new"} class="btn btn-primary">
<%= dgettext("actions", "add a type first") %>
{dgettext("actions", "add a type first")}
</.link>
</div>
<% @packs_count == 0 -> %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No ammo") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No ammo")}
{display_emoji("😔")}
</h2>
<.link patch={~p"/ammo/new"} class="btn btn-primary">
<%= dgettext("actions", "Add your first box!") %>
{dgettext("actions", "Add your first box!")}
</.link>
<% true -> %>
<.link patch={~p"/ammo/new"} class="btn btn-primary">
<%= dgettext("actions", "Add Ammo") %>
{dgettext("actions", "Add Ammo")}
</.link>
<div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl">
<div class="flex flex-col flex-wrap justify-center items-center space-y-4 w-full sm:flex-row sm:space-y-0 sm:space-x-4">
<.form
:let={f}
for={%{}}
@ -47,11 +47,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(
{select(
f,
:class,
[
@ -60,9 +58,9 @@
{gettext("Shotgun"), :shotgun},
{gettext("Pistol"), :pistol}
],
class: "mx-2 my-1 min-w-md input input-primary",
class: "mx-2 my-1 min-w-20 input input-primary",
value: @class
) %>
)}
</.form>
<.form
@ -71,28 +69,28 @@
as={:search}
phx-change="search"
phx-submit="search"
class="grow flex items-center"
class="flex items-center grow"
>
<%= text_input(f, :search_term,
{text_input(f, :search_term,
class: "grow input input-primary",
phx_debounce: 300,
placeholder: gettext("Search ammo"),
role: "search",
value: @search
) %>
)}
</.form>
<.toggle_button action="toggle_show_used" value={@show_used}>
<span class="title text-lg text-primary-600">
<%= gettext("Show used") %>
<span class="text-lg title text-primary-600">
{gettext("Show used")}
</span>
</.toggle_button>
</div>
<%= if @packs |> Enum.empty?() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No Ammo") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No Ammo")}
{display_emoji("😔")}
</h2>
<% else %>
<.live_component
@ -104,43 +102,32 @@
>
<:type :let={%{name: type_name} = type}>
<.link navigate={~p"/type/#{type}"} class="link">
<%= type_name %>
{type_name}
</.link>
</:type>
<:range :let={pack}>
<div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center">
<button
type="button"
class="mx-2 my-1 text-sm btn btn-primary"
phx-click="toggle_staged"
phx-value-pack_id={pack.id}
>
<%= if pack.staged,
do: dgettext("actions", "Unstage"),
else: dgettext("actions", "Stage") %>
</button>
<div class="flex flex-wrap justify-center items-center px-4 py-2 h-full min-w-20">
<.link
patch={~p"/ammo/add_shot_record/#{pack}"}
class="mx-2 my-1 text-sm btn btn-primary"
>
<%= dgettext("actions", "Record shots") %>
{dgettext("actions", "Record shots")}
</.link>
</div>
</: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">
<div class="flex flex-wrap justify-center items-center px-4 py-2 h-full min-w-20">
<.link navigate={~p"/container/#{container}"} class="mx-2 my-1 link">
<%= container_name %>
{container_name}
</.link>
<.link patch={~p"/ammo/move/#{pack}"} class="mx-2 my-1 text-sm btn btn-primary">
<%= dgettext("actions", "Move ammo") %>
{dgettext("actions", "Move ammo")}
</.link>
</div>
</:container>
<:actions :let={%{count: pack_count} = pack}>
<div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
<div class="flex justify-center items-center px-4 py-2 space-x-4 h-full">
<.link
navigate={~p"/ammo/show/#{pack}"}
class="text-primary-600 link"

View File

@ -57,22 +57,12 @@ defmodule CanneryWeb.PackLive.Show do
{:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
end
def handle_event(
"toggle_staged",
_params,
%{assigns: %{pack: pack, current_user: current_user}} = socket
) do
{:ok, pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user)
{:noreply, socket |> display_pack(pack)}
end
def handle_event(
"delete_shot_record",
%{"id" => id},
%{assigns: %{pack: %{id: pack_id}, current_user: current_user}} = socket
) do
{:ok, _} =
{:ok, _shot_record} =
ActivityLog.get_shot_record!(id, current_user)
|> ActivityLog.delete_shot_record(current_user)
@ -136,7 +126,7 @@ defmodule CanneryWeb.PackLive.Show do
:actions ->
~H"""
<div class="px-4 py-2 space-x-4 flex justify-center items-center">
<div class="flex justify-center items-center px-4 py-2 space-x-4">
<.link
patch={~p"/ammo/show/#{@pack}/edit/#{@shot_record}"}
class="text-primary-600 link"

View File

@ -1,47 +1,47 @@
<div class="mx-auto space-y-4 max-w-3xl flex flex-col justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= @pack.type.name %>
<div class="flex flex-col justify-center items-center mx-auto space-y-4 max-w-3xl">
<h1 class="text-2xl title title-primary-500">
{@pack.type.name}
</h1>
<div class="space-y-2 flex flex-col justify-center items-center">
<span class="rounded-lg title text-lg">
<%= gettext("Count:") %>
<%= @pack.count %>
<div class="flex flex-col justify-center items-center space-y-2">
<span class="text-lg rounded-lg title">
{gettext("Count:")}
{@pack.count}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Original count:") %>
<%= @original_count %>
<span class="text-lg rounded-lg title">
{gettext("Original count:")}
{@original_count}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Percentage left:") %>
<%= gettext("%{percentage}%", percentage: @percentage_remaining) %>
<span class="text-lg rounded-lg title">
{gettext("Percentage left:")}
{gettext("%{percentage}%", percentage: @percentage_remaining)}
</span>
<%= if @pack.notes do %>
<span class="rounded-lg title text-lg">
<%= gettext("Notes:") %>
<%= @pack.notes %>
<span class="text-lg rounded-lg title">
{gettext("Notes:")}
{@pack.notes}
</span>
<% end %>
<span class="rounded-lg title text-lg">
<%= gettext("Purchased on:") %>
<span class="text-lg rounded-lg title">
{gettext("Purchased on:")}
<.date id={"#{@pack.id}-purchased-on"} date={@pack.purchased_on} />
</span>
<%= if @pack.price_paid do %>
<span class="rounded-lg title text-lg">
<%= gettext("Original cost:") %>
<%= gettext("$%{amount}", amount: display_currency(@pack.price_paid)) %>
<span class="text-lg rounded-lg title">
{gettext("Original cost:")}
{gettext("$%{amount}", amount: display_currency(@pack.price_paid))}
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Current value:") %>
<%= gettext("$%{amount}",
<span class="text-lg rounded-lg title">
{gettext("Current value:")}
{gettext("$%{amount}",
amount: display_currency(@pack.price_paid * @percentage_remaining / 100)
) %>
)}
</span>
<% end %>
</div>
@ -49,7 +49,7 @@
<div class="flex flex-col justify-center items-center">
<div class="flex flex-wrap justify-center items-center text-primary-600">
<.link navigate={~p"/type/#{@pack.type}"} class="mx-4 my-2 btn btn-primary">
<%= dgettext("actions", "View in Catalog") %>
{dgettext("actions", "View in Catalog")}
</.link>
<.link
@ -76,18 +76,12 @@
</div>
<div class="flex flex-wrap justify-center items-center text-primary-600">
<button type="button" class="mx-4 my-2 btn btn-primary" phx-click="toggle_staged">
<%= if @pack.staged,
do: dgettext("actions", "Unstage from range"),
else: dgettext("actions", "Stage for range") %>
</button>
<.link patch={~p"/ammo/show/move/#{@pack}"} class="btn btn-primary">
<%= dgettext("actions", "Move ammo") %>
{dgettext("actions", "Move ammo")}
</.link>
<.link patch={~p"/ammo/show/add_shot_record/#{@pack}"} class="mx-4 my-2 btn btn-primary">
<%= dgettext("actions", "Record shots") %>
{dgettext("actions", "Record shots")}
</.link>
</div>
</div>
@ -96,21 +90,21 @@
<div>
<%= if @container do %>
<h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl">
<%= gettext("Stored in") %>
<h1 class="px-4 py-2 mb-4 text-xl text-center rounded-lg title">
{gettext("Stored in")}
</h1>
<.container_card container={@container} current_user={@current_user} />
<% else %>
<%= gettext("This ammo is not in a container") %>
{gettext("This ammo is not in a container")}
<% end %>
</div>
<%= unless @shot_records |> Enum.empty?() do %>
<hr class="mb-4 w-full" />
<h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl">
<%= gettext("Rounds used") %>
<h1 class="px-4 py-2 mb-4 text-xl text-center rounded-lg title">
{gettext("Rounds used")}
</h1>
<.live_component

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<.form
@ -16,35 +16,35 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :count, gettext("Shots fired"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :count,
{label(f, :count, gettext("Shots fired"), class: "title text-lg text-primary-600")}
{number_input(f, :count,
min: 1,
max: @shot_record.count + @pack.count,
class: "input input-primary col-span-2"
) %>
<%= error_tag(f, :count, "col-span-3") %>
)}
{error_tag(f, :count, "col-span-3")}
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
<%= textarea(f, :notes,
{label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600")}
{textarea(f, :notes,
class: "input input-primary col-span-2",
id: "shot-record-form-notes",
maxlength: 255,
phx_debounce: 300,
phx_update: "ignore",
placeholder: gettext("Really great weather")
) %>
<%= error_tag(f, :notes, "col-span-3") %>
)}
{error_tag(f, :notes, "col-span-3")}
<%= label(f, :date, gettext("Date"), class: "title text-lg text-primary-600") %>
<%= date_input(f, :date, class: "input input-primary col-span-2") %>
<%= error_tag(f, :notes, "col-span-3") %>
{label(f, :date, gettext("Date"), class: "title text-lg text-primary-600")}
{date_input(f, :date, class: "input input-primary col-span-2")}
{error_tag(f, :notes, "col-span-3")}
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
class: "mx-auto btn btn-primary col-span-3",
phx_disable_with: dgettext("prompts", "Saving...")
) %>
)}
</.form>
</div>

View File

@ -4,16 +4,37 @@ defmodule CanneryWeb.RangeLive.Index do
"""
use CanneryWeb, :live_view
alias Cannery.{ActivityLog, ActivityLog.ShotRecord, Ammo}
alias Cannery.{ActivityLog, ActivityLog.ShotRecord}
alias Cannery.{Ammo, Containers}
alias Phoenix.LiveView.Socket
@impl true
def mount(%{"search" => search}, _session, socket) do
{:ok, socket |> assign(class: :all, search: search) |> display_shot_records()}
socket =
socket
|> assign(
class: :all,
start_date: Date.shift(Date.utc_today(), year: -1),
end_date: Date.utc_today(),
search: search
)
|> display_shot_records()
{:ok, socket}
end
def mount(_params, _session, socket) do
{:ok, socket |> assign(class: :all, search: nil) |> display_shot_records()}
socket =
socket
|> assign(
class: :all,
start_date: Date.shift(Date.utc_today(), year: -1),
end_date: Date.utc_today(),
search: nil
)
|> display_shot_records()
{:ok, socket}
end
@impl true
@ -71,7 +92,7 @@ defmodule CanneryWeb.RangeLive.Index do
@impl true
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
{:ok, _} =
{:ok, _shot_record} =
ActivityLog.get_shot_record!(id, current_user)
|> ActivityLog.delete_shot_record(current_user)
@ -81,14 +102,16 @@ defmodule CanneryWeb.RangeLive.Index do
def handle_event(
"toggle_staged",
%{"pack_id" => pack_id},
%{"container_id" => container_id},
%{assigns: %{current_user: current_user}} = socket
) do
pack = Ammo.get_pack!(pack_id, current_user)
container = Containers.get_container!(container_id, current_user)
{:ok, _pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user)
{:ok, _container} =
container
|> Containers.update_container(current_user, %{"staged" => !container.staged})
prompt = dgettext("prompts", "Ammo unstaged succesfully")
prompt = dgettext("prompts", "Container unstaged succesfully")
{:noreply, socket |> put_flash(:info, prompt) |> display_shot_records()}
end
@ -116,11 +139,49 @@ defmodule CanneryWeb.RangeLive.Index do
{:noreply, socket |> assign(:class, :all) |> display_shot_records()}
end
def handle_event(
"change_dates",
%{
"dates_start" => start_date,
"dates_end" => end_date
},
socket
) do
socket =
socket
|> assign(
start_date: start_date,
end_date: end_date
)
|> display_shot_records()
{:noreply, socket}
end
@spec display_shot_records(Socket.t()) :: Socket.t()
defp display_shot_records(
%{assigns: %{class: class, search: search, current_user: current_user}} = socket
%{
assigns: %{
class: class,
start_date: start_date,
end_date: end_date,
search: search,
current_user: current_user
}
} = socket
) do
shot_records = ActivityLog.list_shot_records(current_user, search: search, class: class)
shot_records =
ActivityLog.list_shot_records(current_user,
class: class,
end_date: end_date,
search: search,
start_date: start_date
)
containers =
Containers.list_containers(current_user, staged: true)
|> Map.new(fn container = %{id: container_id} -> {container_id, container} end)
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)
@ -130,6 +191,7 @@ defmodule CanneryWeb.RangeLive.Index do
socket
|> assign(
containers: containers,
packs: packs,
original_counts: original_counts,
cprs: cprs,
@ -153,6 +215,5 @@ defmodule CanneryWeb.RangeLive.Index do
label: gettext("Rounds shot: %{count}", count: sum)
}
end)
|> Enum.sort_by(fn %{date: date} -> date end, Date)
end
end

View File

@ -1,45 +1,57 @@
<div class="flex flex-col space-y-8 justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= gettext("Range day") %>
<div class="flex flex-col justify-center items-center space-y-8">
<h1 class="text-2xl title title-primary-500">
{gettext("Range day")}
</h1>
<%= if @packs |> Enum.empty?() do %>
<h1 class="title text-xl text-primary-600">
<%= gettext("No ammo staged") %>
<%= display_emoji("😔") %>
<%= if @containers |> Enum.empty?() do %>
<h1 class="text-xl title text-primary-600">
{gettext("No containers staged")}
{display_emoji("😔")}
</h1>
<.link navigate={~p"/ammo"} class="btn btn-primary">
<%= dgettext("actions", "Why not get some ready to shoot?") %>
<.link navigate={~p"/containers"} class="btn btn-primary">
{dgettext("actions", "Why not get some ready to shoot?")}
</.link>
<% else %>
<.link navigate={~p"/ammo"} class="btn btn-primary">
<%= dgettext("actions", "Stage ammo") %>
<.link navigate={~p"/containers"} class="btn btn-primary">
{dgettext("actions", "Stage containers")}
</.link>
<div class="w-full flex flex-row flex-wrap justify-center items-stretch">
<div class="flex flex-row flex-wrap justify-center items-stretch w-full">
<.container_card
:for={{container_id, container} <- @containers}
container={container}
current_user={@current_user}
>
<div class="flex flex-wrap justify-center items-center px-4 py-2 h-full min-w-20">
<button
type="button"
class="mx-2 my-1 text-sm btn btn-primary"
phx-click="toggle_staged"
phx-value-container_id={container_id}
>
{if container.staged,
do: dgettext("actions", "Unstage"),
else: dgettext("actions", "Stage")}
</button>
</div>
</.container_card>
</div>
<hr class="hr" />
<div class="flex flex-row flex-wrap justify-center items-stretch w-full">
<.pack_card
:for={%{id: pack_id} = pack <- @packs}
:for={%{id: pack_id, container_id: container_id} = pack <- @packs}
pack={pack}
original_count={Map.fetch!(@original_counts, pack_id)}
cpr={Map.get(@cprs, pack_id)}
last_used_date={Map.get(@last_used_dates, pack_id)}
current_user={@current_user}
container={Map.fetch!(@containers, container_id)}
>
<button
type="button"
class="btn btn-primary"
phx-click="toggle_staged"
phx-value-pack_id={pack.id}
data-confirm={"#{dgettext("prompts", "Are you sure you want to unstage this ammo?")}"}
>
<%= if pack.staged,
do: dgettext("actions", "Unstage from range"),
else: dgettext("actions", "Stage for range") %>
</button>
<.link patch={~p"/range/add_shot_record/#{pack}"} class="btn btn-primary">
<%= dgettext("actions", "Record shots") %>
{dgettext("actions", "Record shots")}
</.link>
</.pack_card>
</div>
@ -48,13 +60,13 @@
<hr class="hr" />
<%= if @shot_record_count == 0 do %>
<h1 class="title text-xl text-primary-600">
<%= gettext("No shots recorded") %>
<%= display_emoji("😔") %>
<h1 class="text-xl title text-primary-600">
{gettext("No shots recorded")}
{display_emoji("😔")}
</h1>
<% else %>
<h1 class="title text-2xl text-primary-600">
<%= gettext("Shot log") %>
<h1 class="text-2xl title text-primary-600">
{gettext("Shot log")}
</h1>
<canvas
@ -68,10 +80,10 @@
aria-label={gettext("Rounds shot chart")}
role="img"
>
<%= dgettext("errors", "Your browser does not support the canvas element.") %>
{dgettext("errors", "Your browser does not support the canvas element.")}
</canvas>
<div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl">
<div class="flex flex-col flex-wrap justify-center items-center space-y-4 w-full sm:flex-row sm:space-y-0 sm:space-x-4">
<.form
:let={f}
for={%{}}
@ -80,11 +92,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(
{select(
f,
:class,
[
@ -93,9 +103,9 @@
{gettext("Shotgun"), :shotgun},
{gettext("Pistol"), :pistol}
],
class: "mx-2 my-1 min-w-md input input-primary",
class: "mx-2 my-1 min-w-20 input input-primary",
value: @class
) %>
)}
</.form>
<.form
@ -104,22 +114,37 @@
as={:search}
phx-change="search"
phx-submit="search"
class="grow flex items-center"
class="flex items-center grow"
>
<%= text_input(f, :search_term,
{text_input(f, :search_term,
class: "grow input input-primary",
phx_debounce: 300,
placeholder: gettext("Search shot records"),
role: "search",
value: @search
) %>
)}
</.form>
<.form
:let={f}
for={%{}}
as={:shot_records}
phx-change="change_dates"
phx-submit="change_dates"
class="flex items-center"
>
{label(f, :dates_start, gettext("Dates"),
class: "title text-primary-600 text-lg text-center"
)}
<.date_range name="dates" />
</.form>
</div>
<%= if @shot_records |> Enum.empty?() do %>
<h1 class="title text-xl text-primary-600">
<%= gettext("No shots recorded") %>
<%= display_emoji("😔") %>
<h1 class="text-xl title text-primary-600">
{gettext("No shots recorded")}
{display_emoji("😔")}
</h1>
<% else %>
<.live_component
@ -129,7 +154,7 @@
current_user={@current_user}
>
<:actions :let={shot_record}>
<div class="px-4 py-2 space-x-4 flex justify-center items-center">
<div class="flex justify-center items-center px-4 py-2 space-x-4">
<.link
patch={~p"/range/edit/#{shot_record}"}
class="text-primary-600 link"

View File

@ -1,6 +1,6 @@
<div>
<h2 class="mb-8 text-center title text-xl text-primary-600">
<%= @title %>
{@title}
</h2>
<.form
:let={f}
@ -15,32 +15,32 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= changeset_errors(@changeset) %>
{changeset_errors(@changeset)}
</div>
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :name,
{label(f, :name, gettext("Name"), class: "title text-lg text-primary-600")}
{text_input(f, :name,
class: "input input-primary col-span-2",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :name, "col-span-3") %>
)}
{error_tag(f, :name, "col-span-3")}
<%= label(f, :bg_color, gettext("Background color"), class: "title text-lg text-primary-600") %>
{label(f, :bg_color, gettext("Background color"), class: "title text-lg text-primary-600")}
<span id="tag-bg-color-input" class="mx-auto col-span-2" phx-update="ignore">
<%= color_input(f, :bg_color) %>
{color_input(f, :bg_color)}
</span>
<%= error_tag(f, :bg_color, "col-span-3") %>
{error_tag(f, :bg_color, "col-span-3")}
<%= label(f, :text_color, gettext("Text color"), class: "title text-lg text-primary-600") %>
{label(f, :text_color, gettext("Text color"), class: "title text-lg text-primary-600")}
<span id="tag-text-color-input" class="mx-auto col-span-2" phx-update="ignore">
<%= color_input(f, :text_color) %>
{color_input(f, :text_color)}
</span>
<%= error_tag(f, :text_color, "col-span-3") %>
{error_tag(f, :text_color, "col-span-3")}
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
class: "mx-auto btn btn-primary col-span-3",
phx_disable_with: dgettext("prompts", "Saving...")
) %>
)}
</.form>
</div>

View File

@ -1,47 +1,47 @@
<div class="flex flex-col space-y-8 justify-center items-center">
<h1 class="title text-2xl title-primary-500">
<%= gettext("Tags") %>
<div class="flex flex-col justify-center items-center space-y-8">
<h1 class="text-2xl title title-primary-500">
{gettext("Tags")}
</h1>
<p class="title text-md text-primary-600">
<%= gettext("Tags can be added to your containers to help you organize") %>
{gettext("Tags can be added to your containers to help you organize")}
</p>
<%= if @tags |> Enum.empty?() and @search |> is_nil() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No tags") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No tags")}
{display_emoji("😔")}
</h2>
<.link patch={~p"/tags/new"} class="btn btn-primary">
<%= dgettext("actions", "Make your first tag!") %>
{dgettext("actions", "Make your first tag!")}
</.link>
<% else %>
<.link patch={~p"/tags/new"} class="btn btn-primary">
<%= dgettext("actions", "New Tag") %>
{dgettext("actions", "New Tag")}
</.link>
<div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl">
<div class="flex flex-col flex-wrap justify-center items-center space-y-4 w-full sm:flex-row sm:space-y-0 sm:space-x-4">
<.form
:let={f}
for={%{}}
as={:search}
phx-change="search"
phx-submit="search"
class="grow flex items-center"
class="flex items-center grow"
>
<%= text_input(f, :search_term,
{text_input(f, :search_term,
class: "grow input input-primary",
phx_debounce: 300,
placeholder: gettext("Search tags"),
role: "search",
value: @search
) %>
)}
</.form>
</div>
<%= if @tags |> Enum.empty?() do %>
<h2 class="title text-xl text-primary-600">
<%= gettext("No tags") %>
<%= display_emoji("😔") %>
<h2 class="text-xl title text-primary-600">
{gettext("No tags")}
{display_emoji("😔")}
</h2>
<% else %>
<div class="flex flex-row flex-wrap justify-center items-stretch">

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