21 Commits

Author SHA1 Message Date
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
154 changed files with 3026 additions and 12946 deletions

View File

@ -17,7 +17,7 @@ steps:
- .mix
- name: test
image: elixir:1.17.3-otp-27-alpine
image: elixir:1.18.3-otp-27-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.3-otp-27
erlang 27.3.1
nodejs 23.10.0

View File

@ -1,3 +1,15 @@
# 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

@ -127,7 +127,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.3-otp-27-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

@ -60,7 +60,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`

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`)

10290
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,21 @@
{
"repository": {},
"description": " ",
"license": "MIT",
"engines": {
"node": "v23.0.0",
"npm": "10.9.0"
"node": "v23.10.0"
},
"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.4.8",
"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.11.0",
"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": "^17.1.16",
"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

@ -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">

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,11 +15,11 @@
:if={@changeset.action && not @changeset.valid?}
class="invalid-feedback col-span-3 text-center"
>
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
{dgettext("errors", "Oops, something went wrong! Please check the errors below.")}
</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,
[
@ -29,44 +29,44 @@
],
class: "text-center col-span-2 input input-primary",
maxlength: 255
) %>
<%= error_tag(f, :class, "col-span-3 text-center") %>
)}
{error_tag(f, :class, "col-span-3 text-center")}
<%= 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: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= 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: "text-center col-span-2 input input-primary",
id: "type-form-desc",
phx_debounce: 300,
phx_update: "ignore"
) %>
<%= error_tag(f, :desc, "col-span-3 text-center") %>
)}
{error_tag(f, :desc, "col-span-3 text-center")}
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Dimensions") %>
{gettext("Dimensions")}
</h2>
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label(f, :cartridge, gettext("Cartridge"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :cartridge,
{label(f, :cartridge, gettext("Cartridge"), class: "title text-lg text-primary-600")}
{text_input(f, :cartridge,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("5.56x46mm NATO")
) %>
<%= error_tag(f, :cartridge, "col-span-3 text-center") %>
)}
{error_tag(f, :cartridge, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :cartridge, value: nil) %>
{hidden_input(f, :cartridge, value: nil)}
<% end %>
<%= label(
{label(
f,
:caliber,
if(Changeset.get_field(@changeset, :class) == :shotgun,
@ -74,66 +74,62 @@
else: gettext("Caliber")
),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :caliber,
)}
{text_input(f, :caliber,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext(".223")
) %>
<%= error_tag(f, :caliber, "col-span-3 text-center") %>
)}
{error_tag(f, :caliber, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) == :shotgun do %>
<%= label(f, :unfired_length, gettext("Unfired shell length"),
{label(f, :unfired_length, gettext("Unfired shell length"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :unfired_length,
)}
{text_input(f, :unfired_length,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :unfired_length, "col-span-3 text-center") %>
)}
{error_tag(f, :unfired_length, "col-span-3 text-center")}
<%= label(f, :brass_height, gettext("Brass height"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :brass_height,
{label(f, :brass_height, gettext("Brass height"), class: "title text-lg text-primary-600")}
{text_input(f, :brass_height,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :brass_height, "col-span-3 text-center") %>
)}
{error_tag(f, :brass_height, "col-span-3 text-center")}
<%= label(f, :chamber_size, gettext("Chamber size"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :chamber_size,
{label(f, :chamber_size, gettext("Chamber size"), class: "title text-lg text-primary-600")}
{text_input(f, :chamber_size,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :chamber_size, "col-span-3 text-center") %>
)}
{error_tag(f, :chamber_size, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :unfired_length, value: nil) %>
<%= hidden_input(f, :brass_height, value: nil) %>
<%= hidden_input(f, :chamber_size, value: nil) %>
{hidden_input(f, :unfired_length, value: nil)}
{hidden_input(f, :brass_height, value: nil)}
{hidden_input(f, :chamber_size, value: nil)}
<% end %>
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Projectile") %>
{gettext("Projectile")}
</h2>
<%= label(f, :grains, gettext("Grains"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :grains,
{label(f, :grains, gettext("Grains"), class: "title text-lg text-primary-600")}
{number_input(f, :grains,
step: "1",
class: "text-center col-span-2 input input-primary",
min: 1
) %>
<%= error_tag(f, :grains, "col-span-3 text-center") %>
)}
{error_tag(f, :grains, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label f, :bullet_type, class: "flex title text-lg text-primary-600 space-x-2" do %>
<p><%= gettext("Bullet type") %></p>
<p>{gettext("Bullet type")}</p>
<.link
href="https://shootersreference.com/reloadingdata/bullet_abbreviations/"
@ -144,18 +140,18 @@
<i class="fas fa-md fa-external-link-alt"></i>
</.link>
<% end %>
<%= text_input(f, :bullet_type,
{text_input(f, :bullet_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("FMJ")
) %>
<%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
)}
{error_tag(f, :bullet_type, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :bullet_type, value: nil) %>
{hidden_input(f, :bullet_type, value: nil)}
<% end %>
<%= label(
{label(
f,
:bullet_core,
if(Changeset.get_field(@changeset, :class) == :shotgun,
@ -163,229 +159,225 @@
else: gettext("Bullet core")
),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :bullet_core,
)}
{text_input(f, :bullet_core,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Steel")
) %>
<%= error_tag(f, :bullet_core, "col-span-3 text-center") %>
)}
{error_tag(f, :bullet_core, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label(f, :jacket_type, gettext("Jacket type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :jacket_type,
{label(f, :jacket_type, gettext("Jacket type"), class: "title text-lg text-primary-600")}
{text_input(f, :jacket_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Bimetal")
) %>
<%= error_tag(f, :jacket_type, "col-span-3 text-center") %>
)}
{error_tag(f, :jacket_type, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :jacket_type, value: nil) %>
{hidden_input(f, :jacket_type, value: nil)}
<% end %>
<%= label(f, :case_material, gettext("Case material"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :case_material,
{label(f, :case_material, gettext("Case material"), class: "title text-lg text-primary-600")}
{text_input(f, :case_material,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Brass")
) %>
<%= error_tag(f, :case_material, "col-span-3 text-center") %>
)}
{error_tag(f, :case_material, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) == :shotgun do %>
<%= label(f, :wadding, gettext("Wadding"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :wadding,
{label(f, :wadding, gettext("Wadding"), class: "title text-lg text-primary-600")}
{text_input(f, :wadding,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :wadding, "col-span-3 text-center") %>
)}
{error_tag(f, :wadding, "col-span-3 text-center")}
<%= label(f, :shot_type, gettext("Shot type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :shot_type,
{label(f, :shot_type, gettext("Shot type"), class: "title text-lg text-primary-600")}
{text_input(f, :shot_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Target, bird, buck, etc")
) %>
<%= error_tag(f, :shot_type, "col-span-3 text-center") %>
)}
{error_tag(f, :shot_type, "col-span-3 text-center")}
<%= label(f, :shot_material, gettext("Shot material"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :shot_material,
{label(f, :shot_material, gettext("Shot material"), class: "title text-lg text-primary-600")}
{text_input(f, :shot_material,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :shot_material, "col-span-3 text-center") %>
)}
{error_tag(f, :shot_material, "col-span-3 text-center")}
<%= label(f, :shot_size, gettext("Shot size"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :shot_size,
{label(f, :shot_size, gettext("Shot size"), class: "title text-lg text-primary-600")}
{text_input(f, :shot_size,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :shot_size, "col-span-3 text-center") %>
)}
{error_tag(f, :shot_size, "col-span-3 text-center")}
<%= label(f, :load_grains, gettext("Load grains"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :load_grains,
{label(f, :load_grains, gettext("Load grains"), class: "title text-lg text-primary-600")}
{number_input(f, :load_grains,
step: "1",
class: "text-center col-span-2 input input-primary",
min: 1
) %>
<%= error_tag(f, :load_grains, "col-span-3 text-center") %>
)}
{error_tag(f, :load_grains, "col-span-3 text-center")}
<%= label(f, :shot_charge_weight, gettext("Shot charge weight"),
{label(f, :shot_charge_weight, gettext("Shot charge weight"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :shot_charge_weight,
)}
{text_input(f, :shot_charge_weight,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :shot_charge_weight, "col-span-3 text-center") %>
)}
{error_tag(f, :shot_charge_weight, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :wadding, value: nil) %>
<%= hidden_input(f, :shot_type, value: nil) %>
<%= hidden_input(f, :shot_material, value: nil) %>
<%= hidden_input(f, :shot_size, value: nil) %>
<%= hidden_input(f, :load_grains, value: nil) %>
<%= hidden_input(f, :shot_charge_weight, value: nil) %>
{hidden_input(f, :wadding, value: nil)}
{hidden_input(f, :shot_type, value: nil)}
{hidden_input(f, :shot_material, value: nil)}
{hidden_input(f, :shot_size, value: nil)}
{hidden_input(f, :load_grains, value: nil)}
{hidden_input(f, :shot_charge_weight, value: nil)}
<% end %>
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Powder") %>
{gettext("Powder")}
</h2>
<%= label(f, :powder_type, gettext("Powder type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :powder_type,
{label(f, :powder_type, gettext("Powder type"), class: "title text-lg text-primary-600")}
{text_input(f, :powder_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :powder_type, "col-span-3 text-center") %>
)}
{error_tag(f, :powder_type, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label(f, :powder_grains_per_charge, gettext("Powder grains per charge"),
{label(f, :powder_grains_per_charge, gettext("Powder grains per charge"),
class: "title text-lg text-primary-600"
) %>
<%= number_input(f, :powder_grains_per_charge,
)}
{number_input(f, :powder_grains_per_charge,
step: "1",
class: "text-center col-span-2 input input-primary",
min: 1
) %>
<%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %>
)}
{error_tag(f, :powder_grains_per_charge, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :powder_grains_per_charge, value: nil) %>
{hidden_input(f, :powder_grains_per_charge, value: nil)}
<% end %>
<%= label(f, :pressure, gettext("Pressure"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :pressure,
{label(f, :pressure, gettext("Pressure"), class: "title text-lg text-primary-600")}
{text_input(f, :pressure,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("+P")
) %>
<%= error_tag(f, :pressure, "col-span-3 text-center") %>
)}
{error_tag(f, :pressure, "col-span-3 text-center")}
<%= if Changeset.get_field(@changeset, :class) == :shotgun do %>
<%= label(f, :dram_equivalent, gettext("Dram equivalent"),
{label(f, :dram_equivalent, gettext("Dram equivalent"),
class: "title text-lg text-primary-600"
) %>
<%= text_input(f, :dram_equivalent,
)}
{text_input(f, :dram_equivalent,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :dram_equivalent, "col-span-3 text-center") %>
)}
{error_tag(f, :dram_equivalent, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :dram_equivalent, value: nil) %>
{hidden_input(f, :dram_equivalent, value: nil)}
<% end %>
<%= if Changeset.get_field(@changeset, :class) in [:rifle, :pistol] do %>
<%= label(f, :muzzle_velocity, gettext("Muzzle velocity"),
{label(f, :muzzle_velocity, gettext("Muzzle velocity"),
class: "title text-lg text-primary-600"
) %>
<%= number_input(f, :muzzle_velocity,
)}
{number_input(f, :muzzle_velocity,
step: "1",
class: "text-center col-span-2 input input-primary",
min: 1
) %>
<%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %>
)}
{error_tag(f, :muzzle_velocity, "col-span-3 text-center")}
<% else %>
<%= hidden_input(f, :muzzle_velocity, value: nil) %>
{hidden_input(f, :muzzle_velocity, value: nil)}
<% end %>
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Primer") %>
{gettext("Primer")}
</h2>
<%= label(f, :primer_type, gettext("Primer type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :primer_type,
{label(f, :primer_type, gettext("Primer type"), class: "title text-lg text-primary-600")}
{text_input(f, :primer_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Boxer")
) %>
<%= error_tag(f, :primer_type, "col-span-3 text-center") %>
)}
{error_tag(f, :primer_type, "col-span-3 text-center")}
<%= label(f, :firing_type, gettext("Firing type"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :firing_type,
{label(f, :firing_type, gettext("Firing type"), class: "title text-lg text-primary-600")}
{text_input(f, :firing_type,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300,
placeholder: gettext("Centerfire")
) %>
<%= error_tag(f, :firing_type, "col-span-3 text-center") %>
)}
{error_tag(f, :firing_type, "col-span-3 text-center")}
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Attributes") %>
{gettext("Attributes")}
</h2>
<%= label(f, :tracer, gettext("Tracer"), class: "title text-lg text-primary-600") %>
<%= checkbox(f, :tracer, class: "text-center col-span-2 checkbox") %>
<%= error_tag(f, :tracer, "col-span-3 text-center") %>
{label(f, :tracer, gettext("Tracer"), class: "title text-lg text-primary-600")}
{checkbox(f, :tracer, class: "text-center col-span-2 checkbox")}
{error_tag(f, :tracer, "col-span-3 text-center")}
<%= label(f, :incendiary, gettext("Incendiary"), class: "title text-lg text-primary-600") %>
<%= checkbox(f, :incendiary, class: "text-center col-span-2 checkbox") %>
<%= error_tag(f, :incendiary, "col-span-3 text-center") %>
{label(f, :incendiary, gettext("Incendiary"), class: "title text-lg text-primary-600")}
{checkbox(f, :incendiary, class: "text-center col-span-2 checkbox")}
{error_tag(f, :incendiary, "col-span-3 text-center")}
<%= label(f, :blank, gettext("Blank"), class: "title text-lg text-primary-600") %>
<%= checkbox(f, :blank, class: "text-center col-span-2 checkbox") %>
<%= error_tag(f, :blank, "col-span-3 text-center") %>
{label(f, :blank, gettext("Blank"), class: "title text-lg text-primary-600")}
{checkbox(f, :blank, class: "text-center col-span-2 checkbox")}
{error_tag(f, :blank, "col-span-3 text-center")}
<%= label(f, :corrosive, gettext("Corrosive"), class: "title text-lg text-primary-600") %>
<%= checkbox(f, :corrosive, class: "text-center col-span-2 checkbox") %>
<%= error_tag(f, :corrosive, "col-span-3 text-center") %>
{label(f, :corrosive, gettext("Corrosive"), class: "title text-lg text-primary-600")}
{checkbox(f, :corrosive, class: "text-center col-span-2 checkbox")}
{error_tag(f, :corrosive, "col-span-3 text-center")}
<h2 class="text-center title text-lg text-primary-600 col-span-3">
<%= gettext("Manufacturer") %>
{gettext("Manufacturer")}
</h2>
<%= label(f, :manufacturer, gettext("Manufacturer"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :manufacturer,
{label(f, :manufacturer, gettext("Manufacturer"), class: "title text-lg text-primary-600")}
{text_input(f, :manufacturer,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :manufacturer, "col-span-3 text-center") %>
)}
{error_tag(f, :manufacturer, "col-span-3 text-center")}
<%= label(f, :upc, gettext("UPC"), class: "title text-lg text-primary-600") %>
<%= text_input(f, :upc,
{label(f, :upc, gettext("UPC"), class: "title text-lg text-primary-600")}
{text_input(f, :upc,
class: "text-center col-span-2 input input-primary",
maxlength: 255,
phx_debounce: 300
) %>
<%= error_tag(f, :upc, "col-span-3 text-center") %>
)}
{error_tag(f, :upc, "col-span-3 text-center")}
<%= submit(dgettext("actions", "Save"),
{submit(dgettext("actions", "Save"),
phx_disable_with: dgettext("prompts", "Saving..."),
class: "mx-auto col-span-3 btn btn-primary"
) %>
)}
</.form>
</div>

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