Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
859e5756ae | |||
45b46b761d | |||
bda051ebc8 | |||
01fa306429 | |||
5cff5d8280 | |||
71778d12a6 | |||
9b721a170b | |||
366a6d160d | |||
926d2fe6c2 | |||
c7bd7238c6 | |||
e2c17b6b51 | |||
20988ac1ec | |||
37d101a71e | |||
449a92e4b7 | |||
5d17ee0a11 | |||
b6b6cecc0a | |||
08916a504f | |||
3eda522903 | |||
2e6e26006d | |||
b66d0ea8a1 | |||
839e1d7124 | |||
76834845a3 | |||
cc1413ade5 | |||
668e4c611b | |||
ab3d3721d6 | |||
7e14f292a6 | |||
16a5cb9254 | |||
f722f9901b | |||
6adae82e94 | |||
a87bf15f72 | |||
|
75c0f8642b | ||
ec782515ac | |||
e1cb46cb97 | |||
56a49ed2e3 | |||
f25c151956 | |||
c2ddc2ae0d | |||
33e4d26a3d | |||
179d67a896 | |||
15354d6004 | |||
e358cd6e4e | |||
202b70dc66 | |||
b963baa49d | |||
70701a27d3 | |||
67dc16d222 | |||
fa35038426 |
17
.drone.yml
17
.drone.yml
@ -17,7 +17,7 @@ steps:
|
|||||||
- .mix
|
- .mix
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
image: elixir:1.16.1-alpine
|
image: elixir:1.18.3-otp-27-alpine
|
||||||
environment:
|
environment:
|
||||||
TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
|
TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
|
||||||
HOST: testing.example.tld
|
HOST: testing.example.tld
|
||||||
@ -26,17 +26,16 @@ steps:
|
|||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache build-base npm git
|
- apk add --no-cache build-base npm git
|
||||||
- mix local.rebar --force --if-missing
|
- mix local.rebar --force
|
||||||
- mix local.hex --force --if-missing
|
- mix local.hex --force
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- npm set cache .npm
|
- npm set cache .npm
|
||||||
- npm --prefix ./assets ci --no-audit --prefer-offline
|
- npm --prefix ./assets ci --no-audit --prefer-offline
|
||||||
- npm run --prefix ./assets deploy
|
- mix do phx.digest, gettext.extract, assets.deploy
|
||||||
- mix do phx.digest, gettext.extract
|
|
||||||
- mix test.all
|
- mix test.all
|
||||||
|
|
||||||
- name: build and publish stable
|
- name: build and publish stable
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: plugins/docker
|
||||||
privileged: true
|
privileged: true
|
||||||
settings:
|
settings:
|
||||||
repo: shibaobun/cannery
|
repo: shibaobun/cannery
|
||||||
@ -44,6 +43,8 @@ steps:
|
|||||||
compress: true
|
compress: true
|
||||||
platforms:
|
platforms:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
- linux/arm/v7
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
@ -54,7 +55,7 @@ steps:
|
|||||||
- stable
|
- stable
|
||||||
|
|
||||||
- name: build and publish tagged version
|
- name: build and publish tagged version
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: plugins/docker
|
||||||
privileged: true
|
privileged: true
|
||||||
settings:
|
settings:
|
||||||
repo: shibaobun/cannery
|
repo: shibaobun/cannery
|
||||||
@ -62,6 +63,8 @@ steps:
|
|||||||
compress: true
|
compress: true
|
||||||
platforms:
|
platforms:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
- linux/arm/v7
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -28,10 +28,11 @@ npm-debug.log
|
|||||||
# The directory NPM downloads your dependencies sources to.
|
# The directory NPM downloads your dependencies sources to.
|
||||||
/assets/node_modules/
|
/assets/node_modules/
|
||||||
|
|
||||||
# Since we are building assets from assets/,
|
# Ignore assets that are produced by build tools.
|
||||||
# we ignore priv/static. You may want to comment
|
/priv/static/assets/
|
||||||
# this depending on your deployment strategy.
|
|
||||||
/priv/static/
|
# Ignore digested assets cache.
|
||||||
|
/priv/static/cache_manifest.json
|
||||||
|
|
||||||
# vs code
|
# vs code
|
||||||
.elixir_ls/
|
.elixir_ls/
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
elixir 1.16.1-otp-26
|
elixir 1.18.3-otp-27
|
||||||
erlang 26.2.2
|
erlang 27.3.1
|
||||||
nodejs 21.6.2
|
nodejs 23.10.0
|
||||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@ -1,3 +1,32 @@
|
|||||||
|
# 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
|
||||||
|
- Fix registration page not offering all translations
|
||||||
|
- Update deps
|
||||||
|
|
||||||
|
# v0.9.11
|
||||||
|
- Fix an issue with emails not being able to be sent for real this time
|
||||||
|
- Fix some dropdowns not filling in the correct data
|
||||||
|
- Add debounces to more fields
|
||||||
|
- Update deps
|
||||||
|
|
||||||
|
# v0.9.10
|
||||||
|
- Fix issue with logger failing on oban exceptions
|
||||||
|
- Fix an issue with emails not being able to be sent
|
||||||
|
- Update deps
|
||||||
|
|
||||||
# v0.9.9
|
# v0.9.9
|
||||||
- Actually fix bar graph
|
- Actually fix bar graph
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ status](https://weblate.bubbletea.dev/widgets/cannery/-/287x66-black.png)](https
|
|||||||
|
|
||||||
If you're multilingual, this project can use your translations! Visit
|
If you're multilingual, this project can use your translations! Visit
|
||||||
[weblate](https://weblate.bubbletea.dev/engage/cannery/) for more information.
|
[weblate](https://weblate.bubbletea.dev/engage/cannery/) for more information.
|
||||||
|
Also, if your language isn't displayed here, I'd love to add that language so
|
||||||
|
you can start! Please contact me at
|
||||||
|
(shibao@bubbletea.dev)[mailto:shibao@bubbletea.dev] and let me know!
|
||||||
|
|
||||||
## Style Tips
|
## Style Tips
|
||||||
|
|
||||||
@ -127,7 +130,7 @@ In `test` mode (or in the Docker container), Cannery will listen for the same en
|
|||||||
In `prod` mode (or in the Docker container), Cannery will listen for the same environment variables as dev mode, but also include the following at runtime:
|
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
|
- `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_HOST`: The url for your SMTP email provider. Must be set
|
||||||
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
|
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
|
||||||
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
|
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
|
||||||
|
11
Dockerfile
11
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM elixir:1.16.1-alpine AS build
|
FROM elixir:1.18.3-otp-27-alpine AS build
|
||||||
|
|
||||||
# install build dependencies
|
# install build dependencies
|
||||||
RUN apk add --no-cache build-base npm git python3
|
RUN apk add --no-cache build-base npm git python3
|
||||||
@ -7,8 +7,8 @@ RUN apk add --no-cache build-base npm git python3
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# install hex + rebar
|
# install hex + rebar
|
||||||
RUN mix local.hex --force && \
|
RUN mix local.rebar --force && \
|
||||||
mix local.rebar --force
|
mix local.hex --force
|
||||||
|
|
||||||
# set build ENV
|
# set build ENV
|
||||||
ENV MIX_ENV=prod
|
ENV MIX_ENV=prod
|
||||||
@ -25,13 +25,12 @@ RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
|
|||||||
COPY lib lib
|
COPY lib lib
|
||||||
COPY priv priv
|
COPY priv priv
|
||||||
COPY assets assets
|
COPY assets assets
|
||||||
RUN npm run --prefix ./assets deploy
|
|
||||||
RUN mix do phx.digest, gettext.extract
|
RUN mix do phx.digest, gettext.extract
|
||||||
|
|
||||||
# compile and build release
|
# compile and build release
|
||||||
# uncomment COPY if rel/ exists
|
# uncomment COPY if rel/ exists
|
||||||
# COPY rel rel
|
# COPY rel rel
|
||||||
RUN mix do compile, release
|
RUN mix do assets.deploy, compile, release
|
||||||
|
|
||||||
# prepare release image
|
# prepare release image
|
||||||
FROM alpine:latest AS app
|
FROM alpine:latest AS app
|
||||||
@ -43,6 +42,8 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN chown nobody:nobody /app
|
RUN chown nobody:nobody /app
|
||||||
|
|
||||||
|
ENV MIX_ENV=prod
|
||||||
|
|
||||||
USER nobody:nobody
|
USER nobody:nobody
|
||||||
|
|
||||||
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/cannery ./
|
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/cannery ./
|
||||||
|
@ -60,7 +60,7 @@ You can use the following environment variables to configure Cannery in
|
|||||||
Defaults to `false`.
|
Defaults to `false`.
|
||||||
- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`.
|
- `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
|
- `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
|
- `REGISTRATION`: Controls if user sign-up should be invite only or set to
|
||||||
public. Set to `public` to enable public registration. Defaults to `invite`.
|
public. Set to `public` to enable public registration. Defaults to `invite`.
|
||||||
- `LOCALE`: Sets a custom default locale. Defaults to `en_US`
|
- `LOCALE`: Sets a custom default locale. Defaults to `en_US`
|
||||||
@ -94,6 +94,7 @@ license can be found at
|
|||||||
|
|
||||||
# Links
|
# Links
|
||||||
|
|
||||||
|
- [Website](https://cannery.app): Project website
|
||||||
- [Gitea](https://gitea.bubbletea.dev/shibao/cannery): Main repo, feature
|
- [Gitea](https://gitea.bubbletea.dev/shibao/cannery): Main repo, feature
|
||||||
requests and bug reports
|
requests and bug reports
|
||||||
- [Github](https://github.com/shibaobun/cannery): Source code mirror, please
|
- [Github](https://github.com/shibaobun/cannery): Source code mirror, please
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
@import "tailwindcss/base";
|
|
||||||
@import "tailwindcss/components";
|
|
||||||
@import "tailwindcss/utilities";
|
|
||||||
|
|
||||||
$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";
|
|
||||||
|
|
||||||
@import "components";
|
|
||||||
|
|
||||||
/* fix firefox scrollbars */
|
|
||||||
* {
|
|
||||||
scrollbar-width: auto;
|
|
||||||
scrollbar-color: rgba(161, 161, 170, var(--tw-bg-opacity)) white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-fade {
|
|
||||||
animation: pulse 1s ease-in-out 0s infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { scale: 0.95; opacity: 0.5; }
|
|
||||||
100% { scale: 1.0; opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// disconnect toast
|
|
||||||
.phx-connected > #disconnect {
|
|
||||||
opacity: 0 !important;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-error > #disconnect {
|
|
||||||
opacity: 0.95 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alerts and form errors used by phx.new */
|
|
||||||
.alert {
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.alert-info {
|
|
||||||
color: #31708f;
|
|
||||||
background-color: #d9edf7;
|
|
||||||
border-color: #bce8f1;
|
|
||||||
}
|
|
||||||
.alert-warning {
|
|
||||||
color: #8a6d3b;
|
|
||||||
background-color: #fcf8e3;
|
|
||||||
border-color: #faebcc;
|
|
||||||
}
|
|
||||||
.alert-danger {
|
|
||||||
color: #a94442;
|
|
||||||
background-color: #f2dede;
|
|
||||||
border-color: #ebccd1;
|
|
||||||
}
|
|
||||||
.alert p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.alert:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.invalid-feedback {
|
|
||||||
color: #a94442;
|
|
||||||
display: block;
|
|
||||||
margin: -1rem 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LiveView specific classes for your customization */
|
|
||||||
.phx-no-feedback.invalid-feedback,
|
|
||||||
.phx-no-feedback .invalid-feedback {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-click-loading {
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-loading{
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-modal {
|
|
||||||
opacity: 1!important;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-modal-content {
|
|
||||||
background-color: #fefefe;
|
|
||||||
margin: 15vh auto;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #888;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-modal-close {
|
|
||||||
color: #aaa;
|
|
||||||
float: right;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phx-modal-close:hover,
|
|
||||||
.phx-modal-close:focus {
|
|
||||||
color: black;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in-scale {
|
|
||||||
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-out-scale {
|
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
|
||||||
}
|
|
||||||
.fade-out {
|
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in-scale-keys{
|
|
||||||
0% { scale: 0.95; opacity: 0; }
|
|
||||||
100% { scale: 1.0; opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out-scale-keys{
|
|
||||||
0% { scale: 1.0; opacity: 1; }
|
|
||||||
100% { scale: 0.95; opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in-keys{
|
|
||||||
0% { opacity: 0; }
|
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out-keys{
|
|
||||||
0% { opacity: 1; }
|
|
||||||
100% { opacity: 0; }
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
266
assets/css/style.css
Normal file
266
assets/css/style.css
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
@import "tailwindcss" source("../..");
|
||||||
|
|
||||||
|
@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 "@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 */
|
||||||
|
* {
|
||||||
|
scrollbar-width: auto;
|
||||||
|
scrollbar-color: rgba(161, 161, 170, var(--tw-bg-opacity)) white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-fade {
|
||||||
|
animation: pulse 1s ease-in-out 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { scale: 0.95; opacity: 0.5; }
|
||||||
|
100% { scale: 1.0; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disconnect toast */
|
||||||
|
.phx-connected > #disconnect {
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-error > #disconnect {
|
||||||
|
opacity: 0.95 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts and form errors used by phx.new */
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
color: #31708f;
|
||||||
|
background-color: #d9edf7;
|
||||||
|
border-color: #bce8f1;
|
||||||
|
}
|
||||||
|
.alert-warning {
|
||||||
|
color: #8a6d3b;
|
||||||
|
background-color: #fcf8e3;
|
||||||
|
border-color: #faebcc;
|
||||||
|
}
|
||||||
|
.alert-danger {
|
||||||
|
color: #a94442;
|
||||||
|
background-color: #f2dede;
|
||||||
|
border-color: #ebccd1;
|
||||||
|
}
|
||||||
|
.alert p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.alert:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.invalid-feedback {
|
||||||
|
color: #a94442;
|
||||||
|
display: block;
|
||||||
|
margin: -1rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LiveView specific classes for your customization */
|
||||||
|
.phx-no-feedback.invalid-feedback,
|
||||||
|
.phx-no-feedback .invalid-feedback {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-click-loading {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-loading{
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-modal {
|
||||||
|
opacity: 1!important;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 15vh auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-modal-close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phx-modal-close:hover,
|
||||||
|
.phx-modal-close:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-scale {
|
||||||
|
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out-scale {
|
||||||
|
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||||
|
}
|
||||||
|
.fade-out {
|
||||||
|
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-scale-keys{
|
||||||
|
0% { scale: 0.95; opacity: 0; }
|
||||||
|
100% { scale: 1.0; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out-scale-keys{
|
||||||
|
0% { scale: 1.0; opacity: 1; }
|
||||||
|
100% { scale: 0.95; opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-keys{
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out-keys{
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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.input-primary {
|
||||||
|
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-above {
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-below {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.input-primary .ss-search input[type="search"] {
|
||||||
|
@apply border-primary-500 hover:border-primary-600 active:border-primary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-above .ss-search {
|
||||||
|
padding: var(--ss-spacing-l) 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-below .ss-search {
|
||||||
|
padding: 0 0 var(--ss-spacing-l) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-above .ss-list > *:not(:first-child) {
|
||||||
|
margin: var(--ss-spacing-l) 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content.ss-open-below .ss-list > *:not(:last-child) {
|
||||||
|
margin: 0 0 var(--ss-spacing-l) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-content .ss-list .ss-option {
|
||||||
|
border-radius: var(--ss-border-radius);
|
||||||
|
}
|
@ -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`
|
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||||
// to get started and then uncomment the line below.
|
// to get started and then uncomment the line below.
|
||||||
// import "./user_socket.js"
|
// import "./user_socket.js"
|
||||||
@ -24,15 +20,16 @@ import 'phoenix_html'
|
|||||||
// Establish Phoenix Socket and LiveView configuration.
|
// Establish Phoenix Socket and LiveView configuration.
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
import { LiveSocket } from 'phoenix_live_view'
|
import { LiveSocket } from 'phoenix_live_view'
|
||||||
import topbar from 'topbar'
|
|
||||||
import ShotLogChart from './shot_log_chart'
|
|
||||||
import Date from './date'
|
import Date from './date'
|
||||||
import DateTime from './datetime'
|
import DateTime from './datetime'
|
||||||
|
import ShotLogChart from './shot_log_chart'
|
||||||
|
import SlimSelect from './slim_select'
|
||||||
|
import topbar from 'topbar'
|
||||||
|
|
||||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
||||||
const liveSocket = new LiveSocket('/live', Socket, {
|
const liveSocket = new LiveSocket('/live', Socket, {
|
||||||
params: { _csrf_token: csrfToken },
|
params: { _csrf_token: csrfToken },
|
||||||
hooks: { Date, DateTime, ShotLogChart }
|
hooks: { Date, DateTime, ShotLogChart, SlimSelect }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show progress bar on live navigation and form submits
|
// Show progress bar on live navigation and form submits
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
displayDate (el) {
|
displayDate (el) {
|
||||||
const date =
|
const date =
|
||||||
Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' })
|
Intl.DateTimeFormat([], { timeZone: 'UTC', dateStyle: 'short' })
|
||||||
.format(new Date(el.dateTime))
|
.format(new Date(el.dateTime))
|
||||||
|
|
||||||
el.innerText = date
|
el.innerText = date
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
displayDateTime (el) {
|
displayDateTime (el) {
|
||||||
const date =
|
const date =
|
||||||
Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'long' })
|
Intl.DateTimeFormat([], { timeZone: 'UTC', dateStyle: 'short', timeStyle: 'long' })
|
||||||
.format(new Date(el.dateTime))
|
.format(new Date(el.dateTime))
|
||||||
|
|
||||||
el.innerText = date
|
el.innerText = date
|
||||||
|
28
assets/js/slim_select.js
Normal file
28
assets/js/slim_select.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import SlimSelect from 'slim-select'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initalizeSlimSelect (el) {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
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`)
|
||||||
|
main.setAttribute('phx-update', 'ignore')
|
||||||
|
|
||||||
|
const content = document.querySelector(`.ss-content[data-id="${el.dataset.id}"]`)
|
||||||
|
content.setAttribute('id', `${el.dataset.id}-content`)
|
||||||
|
content.setAttribute('phx-update', 'ignore')
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
this.el.slimselect?.destroy()
|
||||||
|
this.initalizeSlimSelect(this.el)
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.initalizeSlimSelect(this.el)
|
||||||
|
}
|
||||||
|
}
|
13631
assets/package-lock.json
generated
13631
assets/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,49 +1,21 @@
|
|||||||
{
|
{
|
||||||
"repository": {},
|
|
||||||
"description": " ",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "v21.6.2",
|
"node": "v23.10.0"
|
||||||
"npm": "10.2.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "NODE_ENV=production webpack --mode production",
|
|
||||||
"watch": "webpack --mode development --watch --watch-options-stdin",
|
|
||||||
"format": "standard --fix",
|
"format": "standard --fix",
|
||||||
"test": "standard"
|
"test": "standard"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.8",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^4.1.0",
|
||||||
"phoenix": "file:../deps/phoenix",
|
"slim-select": "^2.11.0",
|
||||||
"phoenix_html": "file:../deps/phoenix_html",
|
"topbar": "^3.0.0"
|
||||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
|
||||||
"topbar": "^2.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.23.9",
|
"npm-check-updates": "^17.1.16",
|
||||||
"@babel/preset-env": "^7.23.9",
|
"standard": "^17.1.2"
|
||||||
"autoprefixer": "^10.4.17",
|
|
||||||
"babel-loader": "^9.1.3",
|
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
|
||||||
"css-loader": "^6.10.0",
|
|
||||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"mini-css-extract-plugin": "^2.8.0",
|
|
||||||
"npm-check-updates": "^16.14.15",
|
|
||||||
"postcss": "^8.4.35",
|
|
||||||
"postcss-import": "^16.0.1",
|
|
||||||
"postcss-loader": "^8.1.0",
|
|
||||||
"postcss-preset-env": "^9.4.0",
|
|
||||||
"sass": "^1.71.1",
|
|
||||||
"sass-loader": "^14.1.1",
|
|
||||||
"standard": "^17.1.0",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
|
||||||
"webpack": "^5.90.3",
|
|
||||||
"webpack-cli": "^5.1.4",
|
|
||||||
"webpack-dev-server": "^5.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-import': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
@ -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: []
|
|
||||||
}
|
|
@ -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: '../' }] })
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,13 +8,14 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :cannery,
|
config :cannery,
|
||||||
ecto_repos: [Cannery.Repo],
|
env: :dev,
|
||||||
generators: [binary_id: true]
|
ecto_repos: [Cannery.Repo]
|
||||||
|
|
||||||
config :cannery, Cannery.Accounts, registration: System.get_env("REGISTRATION", "invite")
|
config :cannery, Cannery.Accounts, registration: System.get_env("REGISTRATION", "invite")
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :cannery, CanneryWeb.Endpoint,
|
config :cannery, CanneryWeb.Endpoint,
|
||||||
|
adapter: Bandit.PhoenixAdapter,
|
||||||
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
|
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
|
||||||
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
|
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
|
||||||
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
|
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
|
||||||
@ -28,9 +29,10 @@ config :cannery, CanneryWeb.Endpoint,
|
|||||||
config :cannery, Cannery.Application, automigrate: false
|
config :cannery, Cannery.Application, automigrate: false
|
||||||
|
|
||||||
config :cannery, :generators,
|
config :cannery, :generators,
|
||||||
migration: true,
|
|
||||||
binary_id: 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
|
# Configures the mailer
|
||||||
#
|
#
|
||||||
@ -54,14 +56,25 @@ config :cannery, Oban,
|
|||||||
queues: [default: 10, mailers: 20]
|
queues: [default: 10, mailers: 20]
|
||||||
|
|
||||||
# Configure esbuild (the version is required)
|
# Configure esbuild (the version is required)
|
||||||
# config :esbuild,
|
config :esbuild,
|
||||||
# version: "0.14.0",
|
version: "0.17.11",
|
||||||
# default: [
|
cannery: [
|
||||||
# args:
|
args:
|
||||||
# ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||||
# cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
# env: %{"NODE_PATH" => Path.expand("../deps", __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
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
|
@ -2,6 +2,7 @@ import Config
|
|||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :cannery, Cannery.Repo,
|
config :cannery, Cannery.Repo,
|
||||||
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true,
|
show_sensitive_data_on_connection_error: true,
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
@ -12,21 +13,14 @@ config :cannery, Cannery.Repo,
|
|||||||
# watchers to your application. For example, we use it
|
# watchers to your application. For example, we use it
|
||||||
# with esbuild to bundle .js and .css sources.
|
# with esbuild to bundle .js and .css sources.
|
||||||
config :cannery, CanneryWeb.Endpoint,
|
config :cannery, CanneryWeb.Endpoint,
|
||||||
|
http: [ip: {0, 0, 0, 0}, port: 4000],
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
code_reloader: true,
|
code_reloader: true,
|
||||||
debug_errors: true,
|
debug_errors: true,
|
||||||
secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
|
secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
|
||||||
watchers: [
|
watchers: [
|
||||||
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
|
esbuild: {Esbuild, :install_and_run, [:cannery, ~w(--sourcemap=inline --watch)]},
|
||||||
# esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
|
tailwind: {Tailwind, :install_and_run, [:cannery, ~w(--watch)]}
|
||||||
node: [
|
|
||||||
"node_modules/webpack/bin/webpack.js",
|
|
||||||
"--mode",
|
|
||||||
"development",
|
|
||||||
"--watch",
|
|
||||||
"--watch-options-stdin",
|
|
||||||
cd: Path.expand("../assets", __DIR__)
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# ## SSL Support
|
# ## SSL Support
|
||||||
@ -57,7 +51,7 @@ config :cannery, CanneryWeb.Endpoint,
|
|||||||
config :cannery, CanneryWeb.Endpoint,
|
config :cannery, CanneryWeb.Endpoint,
|
||||||
live_reload: [
|
live_reload: [
|
||||||
patterns: [
|
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"priv/gettext/.*(po)$",
|
||||||
~r"lib/cannery_web/*/.*(ex)$"
|
~r"lib/cannery_web/*/.*(ex)$"
|
||||||
]
|
]
|
||||||
@ -73,3 +67,9 @@ config :phoenix, :stacktrace_depth, 20
|
|||||||
|
|
||||||
# Initialize plugs at runtime for faster development compilation
|
# Initialize plugs at runtime for faster development compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
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
|
||||||
|
@ -14,6 +14,8 @@ config :cannery, CanneryWeb.Endpoint, cache_static_manifest: "priv/static/cache_
|
|||||||
# Do not print debug messages in production
|
# Do not print debug messages in production
|
||||||
config :logger, level: :info
|
config :logger, level: :info
|
||||||
|
|
||||||
|
config :cannery, env: :prod
|
||||||
|
|
||||||
# ## SSL Support
|
# ## SSL Support
|
||||||
#
|
#
|
||||||
# To get SSL working, you will need to add the `https` key
|
# To get SSL working, you will need to add the `https` key
|
||||||
|
@ -7,13 +7,23 @@ import Config
|
|||||||
# any compile-time configuration in here, as it won't be applied.
|
# any compile-time configuration in here, as it won't be applied.
|
||||||
# The block below contains prod specific runtime configuration.
|
# The block below contains prod specific runtime configuration.
|
||||||
|
|
||||||
# Start the phoenix server if environment is set and running in a release
|
# ## Using releases
|
||||||
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
|
#
|
||||||
|
# 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
|
config :cannery, CanneryWeb.Endpoint, server: true
|
||||||
end
|
end
|
||||||
|
|
||||||
config :cannery, CanneryWeb.HTMLHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
|
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
|
# Set default locale
|
||||||
config :gettext, :default_locale, System.get_env("LOCALE", "en_US")
|
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") ||
|
System.get_env("SECRET_KEY_BASE") ||
|
||||||
raise """
|
raise """
|
||||||
environment variable SECRET_KEY_BASE is missing.
|
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
|
config :cannery, CanneryWeb.Endpoint, secret_key_base: secret_key_base
|
||||||
|
@ -9,8 +9,9 @@ config :bcrypt_elixir, :log_rounds, 1
|
|||||||
# to provide built-in test partitioning in CI environment.
|
# to provide built-in test partitioning in CI environment.
|
||||||
# Run `mix help test` for more information.
|
# Run `mix help test` for more information.
|
||||||
config :cannery, Cannery.Repo,
|
config :cannery, Cannery.Repo,
|
||||||
|
pool_size: 10,
|
||||||
pool: Ecto.Adapters.SQL.Sandbox,
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
pool_size: 10
|
timeout: 60000
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
# you can enable the server option below.
|
# you can enable the server option below.
|
||||||
@ -19,6 +20,8 @@ config :cannery, CanneryWeb.Endpoint,
|
|||||||
secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
|
secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
|
||||||
server: false
|
server: false
|
||||||
|
|
||||||
|
config :cannery, env: :test
|
||||||
|
|
||||||
# In test we don't send emails.
|
# In test we don't send emails.
|
||||||
config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test
|
config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
@ -32,4 +35,4 @@ config :logger, level: :warning
|
|||||||
config :phoenix, :plug_init_mode, :runtime
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|
||||||
# Disable Oban
|
# Disable Oban
|
||||||
config :cannery, Oban, queues: false, plugins: false
|
config :cannery, Oban, queues: false, plugins: false, testing: :manual
|
||||||
|
@ -6,4 +6,35 @@ defmodule Cannery do
|
|||||||
Contexts are also responsible for managing your data, regardless
|
Contexts are also responsible for managing your data, regardless
|
||||||
if it comes from the database, an external API or others.
|
if it comes from the database, an external API or others.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def context do
|
||||||
|
quote do
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
import Ecto.Query
|
||||||
|
alias Cannery.Accounts.User
|
||||||
|
alias Cannery.Repo
|
||||||
|
alias Ecto.{Changeset, Multi, Queryable, UUID}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def schema do
|
||||||
|
quote do
|
||||||
|
use Ecto.Schema
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
import Ecto.{Changeset, Query}
|
||||||
|
alias Cannery.Accounts.User
|
||||||
|
alias Ecto.{Association, Changeset, Queryable, UUID}
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
@timestamps_opts [type: :utc_datetime_usec]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
When used, dispatch to the appropriate context/schema/etc.
|
||||||
|
"""
|
||||||
|
defmacro __using__(which) when is_atom(which) do
|
||||||
|
apply(__MODULE__, which, [])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,10 +3,9 @@ defmodule Cannery.Accounts do
|
|||||||
The Accounts context.
|
The Accounts context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
use Cannery, :context
|
||||||
alias Cannery.{Mailer, Repo}
|
alias Cannery.Mailer
|
||||||
alias Cannery.Accounts.{Invite, Invites, User, UserToken}
|
alias Cannery.Accounts.{Invite, Invites, UserToken}
|
||||||
alias Ecto.{Changeset, Multi}
|
|
||||||
alias Oban.Job
|
alias Oban.Job
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
@ -219,7 +218,7 @@ defmodule Cannery.Accounts do
|
|||||||
|
|
||||||
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
||||||
%UserToken{sent_to: email} <- Repo.one(query),
|
%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
|
:ok
|
||||||
else
|
else
|
||||||
_error_tuple -> :error
|
_error_tuple -> :error
|
||||||
|
@ -5,24 +5,19 @@ defmodule Cannery.Accounts.Invite do
|
|||||||
`:uses_left` is defined.
|
`:uses_left` is defined.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.Accounts.User
|
|
||||||
alias Ecto.{Association, Changeset, UUID}
|
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "invites" do
|
schema "invites" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :token, :string
|
field :token, :string
|
||||||
field :uses_left, :integer, default: nil
|
field :uses_left, :integer, default: nil
|
||||||
field :disabled_at, :naive_datetime
|
field :disabled_at, :utc_datetime_usec
|
||||||
|
|
||||||
belongs_to :created_by, User
|
belongs_to :created_by, User
|
||||||
|
|
||||||
has_many :users, User
|
has_many :users, User
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
@ -30,12 +25,12 @@ defmodule Cannery.Accounts.Invite do
|
|||||||
name: String.t(),
|
name: String.t(),
|
||||||
token: token(),
|
token: token(),
|
||||||
uses_left: integer() | nil,
|
uses_left: integer() | nil,
|
||||||
disabled_at: NaiveDateTime.t(),
|
disabled_at: DateTime.t(),
|
||||||
created_by: User.t() | nil | Association.NotLoaded.t(),
|
created_by: User.t() | nil | Association.NotLoaded.t(),
|
||||||
created_by_id: User.id() | nil,
|
created_by_id: User.id() | nil,
|
||||||
users: [User.t()] | Association.NotLoaded.t(),
|
users: [User.t()] | Association.NotLoaded.t(),
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_invite :: %__MODULE__{}
|
@type new_invite :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
|
@ -3,10 +3,8 @@ defmodule Cannery.Accounts.Invites do
|
|||||||
The Invites context.
|
The Invites context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
use Cannery, :context
|
||||||
alias Ecto.Multi
|
alias Cannery.Accounts.Invite
|
||||||
alias Cannery.Accounts.{Invite, User}
|
|
||||||
alias Cannery.Repo
|
|
||||||
|
|
||||||
@invite_token_length 20
|
@invite_token_length 20
|
||||||
|
|
||||||
@ -125,7 +123,7 @@ defmodule Cannery.Accounts.Invites do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp decrement_invite_changeset(%Invite{uses_left: 1} = invite) do
|
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})
|
invite |> Invite.update_changeset(%{uses_left: 0, disabled_at: now})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,11 +3,8 @@ defmodule Cannery.Accounts.User do
|
|||||||
A Cannery user
|
A Cannery user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
alias Cannery.Accounts.Invite
|
||||||
import CanneryWeb.Gettext
|
|
||||||
alias Ecto.{Association, Changeset, UUID}
|
|
||||||
alias Cannery.Accounts.{Invite, User}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
@ -20,13 +17,12 @@ defmodule Cannery.Accounts.User do
|
|||||||
:updated_at
|
:updated_at
|
||||||
]}
|
]}
|
||||||
@derive {Inspect, except: [:password]}
|
@derive {Inspect, except: [:password]}
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :email, :string
|
field :email, :string
|
||||||
field :password, :string, virtual: true
|
field :password, :string, virtual: true
|
||||||
field :hashed_password, :string
|
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 :role, Ecto.Enum, values: [:admin, :user], default: :user
|
||||||
field :locale, :string
|
field :locale, :string
|
||||||
|
|
||||||
@ -34,7 +30,7 @@ defmodule Cannery.Accounts.User do
|
|||||||
|
|
||||||
belongs_to :invite, Invite
|
belongs_to :invite, Invite
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %User{
|
@type t :: %User{
|
||||||
@ -42,14 +38,14 @@ defmodule Cannery.Accounts.User do
|
|||||||
email: String.t(),
|
email: String.t(),
|
||||||
password: String.t(),
|
password: String.t(),
|
||||||
hashed_password: String.t(),
|
hashed_password: String.t(),
|
||||||
confirmed_at: NaiveDateTime.t(),
|
confirmed_at: DateTime.t(),
|
||||||
role: role(),
|
role: role(),
|
||||||
locale: String.t() | nil,
|
locale: String.t() | nil,
|
||||||
created_invites: [Invite.t()] | Association.NotLoaded.t(),
|
created_invites: [Invite.t()] | Association.NotLoaded.t(),
|
||||||
invite: Invite.t() | nil | Association.NotLoaded.t(),
|
invite: Invite.t() | nil | Association.NotLoaded.t(),
|
||||||
invite_id: Invite.id() | nil,
|
invite_id: Invite.id() | nil,
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_user :: %User{}
|
@type new_user :: %User{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
@ -141,7 +137,7 @@ defmodule Cannery.Accounts.User do
|
|||||||
|> cast(attrs, [:email])
|
|> cast(attrs, [:email])
|
||||||
|> validate_email()
|
|> validate_email()
|
||||||
|> case do
|
|> case do
|
||||||
%{changes: %{email: _}} = changeset -> changeset
|
%{changes: %{email: _email}} = changeset -> changeset
|
||||||
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
|
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -172,7 +168,7 @@ defmodule Cannery.Accounts.User do
|
|||||||
"""
|
"""
|
||||||
@spec confirm_changeset(t() | changeset()) :: changeset()
|
@spec confirm_changeset(t() | changeset()) :: changeset()
|
||||||
def confirm_changeset(user_or_changeset) do
|
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)
|
user_or_changeset |> change(confirmed_at: now)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -198,6 +194,8 @@ defmodule Cannery.Accounts.User do
|
|||||||
"""
|
"""
|
||||||
@spec validate_current_password(changeset(), String.t()) :: changeset()
|
@spec validate_current_password(changeset(), String.t()) :: changeset()
|
||||||
def validate_current_password(changeset, password) do
|
def validate_current_password(changeset, password) do
|
||||||
|
changeset = cast(changeset, %{current_password: password}, [:current_password])
|
||||||
|
|
||||||
if valid_password?(changeset.data, password),
|
if valid_password?(changeset.data, password),
|
||||||
do: changeset,
|
do: changeset,
|
||||||
else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))
|
else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))
|
||||||
|
@ -3,10 +3,7 @@ defmodule Cannery.Accounts.UserToken do
|
|||||||
Schema for a user's session token
|
Schema for a user's session token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Query
|
|
||||||
alias Cannery.Accounts.User
|
|
||||||
alias Ecto.{Association, UUID}
|
|
||||||
|
|
||||||
@hash_algorithm :sha256
|
@hash_algorithm :sha256
|
||||||
@rand_size 32
|
@rand_size 32
|
||||||
@ -18,8 +15,6 @@ defmodule Cannery.Accounts.UserToken do
|
|||||||
@change_email_validity_in_days 7
|
@change_email_validity_in_days 7
|
||||||
@session_validity_in_days 60
|
@session_validity_in_days 60
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "users_tokens" do
|
schema "users_tokens" do
|
||||||
field :token, :binary
|
field :token, :binary
|
||||||
field :context, :string
|
field :context, :string
|
||||||
@ -37,7 +32,7 @@ defmodule Cannery.Accounts.UserToken do
|
|||||||
sent_to: String.t(),
|
sent_to: String.t(),
|
||||||
user: User.t() | Association.NotLoaded.t(),
|
user: User.t() | Association.NotLoaded.t(),
|
||||||
user_id: User.id() | nil,
|
user_id: User.id() | nil,
|
||||||
inserted_at: NaiveDateTime.t()
|
inserted_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_user_token :: %__MODULE__{}
|
@type new_user_token :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
@ -155,7 +150,7 @@ defmodule Cannery.Accounts.UserToken do
|
|||||||
from t in __MODULE__, where: t.user_id == ^user.id
|
from t in __MODULE__, where: t.user_id == ^user.id
|
||||||
end
|
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
|
from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,14 +3,14 @@ defmodule Cannery.ActivityLog do
|
|||||||
The ActivityLog context.
|
The ActivityLog context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
use Cannery, :context
|
||||||
alias Cannery.Ammo.{Pack, Type}
|
alias Cannery.{ActivityLog.ShotRecord, Ammo.Pack, Ammo.Type}
|
||||||
alias Cannery.{Accounts.User, ActivityLog.ShotRecord, Repo}
|
|
||||||
alias Ecto.{Multi, Queryable}
|
|
||||||
|
|
||||||
@type list_shot_records_option ::
|
@type list_shot_records_option ::
|
||||||
{:search, String.t() | nil}
|
{:search, String.t() | nil}
|
||||||
| {:class, Type.class() | :all | nil}
|
| {:class, Type.class() | :all | nil}
|
||||||
|
| {:start_date, String.t() | nil}
|
||||||
|
| {:end_date, String.t() | nil}
|
||||||
| {:pack_id, Pack.id() | nil}
|
| {:pack_id, Pack.id() | nil}
|
||||||
@type list_shot_records_options :: [list_shot_records_option()]
|
@type list_shot_records_options :: [list_shot_records_option()]
|
||||||
|
|
||||||
@ -51,6 +51,8 @@ defmodule Cannery.ActivityLog do
|
|||||||
|> list_shot_records_search(Keyword.get(opts, :search))
|
|> list_shot_records_search(Keyword.get(opts, :search))
|
||||||
|> list_shot_records_class(Keyword.get(opts, :class))
|
|> list_shot_records_class(Keyword.get(opts, :class))
|
||||||
|> list_shot_records_pack_id(Keyword.get(opts, :pack_id))
|
|> 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()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -102,6 +104,20 @@ defmodule Cannery.ActivityLog do
|
|||||||
|
|
||||||
defp list_shot_records_pack_id(query, _all), do: query
|
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 """
|
@doc """
|
||||||
Returns a count of shot records.
|
Returns a count of shot records.
|
||||||
|
|
||||||
|
@ -3,11 +3,8 @@ defmodule Cannery.ActivityLog.ShotRecord do
|
|||||||
A shot record records a group of ammo shot during a range trip
|
A shot record records a group of ammo shot during a range trip
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import CanneryWeb.Gettext
|
alias Cannery.{Ammo, Ammo.Pack}
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.{Accounts.User, Ammo, Ammo.Pack}
|
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
@ -27,7 +24,7 @@ defmodule Cannery.ActivityLog.ShotRecord do
|
|||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
field :pack_id, :binary_id
|
field :pack_id, :binary_id
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
@ -37,8 +34,8 @@ defmodule Cannery.ActivityLog.ShotRecord do
|
|||||||
date: Date.t() | nil,
|
date: Date.t() | nil,
|
||||||
pack_id: Pack.id(),
|
pack_id: Pack.id(),
|
||||||
user_id: User.id(),
|
user_id: User.id(),
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_shot_record :: %__MODULE__{}
|
@type new_shot_record :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
|
@ -3,13 +3,11 @@ defmodule Cannery.Ammo do
|
|||||||
The Ammo context.
|
The Ammo context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import CanneryWeb.Gettext
|
use Cannery, :context
|
||||||
import Ecto.Query, warn: false
|
alias Cannery.Containers
|
||||||
alias Cannery.{Accounts.User, Containers, Repo}
|
|
||||||
alias Cannery.Containers.{Container, ContainerTag, Tag}
|
alias Cannery.Containers.{Container, ContainerTag, Tag}
|
||||||
alias Cannery.{ActivityLog, ActivityLog.ShotRecord}
|
alias Cannery.{ActivityLog, ActivityLog.ShotRecord}
|
||||||
alias Cannery.Ammo.{Pack, Type}
|
alias Cannery.Ammo.{Pack, Type}
|
||||||
alias Ecto.{Changeset, Queryable}
|
|
||||||
|
|
||||||
@pack_create_limit 10_000
|
@pack_create_limit 10_000
|
||||||
@pack_preloads [:type]
|
@pack_preloads [:type]
|
||||||
@ -549,7 +547,7 @@ defmodule Cannery.Ammo do
|
|||||||
|
|
||||||
@spec list_packs_staged(Queryable.t(), staged :: boolean() | nil) :: Queryable.t()
|
@spec list_packs_staged(Queryable.t(), staged :: boolean() | nil) :: Queryable.t()
|
||||||
defp list_packs_staged(query, staged) when staged |> is_boolean(),
|
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
|
defp list_packs_staged(query, _nil), do: query
|
||||||
|
|
||||||
@ -922,7 +920,7 @@ defmodule Cannery.Ammo do
|
|||||||
multiplier <= @pack_create_limit and
|
multiplier <= @pack_create_limit and
|
||||||
type_id |> is_binary() and
|
type_id |> is_binary() and
|
||||||
container_id |> is_binary() do
|
container_id |> is_binary() do
|
||||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
changesets =
|
changesets =
|
||||||
Enum.map(1..multiplier, fn _count ->
|
Enum.map(1..multiplier, fn _count ->
|
||||||
@ -959,11 +957,10 @@ defmodule Cannery.Ammo do
|
|||||||
|
|
||||||
defp do_create_packs(
|
defp do_create_packs(
|
||||||
%{"type_id" => type_id, "container_id" => container_id} = attrs,
|
%{"type_id" => type_id, "container_id" => container_id} = attrs,
|
||||||
_multiplier,
|
multiplier,
|
||||||
user
|
user
|
||||||
)
|
)
|
||||||
when is_binary(type_id) and is_binary(container_id) do
|
when is_binary(type_id) and is_binary(container_id) do
|
||||||
changeset =
|
|
||||||
%Pack{}
|
%Pack{}
|
||||||
|> Pack.create_changeset(
|
|> Pack.create_changeset(
|
||||||
get_type!(type_id, user),
|
get_type!(type_id, user),
|
||||||
@ -971,13 +968,68 @@ defmodule Cannery.Ammo do
|
|||||||
user,
|
user,
|
||||||
attrs
|
attrs
|
||||||
)
|
)
|
||||||
|> Changeset.add_error(:multiplier, dgettext("errors", "Invalid multiplier"))
|
|> maybe_add_multiplier_error(multiplier)
|
||||||
|
|> Changeset.apply_action(:insert)
|
||||||
{:error, changeset}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_create_packs(invalid_attrs, _multiplier, user) do
|
defp do_create_packs(
|
||||||
{:error, %Pack{} |> Pack.create_changeset(nil, nil, user, invalid_attrs)}
|
%{"type_id" => type_id} = attrs,
|
||||||
|
multiplier,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
when is_binary(type_id) do
|
||||||
|
%Pack{}
|
||||||
|
|> Pack.create_changeset(
|
||||||
|
get_type!(type_id, user),
|
||||||
|
nil,
|
||||||
|
user,
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
|> maybe_add_multiplier_error(multiplier)
|
||||||
|
|> Changeset.apply_action(:insert)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_create_packs(
|
||||||
|
%{"container_id" => container_id} = attrs,
|
||||||
|
multiplier,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
when is_binary(container_id) do
|
||||||
|
%Pack{}
|
||||||
|
|> Pack.create_changeset(
|
||||||
|
nil,
|
||||||
|
Containers.get_container!(container_id, user),
|
||||||
|
user,
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
|> maybe_add_multiplier_error(multiplier)
|
||||||
|
|> Changeset.apply_action(:insert)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_create_packs(invalid_attrs, multiplier, user) do
|
||||||
|
%Pack{}
|
||||||
|
|> Pack.create_changeset(nil, nil, user, invalid_attrs)
|
||||||
|
|> maybe_add_multiplier_error(multiplier)
|
||||||
|
|> Changeset.apply_action(:insert)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add_multiplier_error(changeset, multiplier)
|
||||||
|
when multiplier >= 1 and
|
||||||
|
multiplier <= @pack_create_limit do
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add_multiplier_error(changeset, multiplier) do
|
||||||
|
changeset
|
||||||
|
|> Changeset.add_error(
|
||||||
|
:multiplier,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}",
|
||||||
|
max: @pack_create_limit,
|
||||||
|
multiplier: multiplier
|
||||||
|
)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec preload_pack(Pack.t()) :: Pack.t()
|
@spec preload_pack(Pack.t()) :: Pack.t()
|
||||||
|
@ -6,55 +6,46 @@ defmodule Cannery.Ammo.Pack do
|
|||||||
amount paid for that ammunition, or what condition it is in
|
amount paid for that ammunition, or what condition it is in
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import CanneryWeb.Gettext
|
alias Cannery.{Ammo.Type, Containers, Containers.Container}
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.Ammo.Type
|
|
||||||
alias Cannery.{Accounts.User, Containers, Containers.Container}
|
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
:id,
|
:container_id,
|
||||||
:count,
|
:count,
|
||||||
|
:id,
|
||||||
|
:lot_number,
|
||||||
:notes,
|
:notes,
|
||||||
:price_paid,
|
:price_paid,
|
||||||
:lot_number,
|
:type_id
|
||||||
:staged,
|
|
||||||
:type_id,
|
|
||||||
:container_id
|
|
||||||
]}
|
]}
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "packs" do
|
schema "packs" do
|
||||||
field :count, :integer
|
field :count, :integer
|
||||||
|
field :lot_number, :string
|
||||||
field :notes, :string
|
field :notes, :string
|
||||||
field :price_paid, :float
|
field :price_paid, :float
|
||||||
field :staged, :boolean, default: false
|
|
||||||
field :lot_number, :string
|
|
||||||
field :purchased_on, :date
|
field :purchased_on, :date
|
||||||
|
|
||||||
belongs_to :type, Type
|
belongs_to :type, Type
|
||||||
field :container_id, :binary_id
|
field :container_id, :binary_id
|
||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: id(),
|
|
||||||
count: integer,
|
count: integer,
|
||||||
|
id: id(),
|
||||||
|
lot_number: String.t() | nil,
|
||||||
notes: String.t() | nil,
|
notes: String.t() | nil,
|
||||||
price_paid: float() | nil,
|
price_paid: float() | nil,
|
||||||
staged: boolean(),
|
|
||||||
lot_number: String.t() | nil,
|
|
||||||
purchased_on: Date.t(),
|
purchased_on: Date.t(),
|
||||||
type: Type.t() | nil,
|
type: Type.t() | nil,
|
||||||
type_id: Type.id(),
|
type_id: Type.id(),
|
||||||
container_id: Container.id(),
|
container_id: Container.id(),
|
||||||
user_id: User.id(),
|
user_id: User.id(),
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_pack :: %__MODULE__{}
|
@type new_pack :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
@ -70,34 +61,55 @@ defmodule Cannery.Ammo.Pack do
|
|||||||
) :: changeset()
|
) :: changeset()
|
||||||
def create_changeset(
|
def create_changeset(
|
||||||
pack,
|
pack,
|
||||||
%Type{id: type_id},
|
type,
|
||||||
%Container{id: container_id, user_id: user_id},
|
container,
|
||||||
%User{id: user_id},
|
%User{id: user_id},
|
||||||
attrs
|
attrs
|
||||||
)
|
)
|
||||||
when is_binary(type_id) and is_binary(container_id) and is_binary(user_id) do
|
when is_binary(user_id) do
|
||||||
pack
|
type_id =
|
||||||
|> change(type_id: type_id)
|
case type do
|
||||||
|> change(user_id: user_id)
|
%Type{id: type_id} when is_binary(type_id) ->
|
||||||
|> change(container_id: container_id)
|
type_id
|
||||||
|> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :lot_number])
|
|
||||||
|> validate_number(:count, greater_than: 0)
|
_invalid_type ->
|
||||||
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|
nil
|
||||||
|> validate_length(:lot_number, max: 255)
|
end
|
||||||
|> validate_required([:count, :staged, :purchased_on, :type_id, :container_id, :user_id])
|
|
||||||
|
container_id =
|
||||||
|
case container do
|
||||||
|
%Container{id: container_id, user_id: ^user_id} when is_binary(container_id) ->
|
||||||
|
container_id
|
||||||
|
|
||||||
|
_invalid_container ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Invalid changeset, used to prompt user to select type and container
|
|
||||||
"""
|
|
||||||
def create_changeset(pack, _invalid_type, _invalid_container, _invalid_user, attrs) do
|
|
||||||
pack
|
pack
|
||||||
|> cast(attrs, [:type_id, :container_id])
|
|> change(type_id: type_id)
|
||||||
|> validate_required([:type_id, :container_id])
|
|> change(container_id: container_id)
|
||||||
|
|> change(user_id: user_id)
|
||||||
|
|> 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")
|
||||||
|
)
|
||||||
|> validate_number(:count, greater_than: 0)
|
|> validate_number(:count, greater_than: 0)
|
||||||
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|
||||||
|> validate_length(:lot_number, max: 255)
|
|> validate_length(:lot_number, max: 255)
|
||||||
|> add_error(:invalid, dgettext("errors", "Please select a type and container"))
|
|> validate_required([
|
||||||
|
:container_id,
|
||||||
|
:count,
|
||||||
|
:purchased_on,
|
||||||
|
:type_id,
|
||||||
|
:user_id
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@ -105,19 +117,22 @@ defmodule Cannery.Ammo.Pack do
|
|||||||
def update_changeset(pack, attrs, user) do
|
def update_changeset(pack, attrs, user) do
|
||||||
pack
|
pack
|
||||||
|> cast(attrs, [
|
|> cast(attrs, [
|
||||||
|
:container_id,
|
||||||
:count,
|
:count,
|
||||||
:price_paid,
|
|
||||||
:notes,
|
|
||||||
:staged,
|
|
||||||
:purchased_on,
|
|
||||||
:lot_number,
|
:lot_number,
|
||||||
:container_id
|
:notes,
|
||||||
|
:price_paid,
|
||||||
|
:purchased_on
|
||||||
])
|
])
|
||||||
|> validate_number(:count, greater_than_or_equal_to: 0)
|
|> validate_number(:count, greater_than_or_equal_to: 0)
|
||||||
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|
|> validate_number(:price_paid, greater_than_or_equal_to: 0)
|
||||||
|> validate_container_id(user)
|
|> validate_container_id(user)
|
||||||
|> validate_length(:lot_number, max: 255)
|
|> validate_length(:lot_number, max: 255)
|
||||||
|> validate_required([:count, :staged, :purchased_on, :container_id])
|
|> validate_required([
|
||||||
|
:container_id,
|
||||||
|
:count,
|
||||||
|
:purchased_on
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_container_id(changeset, user) do
|
defp validate_container_id(changeset, user) do
|
||||||
@ -137,7 +152,7 @@ defmodule Cannery.Ammo.Pack do
|
|||||||
@spec range_changeset(t() | new_pack(), attrs :: map()) :: changeset()
|
@spec range_changeset(t() | new_pack(), attrs :: map()) :: changeset()
|
||||||
def range_changeset(pack, attrs) do
|
def range_changeset(pack, attrs) do
|
||||||
pack
|
pack
|
||||||
|> cast(attrs, [:count, :staged])
|
|> cast(attrs, [:count])
|
||||||
|> validate_required([:count, :staged])
|
|> validate_required([:count])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,11 +5,8 @@ defmodule Cannery.Ammo.Type do
|
|||||||
Contains statistical information about the ammunition.
|
Contains statistical information about the ammunition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.Accounts.User
|
|
||||||
alias Cannery.Ammo.Pack
|
alias Cannery.Ammo.Pack
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
@ -46,8 +43,6 @@ defmodule Cannery.Ammo.Type do
|
|||||||
:shot_charge_weight,
|
:shot_charge_weight,
|
||||||
:dram_equivalent
|
:dram_equivalent
|
||||||
]}
|
]}
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "types" do
|
schema "types" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :desc, :string
|
field :desc, :string
|
||||||
@ -95,7 +90,7 @@ defmodule Cannery.Ammo.Type do
|
|||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
has_many :packs, Pack
|
has_many :packs, Pack
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
@ -134,8 +129,8 @@ defmodule Cannery.Ammo.Type do
|
|||||||
dram_equivalent: String.t() | nil,
|
dram_equivalent: String.t() | nil,
|
||||||
user_id: User.id(),
|
user_id: User.id(),
|
||||||
packs: [Pack.t()] | nil,
|
packs: [Pack.t()] | nil,
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_type :: %__MODULE__{}
|
@type new_type :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
|
@ -15,6 +15,7 @@ defmodule Cannery.Application do
|
|||||||
CanneryWeb.Telemetry,
|
CanneryWeb.Telemetry,
|
||||||
# Start the PubSub system
|
# Start the PubSub system
|
||||||
{Phoenix.PubSub, name: Cannery.PubSub},
|
{Phoenix.PubSub, name: Cannery.PubSub},
|
||||||
|
{DNSCluster, query: Application.get_env(:cannery, :dns_cluster_query) || :ignore},
|
||||||
# Start the Endpoint (http/https)
|
# Start the Endpoint (http/https)
|
||||||
CanneryWeb.Endpoint,
|
CanneryWeb.Endpoint,
|
||||||
# Add Oban
|
# Add Oban
|
||||||
|
@ -3,15 +3,15 @@ defmodule Cannery.Containers do
|
|||||||
The Containers context.
|
The Containers context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import CanneryWeb.Gettext
|
use Cannery, :context
|
||||||
import Ecto.Query, warn: false
|
alias Cannery.Ammo.Pack
|
||||||
alias Cannery.{Accounts.User, Ammo.Pack, Repo}
|
|
||||||
alias Cannery.Containers.{Container, ContainerTag, Tag}
|
alias Cannery.Containers.{Container, ContainerTag, Tag}
|
||||||
alias Ecto.{Changeset, Queryable}
|
|
||||||
|
|
||||||
@container_preloads [:tags]
|
@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()]
|
@type list_containers_options :: [list_containers_option()]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -22,7 +22,10 @@ defmodule Cannery.Containers do
|
|||||||
iex> list_containers(%User{id: 123})
|
iex> list_containers(%User{id: 123})
|
||||||
[%Container{}, ...]
|
[%Container{}, ...]
|
||||||
|
|
||||||
iex> list_containers(%User{id: 123}, search: "cool")
|
iex> list_containers(%User{id: 123},
|
||||||
|
...> search: "cool",
|
||||||
|
...> staged: true
|
||||||
|
...> )
|
||||||
[%Container{name: "my cool container"}, ...]
|
[%Container{name: "my cool container"}, ...]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -39,9 +42,16 @@ defmodule Cannery.Containers do
|
|||||||
preload: ^@container_preloads
|
preload: ^@container_preloads
|
||||||
)
|
)
|
||||||
|> list_containers_search(Keyword.get(opts, :search))
|
|> list_containers_search(Keyword.get(opts, :search))
|
||||||
|
|> list_containers_staged(Keyword.get(opts, :staged))
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
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()
|
@spec list_containers_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t()
|
||||||
defp list_containers_search(query, search) when search in ["", nil],
|
defp list_containers_search(query, search) when search in ["", nil],
|
||||||
do: query |> order_by([c: c], c.name)
|
do: query |> order_by([c: c], c.name)
|
||||||
|
@ -3,45 +3,44 @@ defmodule Cannery.Containers.Container do
|
|||||||
A container that holds ammunition and belongs to a user.
|
A container that holds ammunition and belongs to a user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
alias Cannery.{Containers.ContainerTag, Containers.Tag}
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
alias Cannery.{Accounts.User, Containers.ContainerTag, Containers.Tag}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
:id,
|
|
||||||
:name,
|
|
||||||
:desc,
|
:desc,
|
||||||
|
:id,
|
||||||
:location,
|
:location,
|
||||||
:type,
|
:name,
|
||||||
:tags
|
:staged,
|
||||||
|
:tags,
|
||||||
|
:type
|
||||||
]}
|
]}
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "containers" do
|
schema "containers" do
|
||||||
field :name, :string
|
|
||||||
field :desc, :string
|
field :desc, :string
|
||||||
field :location, :string
|
field :location, :string
|
||||||
|
field :name, :string
|
||||||
|
field :staged, :boolean, default: false
|
||||||
field :type, :string
|
field :type, :string
|
||||||
|
|
||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
|
|
||||||
many_to_many :tags, Tag, join_through: ContainerTag
|
many_to_many :tags, Tag, join_through: ContainerTag
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: id(),
|
|
||||||
name: String.t(),
|
|
||||||
desc: String.t(),
|
desc: String.t(),
|
||||||
|
id: id(),
|
||||||
location: String.t(),
|
location: String.t(),
|
||||||
|
name: String.t(),
|
||||||
|
staged: boolean(),
|
||||||
type: String.t(),
|
type: String.t(),
|
||||||
user_id: User.id(),
|
user_id: User.id(),
|
||||||
tags: [Tag.t()] | nil,
|
tags: [Tag.t()] | nil,
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_container :: %__MODULE__{}
|
@type new_container :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
@ -52,19 +51,40 @@ defmodule Cannery.Containers.Container do
|
|||||||
def create_changeset(container, %User{id: user_id}, attrs) do
|
def create_changeset(container, %User{id: user_id}, attrs) do
|
||||||
container
|
container
|
||||||
|> change(user_id: user_id)
|
|> 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(:name, max: 255)
|
||||||
|> validate_length(:type, max: 255)
|
|> validate_length(:type, max: 255)
|
||||||
|> validate_required([:name, :type, :user_id])
|
|> validate_required([
|
||||||
|
:name,
|
||||||
|
:staged,
|
||||||
|
:type,
|
||||||
|
:user_id
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec update_changeset(t() | new_container(), attrs :: map()) :: changeset()
|
@spec update_changeset(t() | new_container(), attrs :: map()) :: changeset()
|
||||||
def update_changeset(container, attrs) do
|
def update_changeset(container, attrs) do
|
||||||
container
|
container
|
||||||
|> cast(attrs, [:name, :desc, :type, :location])
|
|> cast(attrs, [
|
||||||
|
:desc,
|
||||||
|
:location,
|
||||||
|
:name,
|
||||||
|
:staged,
|
||||||
|
:type
|
||||||
|
])
|
||||||
|> validate_length(:name, max: 255)
|
|> validate_length(:name, max: 255)
|
||||||
|> validate_length(:type, max: 255)
|
|> validate_length(:type, max: 255)
|
||||||
|> validate_required([:name, :type])
|
|> validate_required([
|
||||||
|
:name,
|
||||||
|
:staged,
|
||||||
|
:type
|
||||||
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,18 +4,14 @@ defmodule Cannery.Containers.ContainerTag do
|
|||||||
Cannery.Containers.Tag.
|
Cannery.Containers.Tag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.Containers.{Container, Tag}
|
alias Cannery.Containers.{Container, Tag}
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "container_tags" do
|
schema "container_tags" do
|
||||||
belongs_to :container, Container
|
belongs_to :container, Container
|
||||||
belongs_to :tag, Tag
|
belongs_to :tag, Tag
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
@ -24,8 +20,8 @@ defmodule Cannery.Containers.ContainerTag do
|
|||||||
container_id: Container.id(),
|
container_id: Container.id(),
|
||||||
tag: Tag.t(),
|
tag: Tag.t(),
|
||||||
tag_id: Tag.id(),
|
tag_id: Tag.id(),
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_container_tag :: %__MODULE__{}
|
@type new_container_tag :: %__MODULE__{}
|
||||||
@type id :: UUID.t()
|
@type id :: UUID.t()
|
||||||
|
@ -4,10 +4,7 @@ defmodule Cannery.Containers.Tag do
|
|||||||
text and bg colors.
|
text and bg colors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Cannery, :schema
|
||||||
import Ecto.Changeset
|
|
||||||
alias Cannery.Accounts.User
|
|
||||||
alias Ecto.{Changeset, UUID}
|
|
||||||
|
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
@ -16,8 +13,6 @@ defmodule Cannery.Containers.Tag do
|
|||||||
:bg_color,
|
:bg_color,
|
||||||
:text_color
|
:text_color
|
||||||
]}
|
]}
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "tags" do
|
schema "tags" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :bg_color, :string
|
field :bg_color, :string
|
||||||
@ -25,7 +20,7 @@ defmodule Cannery.Containers.Tag do
|
|||||||
|
|
||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
|
|
||||||
timestamps()
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
@ -34,8 +29,8 @@ defmodule Cannery.Containers.Tag do
|
|||||||
bg_color: String.t(),
|
bg_color: String.t(),
|
||||||
text_color: String.t(),
|
text_color: String.t(),
|
||||||
user_id: User.id(),
|
user_id: User.id(),
|
||||||
inserted_at: NaiveDateTime.t(),
|
inserted_at: DateTime.t(),
|
||||||
updated_at: NaiveDateTime.t()
|
updated_at: DateTime.t()
|
||||||
}
|
}
|
||||||
@type new_tag() :: %__MODULE__{}
|
@type new_tag() :: %__MODULE__{}
|
||||||
@type id() :: UUID.t()
|
@type id() :: UUID.t()
|
||||||
|
@ -7,8 +7,8 @@ defmodule Cannery.Email do
|
|||||||
`lib/cannery_web/components/layouts/email_text.txt.eex` for text emails.
|
`lib/cannery_web/components/layouts/email_text.txt.eex` for text emails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
import CanneryWeb.Gettext
|
|
||||||
import Phoenix.Template
|
import Phoenix.Template
|
||||||
alias Cannery.Accounts.User
|
alias Cannery.Accounts.User
|
||||||
alias CanneryWeb.{EmailHTML, Layouts}
|
alias CanneryWeb.{EmailHTML, Layouts}
|
@ -14,7 +14,7 @@ defmodule Cannery.Logger do
|
|||||||
|> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
|
|> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
|
||||||
|> pretty_encode()
|
|> pretty_encode()
|
||||||
|
|
||||||
Logger.error("#{meta.reason}: #{data}")
|
Logger.error("Oban exception: #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event([:oban, :job, :start], measure, meta, _config) do
|
def handle_event([:oban, :job, :start], measure, meta, _config) do
|
||||||
|
@ -17,7 +17,7 @@ defmodule CanneryWeb do
|
|||||||
those modules here.
|
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
|
def router do
|
||||||
quote do
|
quote do
|
||||||
@ -42,9 +42,10 @@ defmodule CanneryWeb do
|
|||||||
formats: [:html, :json],
|
formats: [:html, :json],
|
||||||
layouts: [html: CanneryWeb.Layouts]
|
layouts: [html: CanneryWeb.Layouts]
|
||||||
|
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
|
||||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import CanneryWeb.Gettext
|
|
||||||
|
|
||||||
unquote(verified_routes())
|
unquote(verified_routes())
|
||||||
end
|
end
|
||||||
@ -84,8 +85,9 @@ defmodule CanneryWeb do
|
|||||||
defp html_helpers do
|
defp html_helpers do
|
||||||
quote do
|
quote do
|
||||||
use PhoenixHTMLHelpers
|
use PhoenixHTMLHelpers
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
import Phoenix.{Component, HTML, HTML.Form}
|
import Phoenix.{Component, HTML, HTML.Form}
|
||||||
import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers}
|
import CanneryWeb.{ErrorHelpers, CoreComponents, HTMLHelpers}
|
||||||
|
|
||||||
# Shortcut for generating JS commands
|
# Shortcut for generating JS commands
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
@ -33,7 +33,9 @@ defmodule CanneryWeb.Components.AddShotRecordComponent do
|
|||||||
) do
|
) do
|
||||||
params = shot_record_params |> process_params(pack)
|
params = shot_record_params |> process_params(pack)
|
||||||
|
|
||||||
changeset = %ShotRecord{} |> ShotRecord.create_changeset(current_user, pack, params)
|
changeset =
|
||||||
|
%ShotRecord{}
|
||||||
|
|> ShotRecord.create_changeset(current_user, pack, params)
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
case changeset |> Changeset.apply_action(:validate) do
|
case changeset |> Changeset.apply_action(:validate) do
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
||||||
<%= gettext("Record shots") %>
|
{gettext("Record shots")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
@ -13,49 +13,50 @@
|
|||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:if={@changeset.action && not @changeset.valid?()}
|
:if={@changeset.action && not @changeset.valid?}
|
||||||
class="invalid-feedback col-span-3 text-center"
|
class="invalid-feedback col-span-3 text-center"
|
||||||
>
|
>
|
||||||
<%= changeset_errors(@changeset) %>
|
{changeset_errors(@changeset)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600") %>
|
{label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600")}
|
||||||
<%= number_input(f, :ammo_left,
|
{number_input(f, :ammo_left,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: @pack.count - 1,
|
max: @pack.count - 1,
|
||||||
placeholder: gettext("Rounds left"),
|
placeholder: gettext("Rounds left"),
|
||||||
class: "input input-primary"
|
class: "input input-primary"
|
||||||
) %>
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="mx-2 my-1 text-sm btn btn-primary"
|
class="mx-2 my-1 text-sm btn btn-primary"
|
||||||
phx-click={JS.dispatch("cannery:set-zero", to: "#shot-record-form_ammo_left")}
|
phx-click={JS.dispatch("cannery:set-zero", to: "#shot-record-form_ammo_left")}
|
||||||
>
|
>
|
||||||
<%= gettext("Used up!") %>
|
{gettext("Used up!")}
|
||||||
</button>
|
</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") %>
|
{label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600")}
|
||||||
<%= textarea(f, :notes,
|
{textarea(f, :notes,
|
||||||
id: "add-shot-record-form-notes",
|
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
|
id: "add-shot-record-form-notes",
|
||||||
maxlength: 255,
|
maxlength: 255,
|
||||||
placeholder: gettext("Really great weather"),
|
phx_debounce: 300,
|
||||||
phx_update: "ignore"
|
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") %>
|
{label(f, :date, gettext("Date"), class: "title text-lg text-primary-600")}
|
||||||
<%= date_input(f, :date,
|
{date_input(f, :date,
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
phx_update: "ignore",
|
phx_update: "ignore",
|
||||||
value: Date.utc_today()
|
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",
|
class: "mx-auto btn btn-primary col-span-3",
|
||||||
phx_disable_with: dgettext("prompts", "Saving...")
|
phx_disable_with: dgettext("prompts", "Saving...")
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
"""
|
"""
|
||||||
use CanneryWeb, :live_component
|
use CanneryWeb, :live_component
|
||||||
alias Cannery.{Accounts.User, Ammo, Containers.Container}
|
alias Cannery.{Accounts.User, Ammo, Containers.Container}
|
||||||
|
alias CanneryWeb.Components.TableComponent
|
||||||
alias Ecto.UUID
|
alias Ecto.UUID
|
||||||
alias Phoenix.LiveView.{Rendered, Socket}
|
alias Phoenix.LiveView.{Rendered, Socket}
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
required(:id) => UUID.t(),
|
required(:id) => UUID.t(),
|
||||||
required(:current_user) => User.t(),
|
required(:current_user) => User.t(),
|
||||||
optional(:containers) => [Container.t()],
|
optional(:containers) => [Container.t()],
|
||||||
|
optional(:range) => Rendered.t(),
|
||||||
optional(:tag_actions) => Rendered.t(),
|
optional(:tag_actions) => Rendered.t(),
|
||||||
optional(:actions) => Rendered.t(),
|
optional(:actions) => Rendered.t(),
|
||||||
optional(any()) => any()
|
optional(any()) => any()
|
||||||
@ -23,6 +25,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(assigns)
|
|> assign(assigns)
|
||||||
|
|> assign_new(:range, fn -> [] end)
|
||||||
|> assign_new(:tag_actions, fn -> [] end)
|
|> assign_new(:tag_actions, fn -> [] end)
|
||||||
|> assign_new(:actions, fn -> [] end)
|
|> assign_new(:actions, fn -> [] end)
|
||||||
|> display_containers()
|
|> display_containers()
|
||||||
@ -35,6 +38,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
assigns: %{
|
assigns: %{
|
||||||
containers: containers,
|
containers: containers,
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
|
range: range,
|
||||||
tag_actions: tag_actions,
|
tag_actions: tag_actions,
|
||||||
actions: actions
|
actions: actions
|
||||||
}
|
}
|
||||||
@ -62,13 +66,22 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
end)
|
end)
|
||||||
|> Enum.concat([
|
|> Enum.concat([
|
||||||
%{label: gettext("Packs"), key: :packs, type: :integer},
|
%{label: gettext("Packs"), key: :packs, type: :integer},
|
||||||
%{label: gettext("Rounds"), key: :rounds, type: :integer},
|
%{label: gettext("Rounds"), key: :rounds, type: :integer}
|
||||||
|
])
|
||||||
|
|> Enum.concat(
|
||||||
|
[
|
||||||
%{label: gettext("Tags"), key: :tags, type: :tags},
|
%{label: gettext("Tags"), key: :tags, type: :tags},
|
||||||
%{label: gettext("Actions"), key: :actions, sortable: false, type: :actions}
|
%{label: gettext("Actions"), key: :actions, sortable: false, type: :actions}
|
||||||
])
|
]
|
||||||
|
|> TableComponent.maybe_compose_columns(
|
||||||
|
%{label: gettext("Range"), key: :range},
|
||||||
|
range != []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
extra_data = %{
|
extra_data = %{
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
|
range: range,
|
||||||
tag_actions: tag_actions,
|
tag_actions: tag_actions,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
pack_count:
|
pack_count:
|
||||||
@ -122,7 +135,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-wrap justify-center items-center">
|
<div class="flex flex-wrap justify-center items-center">
|
||||||
<.link navigate={~p"/container/#{@id}"} class="link">
|
<.link navigate={~p"/container/#{@id}"} class="link">
|
||||||
<%= @name %>
|
{@name}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
"""}
|
"""}
|
||||||
@ -136,6 +149,15 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
round_count |> Map.get(container_id, 0)
|
round_count |> Map.get(container_id, 0)
|
||||||
end
|
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
|
defp get_value_for_key(:tags, container, %{tag_actions: tag_actions}) do
|
||||||
assigns = %{tag_actions: tag_actions, container: container}
|
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">
|
<div class="flex flex-wrap justify-center items-center">
|
||||||
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
|
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
|
||||||
|
|
||||||
<%= render_slot(@tag_actions, @container) %>
|
{render_slot(@tag_actions, @container)}
|
||||||
</div>
|
</div>
|
||||||
"""}
|
"""}
|
||||||
end
|
end
|
||||||
@ -159,7 +181,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
|||||||
assigns = %{actions: actions, container: container}
|
assigns = %{actions: actions, container: container}
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@actions, @container) %>
|
{render_slot(@actions, @container)}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ defmodule CanneryWeb.CoreComponents do
|
|||||||
"""
|
"""
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
use CanneryWeb, :verified_routes
|
use CanneryWeb, :verified_routes
|
||||||
import CanneryWeb.{Gettext, HTMLHelpers}
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
import CanneryWeb.HTMLHelpers
|
||||||
alias Cannery.{Accounts, Accounts.Invite, Accounts.User}
|
alias Cannery.{Accounts, Accounts.Invite, Accounts.User}
|
||||||
alias Cannery.{Ammo, Ammo.Pack}
|
alias Cannery.{Ammo, Ammo.Pack}
|
||||||
alias Cannery.{Containers.Container, Containers.Tag}
|
alias Cannery.{Containers.Container, Containers.Tag}
|
||||||
@ -135,14 +136,26 @@ defmodule CanneryWeb.CoreComponents do
|
|||||||
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
|
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
|
||||||
|
|
||||||
@doc """
|
@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
|
user's local timezone
|
||||||
"""
|
"""
|
||||||
def datetime(assigns)
|
def datetime(assigns)
|
||||||
|
|
||||||
@spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
|
attr :name, :string, required: true
|
||||||
defp cast_datetime(%NaiveDateTime{} = datetime) do
|
|
||||||
datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
|
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
|
end
|
||||||
|
|
||||||
defp cast_datetime(_datetime), do: ""
|
defp cast_datetime(_datetime), do: ""
|
||||||
|
@ -2,40 +2,40 @@
|
|||||||
id={"container-#{@container.id}"}
|
id={"container-#{@container.id}"}
|
||||||
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
|
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
|
||||||
flex flex-col justify-around items-center space-y-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"
|
transition-all duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<.link navigate={~p"/container/#{@container}"} class="link">
|
<.link navigate={~p"/container/#{@container}"} class="link">
|
||||||
<h1 class="px-4 py-2 rounded-lg title text-xl">
|
<h1 class="px-4 py-2 rounded-lg title text-xl">
|
||||||
<%= @container.name %>
|
{@container.name}
|
||||||
</h1>
|
</h1>
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center space-y-2">
|
<div class="flex flex-col justify-center items-center space-y-2">
|
||||||
<span :if={@container.desc} class="rounded-lg title text-lg">
|
<span :if={@container.desc} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Description:") %>
|
{gettext("Description:")}
|
||||||
<%= @container.desc %>
|
{@container.desc}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="rounded-lg title text-lg">
|
||||||
<%= gettext("Type:") %>
|
{gettext("Type:")}
|
||||||
<%= @container.type %>
|
{@container.type}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@container.location} class="rounded-lg title text-lg">
|
<span :if={@container.location} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Location:") %>
|
{gettext("Location:")}
|
||||||
<%= @container.location %>
|
{@container.location}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<%= if Ammo.get_packs_count(@current_user, container_id: @container.id) != 0 do %>
|
<%= if Ammo.get_packs_count(@current_user, container_id: @container.id) != 0 do %>
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="rounded-lg title text-lg">
|
||||||
<%= gettext("Packs:") %>
|
{gettext("Packs:")}
|
||||||
<%= Ammo.get_packs_count(@current_user, container_id: @container.id) %>
|
{Ammo.get_packs_count(@current_user, container_id: @container.id)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="rounded-lg title text-lg">
|
||||||
<%= gettext("Rounds:") %>
|
{gettext("Rounds:")}
|
||||||
<%= Ammo.get_round_count(@current_user, container_id: @container.id) %>
|
{Ammo.get_round_count(@current_user, container_id: @container.id)}
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@ -45,7 +45,7 @@
|
|||||||
>
|
>
|
||||||
<.simple_tag_card :for={tag <- @container.tags} tag={tag} />
|
<.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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -53,6 +53,6 @@
|
|||||||
:if={assigns |> Map.has_key?(:inner_block)}
|
:if={assigns |> Map.has_key?(:inner_block)}
|
||||||
class="flex space-x-4 justify-center items-center"
|
class="flex space-x-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
|
<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>
|
</time>
|
||||||
|
@ -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>
|
@ -1,3 +1,3 @@
|
|||||||
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
|
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
|
||||||
<%= cast_datetime(@datetime) %>
|
{cast_datetime(@datetime)}
|
||||||
</time>
|
</time>
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
|
<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">
|
transition-all duration-300 ease-in-out">
|
||||||
<h1 class="title text-xl">
|
<h1 class="title text-xl">
|
||||||
<%= @invite.name %>
|
{@invite.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<%= if @invite.disabled_at |> is_nil() do %>
|
<%= if @invite.disabled_at |> is_nil() do %>
|
||||||
<h2 class="title text-md">
|
<h2 class="title text-md">
|
||||||
<%= if @invite.uses_left do %>
|
<%= if @invite.uses_left do %>
|
||||||
<%= gettext(
|
{gettext(
|
||||||
"Uses Left: %{uses_left_count}",
|
"Uses Left: %{uses_left_count}",
|
||||||
uses_left_count: @invite.uses_left
|
uses_left_count: @invite.uses_left
|
||||||
) %>
|
)}
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= gettext("Uses Left: Unlimited") %>
|
{gettext("Uses Left: Unlimited")}
|
||||||
<% end %>
|
<% end %>
|
||||||
</h2>
|
</h2>
|
||||||
<% else %>
|
<% else %>
|
||||||
<h2 class="title text-md">
|
<h2 class="title text-md">
|
||||||
<%= gettext("Invite Disabled") %>
|
{gettext("Invite Disabled")}
|
||||||
</h2>
|
</h2>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@ -28,19 +28,19 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 :if={@use_count && @use_count != 0} class="title text-md">
|
<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>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||||
<code
|
<code
|
||||||
id={"code-#{@invite.id}"}
|
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
|
phx-no-format
|
||||||
><%= url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
|
><%= 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>
|
||||||
|
|
||||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
patch={@return_to}
|
patch={@return_to}
|
||||||
id="close"
|
id="close"
|
||||||
class="absolute top-8 right-10
|
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"
|
transition-all duration-500 ease-in-out"
|
||||||
phx-remove={hide_modal()}
|
phx-remove={hide_modal()}
|
||||||
aria-label={gettext("Close modal")}
|
aria-label={gettext("Close modal")}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,66 +2,66 @@
|
|||||||
id={"pack-#{@pack.id}"}
|
id={"pack-#{@pack.id}"}
|
||||||
class="mx-4 my-2 px-8 py-4
|
class="mx-4 my-2 px-8 py-4
|
||||||
flex flex-col justify-center items-center
|
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"
|
transition-all duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<.link navigate={~p"/ammo/show/#{@pack}"} class="mb-2 link">
|
<.link navigate={~p"/ammo/show/#{@pack}"} class="mb-2 link">
|
||||||
<h1 class="title text-xl title-primary-500">
|
<h1 class="title text-xl title-primary-500">
|
||||||
<%= @pack.type.name %>
|
{@pack.type.name}
|
||||||
</h1>
|
</h1>
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center">
|
<div class="flex flex-col justify-center items-center">
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="rounded-lg title text-lg">
|
||||||
<%= gettext("Count:") %>
|
{gettext("Count:")}
|
||||||
<%= if @pack.count == 0, do: gettext("Empty"), else: @pack.count %>
|
{if @pack.count == 0, do: gettext("Empty"), else: @pack.count}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@original_count && @original_count != @pack.count} class="rounded-lg title text-lg">
|
<span :if={@original_count && @original_count != @pack.count} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Original Count:") %>
|
{gettext("Original Count:")}
|
||||||
<%= @original_count %>
|
{@original_count}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@pack.notes} class="rounded-lg title text-lg">
|
<span :if={@pack.notes} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Notes:") %>
|
{gettext("Notes:")}
|
||||||
<%= @pack.notes %>
|
{@pack.notes}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@pack.purchased_on} class="rounded-lg title text-lg">
|
<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} />
|
<.date id={"#{@pack.id}-purchased-on"} date={@pack.purchased_on} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@last_used_date} class="rounded-lg title text-lg">
|
<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} />
|
<.date id={"#{@pack.id}-last-used-on"} date={@last_used_date} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@pack.price_paid} class="rounded-lg title text-lg">
|
<span :if={@pack.price_paid} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Price paid:") %>
|
{gettext("Price paid:")}
|
||||||
<%= gettext("$%{amount}", amount: display_currency(@pack.price_paid)) %>
|
{gettext("$%{amount}", amount: display_currency(@pack.price_paid))}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@cpr} class="rounded-lg title text-lg">
|
<span :if={@cpr} class="rounded-lg title text-lg">
|
||||||
<%= gettext("CPR:") %>
|
{gettext("CPR:")}
|
||||||
<%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
|
{gettext("$%{amount}", amount: display_currency(@cpr))}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@pack.lot_number} class="rounded-lg title text-lg">
|
<span :if={@pack.lot_number} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Lot number:") %>
|
{gettext("Lot number:")}
|
||||||
<%= @pack.lot_number %>
|
{@pack.lot_number}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@container} class="rounded-lg title text-lg">
|
<span :if={@container} class="rounded-lg title text-lg">
|
||||||
<%= gettext("Container:") %>
|
{gettext("Container:")}
|
||||||
|
|
||||||
<.link navigate={~p"/container/#{@container}"} class="link">
|
<.link navigate={~p"/container/#{@container}"} class="link">
|
||||||
<%= @container.name %>
|
{@container.name}
|
||||||
</.link>
|
</.link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :if={@inner_block} class="mt-4 flex space-x-4 justify-center items-center">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
|
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}"}
|
style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
|
||||||
>
|
>
|
||||||
<%= @tag.name %>
|
{@tag.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<div
|
<div
|
||||||
id={"tag-#{@tag.id}"}
|
id={"tag-#{@tag.id}"}
|
||||||
class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
|
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"
|
transition-all duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<.simple_tag_card tag={@tag} />
|
<.simple_tag_card tag={@tag} />
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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
|
<input
|
||||||
id={@id || @action}
|
id={@id || @action}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -12,19 +12,17 @@
|
|||||||
else: %{"phx-click": @action, "phx-value-value": @value}
|
else: %{"phx-click": @action, "phx-value-value": @value}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div class="w-11 h-6 bg-gray-300 rounded-full peer
|
<div class="w-11 h-6 bg-zinc-300 rounded-full peer
|
||||||
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
|
peer-checked:bg-zinc-600 peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||||
peer-checked:bg-gray-600
|
after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300
|
||||||
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
|
|
||||||
after:border after:rounded-full after:h-5 after:w-5
|
after:border after:rounded-full after:h-5 after:w-5
|
||||||
after:transition-all after:duration-250 after:ease-in-out
|
after:transition-all after:duration-250 after:ease-in-out
|
||||||
transition-colors duration-250 ease-in-out">
|
transition-colors duration-250 ease-in-out">
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
id={"#{@id || @action}-label"}
|
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>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<span class="mx-2 my-1">
|
<span class="mx-2 my-1">
|
||||||
|
|
|
|
||||||
</span>
|
</span>
|
||||||
<%= @title_content %>
|
{@title_content}
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -25,37 +25,37 @@
|
|||||||
<%= if @current_user do %>
|
<%= if @current_user do %>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link navigate={~p"/tags"} class="text-white hover:underline">
|
<.link navigate={~p"/tags"} class="text-white hover:underline">
|
||||||
<%= gettext("Tags") %>
|
{gettext("Tags")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link navigate={~p"/containers"} class="text-white hover:underline">
|
<.link navigate={~p"/containers"} class="text-white hover:underline">
|
||||||
<%= gettext("Containers") %>
|
{gettext("Containers")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link navigate={~p"/catalog"} class="text-white hover:underline">
|
<.link navigate={~p"/catalog"} class="text-white hover:underline">
|
||||||
<%= gettext("Catalog") %>
|
{gettext("Catalog")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link navigate={~p"/ammo"} class="text-white hover:underline">
|
<.link navigate={~p"/ammo"} class="text-white hover:underline">
|
||||||
<%= gettext("Ammo") %>
|
{gettext("Ammo")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link navigate={~p"/range"} class="text-white hover:underline">
|
<.link navigate={~p"/range"} class="text-white hover:underline">
|
||||||
<%= gettext("Range") %>
|
{gettext("Range")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
|
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
|
||||||
<.link navigate={~p"/invites"} class="text-white hover:underline">
|
<.link navigate={~p"/invites"} class="text-white hover:underline">
|
||||||
<%= gettext("Invites") %>
|
{gettext("Invites")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link href={~p"/users/settings"} class="text-white hover:underline truncate">
|
<.link href={~p"/users/settings"} class="text-white hover:underline truncate">
|
||||||
<%= @current_user.email %>
|
{@current_user.email}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
@ -86,12 +86,12 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||||
<.link href={~p"/users/register"} class="text-white hover:underline truncate">
|
<.link href={~p"/users/register"} class="text-white hover:underline truncate">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link href={~p"/users/log_in"} class="text-white hover:underline truncate">
|
<.link href={~p"/users/log_in"} class="text-white hover:underline truncate">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
<div
|
<div
|
||||||
id={"user-#{@user.id}"}
|
id={"user-#{@user.id}"}
|
||||||
class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
|
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"
|
transition-all duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
||||||
<%= @user.email %>
|
{@user.email}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
||||||
<p>
|
<p>
|
||||||
<%= if @user.confirmed_at do %>
|
<%= if @user.confirmed_at do %>
|
||||||
<%= gettext(
|
{gettext(
|
||||||
"User was confirmed at%{confirmed_datetime}",
|
"User was confirmed at%{confirmed_datetime}",
|
||||||
confirmed_datetime: ""
|
confirmed_datetime: ""
|
||||||
) %>
|
)}
|
||||||
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
|
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= gettext("Email unconfirmed") %>
|
{gettext("Email unconfirmed")}
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= gettext(
|
{gettext(
|
||||||
"User registered on%{registered_datetime}",
|
"User registered on%{registered_datetime}",
|
||||||
registered_datetime: ""
|
registered_datetime: ""
|
||||||
) %>
|
)}
|
||||||
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
|
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
|
||||||
</p>
|
</p>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
<span style="margin-bottom: 0.75em; font-size: 1.5em;">
|
<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>
|
</span>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<span style="margin-bottom: 1em; font-size: 1.25em;">
|
<span style="margin-bottom: 1em; font-size: 1.25em;">
|
||||||
<%= dgettext("emails", "Welcome to Cannery") %>
|
{dgettext("emails", "Welcome to Cannery")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<br />
|
<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 />
|
<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 />
|
<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>
|
</div>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
|
<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>
|
</span>
|
||||||
|
|
||||||
<br />
|
<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 />
|
<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 />
|
<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>
|
</div>
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
|
<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>
|
</span>
|
||||||
|
|
||||||
<br />
|
<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 />
|
<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 />
|
<br />
|
||||||
|
|
||||||
<%= dgettext(
|
{dgettext(
|
||||||
"emails",
|
"emails",
|
||||||
"If you didn't request this change from Cannery, please ignore this."
|
"If you didn't request this change from Cannery, please ignore this."
|
||||||
) %>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="info"
|
phx-value-key="info"
|
||||||
>
|
>
|
||||||
<%= live_flash(@flash, "info") %>
|
{Phoenix.Flash.get(@flash, :info)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@ -20,13 +20,13 @@
|
|||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="error"
|
phx-value-key="error"
|
||||||
>
|
>
|
||||||
<%= live_flash(@flash, "error") %>
|
{Phoenix.Flash.get(@flash, :error)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="mx-4 sm:mx-8 md:mx-16 flex flex-col justify-center items-stretch">
|
<div class="mx-4 sm:mx-8 md:mx-16 flex flex-col justify-center items-stretch">
|
||||||
<%= @inner_content %>
|
{@inner_content}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -40,6 +40,6 @@
|
|||||||
<i class="fas fa-fade text-md fa-satellite-dish"></i>
|
<i class="fas fa-fade text-md fa-satellite-dish"></i>
|
||||||
|
|
||||||
<h1 class="title text-md title-primary-500">
|
<h1 class="title text-md title-primary-500">
|
||||||
<%= gettext("Reconnecting...") %>
|
{gettext("Reconnecting...")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>
|
<title>
|
||||||
<%= @email.subject %>
|
{@email.subject}
|
||||||
</title>
|
</title>
|
||||||
</head>
|
</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;">
|
<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;" />
|
<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"/"}>
|
<a style="color: rgb(31, 31, 31);" href={~p"/"}>
|
||||||
<%= dgettext(
|
{dgettext(
|
||||||
"emails",
|
"emails",
|
||||||
"This email was sent from Cannery, the self-hosted firearm tracker website."
|
"This email was sent from Cannery, the self-hosted firearm tracker website."
|
||||||
) %>
|
)}
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1 +1 @@
|
|||||||
<%= @inner_block %>
|
{@inner_block}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
<!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>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="csrf-token" content={get_csrf_token()} />
|
||||||
<%= csrf_meta_tag() %>
|
|
||||||
<link rel="shortcut icon" type="image/jpg" href={~p"/images/cannery.svg"} />
|
<link rel="shortcut icon" type="image/jpg" href={~p"/images/cannery.svg"} />
|
||||||
<.live_title suffix={" | #{gettext("Cannery")}"}>
|
<.live_title suffix={" | #{gettext("Cannery")}"}>
|
||||||
<%= assigns[:page_title] || gettext("Cannery") %>
|
{assigns[:page_title] || gettext("Cannery")}
|
||||||
</.live_title>
|
</.live_title>
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/style.css"} />
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="m-0 p-0 w-full h-full subpixel-antialiased">
|
<body class="p-0 m-0 w-full h-full subpixel-antialiased">
|
||||||
<%= @inner_content %>
|
{@inner_content}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -74,17 +74,17 @@ defmodule CanneryWeb.Components.MovePackComponent do
|
|||||||
~H"""
|
~H"""
|
||||||
<div class="w-full flex flex-col space-y-8 justify-center items-center">
|
<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">
|
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
||||||
<%= dgettext("actions", "Move ammo") %>
|
{dgettext("actions", "Move ammo")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<%= if @containers |> Enum.empty?() do %>
|
<%= if @containers |> Enum.empty?() do %>
|
||||||
<h2 class="title text-xl text-primary-600">
|
<h2 class="title text-xl text-primary-600">
|
||||||
<%= gettext("No other containers") %>
|
{gettext("No other containers")}
|
||||||
<%= display_emoji("😔") %>
|
{display_emoji("😔")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<.link navigate={~p"/containers/new"} class="btn btn-primary">
|
<.link navigate={~p"/containers/new"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Add another container!") %>
|
{dgettext("actions", "Add another container!")}
|
||||||
</.link>
|
</.link>
|
||||||
<% else %>
|
<% else %>
|
||||||
<.live_component
|
<.live_component
|
||||||
@ -120,7 +120,7 @@ defmodule CanneryWeb.Components.MovePackComponent do
|
|||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
phx-value-container_id={@container.id}
|
phx-value-container_id={@container.id}
|
||||||
>
|
>
|
||||||
<%= dgettext("actions", "Select") %>
|
{dgettext("actions", "Select")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -170,7 +170,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
|
|||||||
|
|
||||||
{type_name,
|
{type_name,
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@type_block, @type) %>
|
{render_slot(@type_block, @type)}
|
||||||
"""}
|
"""}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -196,18 +196,17 @@ defmodule CanneryWeb.Components.PackTableComponent do
|
|||||||
<%= if @last_used_date do %>
|
<%= if @last_used_date do %>
|
||||||
<.date id={"#{@id}-last-used-date"} date={@last_used_date} />
|
<.date id={"#{@id}-last-used-date"} date={@last_used_date} />
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= gettext("Never used") %>
|
{gettext("Never used")}
|
||||||
<% end %>
|
<% end %>
|
||||||
"""}
|
"""}
|
||||||
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}
|
assigns = %{range: range, pack: pack}
|
||||||
|
|
||||||
{staged,
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@range, @pack) %>
|
{render_slot(@range, @pack)}
|
||||||
"""}
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_value_for_key(
|
defp get_value_for_key(
|
||||||
@ -223,7 +222,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
|
|||||||
assigns = %{actions: actions, pack: pack}
|
assigns = %{actions: actions, pack: pack}
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@actions, @pack) %>
|
{render_slot(@actions, @pack)}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -244,7 +243,7 @@ defmodule CanneryWeb.Components.PackTableComponent do
|
|||||||
|
|
||||||
{container_name,
|
{container_name,
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@container_block, {@pack, @container}) %>
|
{render_slot(@container_block, {@pack, @container})}
|
||||||
"""}
|
"""}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
|
|||||||
{pack.type.name,
|
{pack.type.name,
|
||||||
~H"""
|
~H"""
|
||||||
<.link navigate={~p"/ammo/show/#{@pack}"} class="link">
|
<.link navigate={~p"/ammo/show/#{@pack}"} class="link">
|
||||||
<%= @pack.type.name %>
|
{@pack.type.name}
|
||||||
</.link>
|
</.link>
|
||||||
"""}
|
"""}
|
||||||
end
|
end
|
||||||
@ -115,7 +115,7 @@ defmodule CanneryWeb.Components.ShotRecordTableComponent do
|
|||||||
assigns = %{actions: actions, shot_record: shot_record}
|
assigns = %{actions: actions, shot_record: shot_record}
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@actions, @shot_record) %>
|
{render_slot(@actions, @shot_record)}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ defmodule CanneryWeb.Components.TableComponent do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use CanneryWeb, :live_component
|
use CanneryWeb, :live_component
|
||||||
|
alias Cannery.{ComparableDate, ComparableDateTime}
|
||||||
alias Phoenix.LiveView.Socket
|
alias Phoenix.LiveView.Socket
|
||||||
require Integer
|
require Integer
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ defmodule CanneryWeb.Components.TableComponent do
|
|||||||
sort_mode: initial_sort_mode
|
sort_mode: initial_sort_mode
|
||||||
)
|
)
|
||||||
|> assign_new(:row_class, fn -> "bg-white" end)
|
|> 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}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
@ -110,7 +111,7 @@ defmodule CanneryWeb.Components.TableComponent do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
|
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
|
rows
|
||||||
|> Enum.sort_by(
|
|> Enum.sort_by(
|
||||||
fn row ->
|
fn row ->
|
||||||
|
@ -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">
|
<table class="min-w-full table-auto text-center bg-white">
|
||||||
<thead class="border-b border-primary-600">
|
<thead class="border-b border-primary-600">
|
||||||
<tr>
|
<tr>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
<i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
|
<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 %>
|
<%= if @last_sort_key == key do %>
|
||||||
<%= case @sort_mode do %>
|
<%= case @sort_mode do %>
|
||||||
<% :asc -> %>
|
<% :asc -> %>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<% else %>
|
<% else %>
|
||||||
<th class={["p-2 cursor-not-allowed", column[:class]]}>
|
<th class={["p-2 cursor-not-allowed", column[:class]]}>
|
||||||
<%= label %>
|
{label}
|
||||||
</th>
|
</th>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -41,9 +41,9 @@
|
|||||||
<td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
|
<td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
|
||||||
<%= case values |> Map.get(key) do %>
|
<%= case values |> Map.get(key) do %>
|
||||||
<% {_custom_sort_value, value} -> %>
|
<% {_custom_sort_value, value} -> %>
|
||||||
<%= value %>
|
{value}
|
||||||
<% value -> %>
|
<% value -> %>
|
||||||
<%= value %>
|
{value}
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -278,7 +278,7 @@ defmodule CanneryWeb.Components.TypeTableComponent do
|
|||||||
{type_name,
|
{type_name,
|
||||||
~H"""
|
~H"""
|
||||||
<.link navigate={~p"/type/#{@id}"} class="link">
|
<.link navigate={~p"/type/#{@id}"} class="link">
|
||||||
<%= @name %>
|
{@name}
|
||||||
</.link>
|
</.link>
|
||||||
"""}
|
"""}
|
||||||
end
|
end
|
||||||
@ -287,7 +287,7 @@ defmodule CanneryWeb.Components.TypeTableComponent do
|
|||||||
assigns = %{actions: actions, type: type}
|
assigns = %{actions: actions, type: type}
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= render_slot(@actions, @type) %>
|
{render_slot(@actions, @type)}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>
|
<title>
|
||||||
<%= dgettext("errors", "Error") %> | <%= gettext("Cannery") %>
|
{dgettext("errors", "Error")} | {gettext("Cannery")}
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
<script defer type="text/javascript" src="/js/app.js">
|
<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="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">
|
<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">
|
<h1 class="title text-primary-600 text-3xl">
|
||||||
<%= @error_string %>
|
{@error_string}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<hr class="w-full hr" />
|
<hr class="w-full hr" />
|
||||||
|
|
||||||
<.link href={~p"/"} class="link title text-primary-600 text-lg">
|
<.link href={~p"/"} class="link title text-primary-600 text-lg">
|
||||||
<%= dgettext("errors", "Go back home") %>
|
{dgettext("errors", "Go back home")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
defmodule CanneryWeb.ErrorJSON do
|
defmodule CanneryWeb.ErrorJSON do
|
||||||
import CanneryWeb.Gettext
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
|
||||||
def render(template, _assigns) do
|
def render(template, _assigns) do
|
||||||
error_string =
|
error_string =
|
||||||
|
@ -4,9 +4,9 @@ defmodule CanneryWeb.UserAuth do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use CanneryWeb, :verified_routes
|
use CanneryWeb, :verified_routes
|
||||||
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
import CanneryWeb.Gettext
|
|
||||||
alias Cannery.{Accounts, Accounts.User}
|
alias Cannery.{Accounts, Accounts.User}
|
||||||
|
|
||||||
# Make the remember me cookie valid for 60 days.
|
# Make the remember me cookie valid for 60 days.
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
defmodule CanneryWeb.UserConfirmationController do
|
defmodule CanneryWeb.UserConfirmationController do
|
||||||
use CanneryWeb, :controller
|
use CanneryWeb, :controller
|
||||||
|
|
||||||
import CanneryWeb.Gettext
|
|
||||||
alias Cannery.Accounts
|
alias Cannery.Accounts
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<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">
|
<h1 class="title text-primary-600 text-xl">
|
||||||
<%= dgettext("actions", "Resend confirmation instructions") %>
|
{dgettext("actions", "Resend confirmation instructions")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
@ -10,22 +10,22 @@
|
|||||||
action={~p"/users/confirm"}
|
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"
|
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") %>
|
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
|
||||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
{email_input(f, :email, 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"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<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">
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
defmodule CanneryWeb.UserRegistrationController do
|
defmodule CanneryWeb.UserRegistrationController do
|
||||||
use CanneryWeb, :controller
|
use CanneryWeb, :controller
|
||||||
import CanneryWeb.Gettext
|
|
||||||
alias Cannery.{Accounts, Accounts.Invites}
|
alias Cannery.{Accounts, Accounts.Invites}
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<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">
|
<h1 class="title text-primary-600 text-xl">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
@ -9,42 +9,47 @@
|
|||||||
action={~p"/users/register"}
|
action={~p"/users/register"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
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">
|
<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>
|
</p>
|
||||||
|
|
||||||
<%= if @invite_token do %>
|
<%= if @invite_token do %>
|
||||||
<%= hidden_input(f, :invite_token, value: @invite_token) %>
|
{hidden_input(f, :invite_token, value: @invite_token)}
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
|
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
|
||||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
{email_input(f, :email, required: true, class: "input input-primary col-span-2")}
|
||||||
<%= error_tag(f, :email, "col-span-3") %>
|
{error_tag(f, :email, "col-span-3")}
|
||||||
|
|
||||||
<%= label(f, :password, gettext("Password"), class: "title text-lg text-primary-600") %>
|
{label(f, :password, gettext("Password"), class: "title text-lg text-primary-600")}
|
||||||
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
|
{password_input(f, :password, required: true, class: "input input-primary col-span-2")}
|
||||||
<%= error_tag(f, :password, "col-span-3") %>
|
{error_tag(f, :password, "col-span-3")}
|
||||||
|
|
||||||
<%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600") %>
|
{label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600")}
|
||||||
<%= select(
|
{select(
|
||||||
f,
|
f,
|
||||||
:locale,
|
:locale,
|
||||||
[{gettext("English"), "en_US"}],
|
[
|
||||||
|
{"English", "en_US"},
|
||||||
|
{"Deutsch", "de"},
|
||||||
|
{"Français", "fr"},
|
||||||
|
{"Español", "es"}
|
||||||
|
],
|
||||||
class: "input input-primary col-span-2"
|
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>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Forgot your password?") %>
|
{dgettext("actions", "Forgot your password?")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@ defmodule CanneryWeb.UserResetPasswordController do
|
|||||||
# leaked token giving the user access to the account.
|
# leaked token giving the user access to the account.
|
||||||
def update(conn, %{"user" => user_params}) do
|
def update(conn, %{"user" => user_params}) do
|
||||||
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
||||||
{:ok, _} ->
|
{:ok, _socket} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|
||||||
|> redirect(to: ~p"/users/log_in")
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<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">
|
<h1 class="title text-primary-600 text-xl">
|
||||||
<%= dgettext("actions", "Reset password") %>
|
{dgettext("actions", "Reset password")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
@ -9,36 +9,36 @@
|
|||||||
action={~p"/users/reset_password/#{@token}"}
|
action={~p"/users/reset_password/#{@token}"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
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">
|
<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>
|
</p>
|
||||||
|
|
||||||
<%= label(f, :password, gettext("New password"), class: "title text-lg text-primary-600") %>
|
{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") %>
|
{password_input(f, :password, required: true, class: "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"
|
class: "title text-lg text-primary-600"
|
||||||
) %>
|
)}
|
||||||
<%= password_input(f, :password_confirmation,
|
{password_input(f, :password_confirmation,
|
||||||
required: true,
|
required: true,
|
||||||
class: "input input-primary col-span-2"
|
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"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<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">
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<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">
|
<h1 class="title text-primary-600 text-xl">
|
||||||
<%= dgettext("actions", "Forgot your password?") %>
|
{dgettext("actions", "Forgot your password?")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
@ -10,22 +10,22 @@
|
|||||||
action={~p"/users/reset_password"}
|
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"
|
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") %>
|
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
|
||||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
{email_input(f, :email, 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"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<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">
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<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">
|
<h1 class="title text-primary-600 text-xl">
|
||||||
<%= dgettext("actions", "Log in") %>
|
{dgettext("actions", "Log in")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.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"
|
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">
|
<p :if={@error_message} class="alert alert-danger col-span-3">
|
||||||
<%= @error_message %>
|
{@error_message}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %>
|
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-600")}
|
||||||
<%= email_input(f, :email,
|
{email_input(f, :email,
|
||||||
autocomplete: :email,
|
autocomplete: :email,
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
required: true
|
required: true
|
||||||
) %>
|
)}
|
||||||
|
|
||||||
<%= label(f, :password, gettext("Password"), class: "title text-lg text-primary-600") %>
|
{label(f, :password, gettext("Password"), class: "title text-lg text-primary-600")}
|
||||||
<%= password_input(f, :password,
|
{password_input(f, :password,
|
||||||
autocomplete: "current-password",
|
autocomplete: "current-password",
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
required: true
|
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"
|
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>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<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">
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Register") %>
|
{dgettext("actions", "Register")}
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Forgot your password?") %>
|
{dgettext("actions", "Forgot your password?")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
defmodule CanneryWeb.UserSettingsController do
|
defmodule CanneryWeb.UserSettingsController do
|
||||||
use CanneryWeb, :controller
|
use CanneryWeb, :controller
|
||||||
import CanneryWeb.Gettext
|
|
||||||
alias Cannery.Accounts
|
alias Cannery.Accounts
|
||||||
alias CanneryWeb.UserAuth
|
alias CanneryWeb.UserAuth
|
||||||
|
|
||||||
|
@ -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">
|
<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 title text-primary-600 text-2xl text-center">
|
<h1 class="pb-4 text-2xl text-center title text-primary-600">
|
||||||
<%= gettext("Settings") %>
|
{gettext("Settings")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
@ -9,40 +9,40 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={@email_changeset}
|
for={@email_changeset}
|
||||||
action={~p"/users/settings"}
|
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">
|
<h3 class="col-span-3 text-lg text-center title text-primary-600">
|
||||||
<%= dgettext("actions", "Change email") %>
|
{dgettext("actions", "Change email")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={@email_changeset.action && not @email_changeset.valid?()}
|
: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>
|
</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") %>
|
{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") %>
|
{email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2")}
|
||||||
<%= error_tag(f, :email, "col-span-3") %>
|
{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",
|
for: "current_password_for_email",
|
||||||
class: "mx-2 my-1 title text-lg text-primary-600"
|
class: "mx-2 my-1 title text-lg text-primary-600"
|
||||||
) %>
|
)}
|
||||||
<%= password_input(f, :current_password,
|
{password_input(f, :current_password,
|
||||||
required: true,
|
required: true,
|
||||||
name: "current_password",
|
name: "current_password",
|
||||||
id: "current_password_for_email",
|
id: "current_password_for_email",
|
||||||
class: "mx-2 my-1 input input-primary col-span-2"
|
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"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
@ -51,52 +51,52 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={@password_changeset}
|
for={@password_changeset}
|
||||||
action={~p"/users/settings"}
|
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">
|
<h3 class="col-span-3 text-lg text-center title text-primary-600">
|
||||||
<%= dgettext("actions", "Change password") %>
|
{dgettext("actions", "Change password")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={@password_changeset.action && not @password_changeset.valid?()}
|
: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>
|
</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") %>
|
{label(f, :password, gettext("New password"), class: "title text-lg text-primary-600")}
|
||||||
<%= password_input(f, :password,
|
{password_input(f, :password,
|
||||||
required: true,
|
required: true,
|
||||||
class: "mx-2 my-1 input input-primary col-span-2"
|
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"
|
class: "title text-lg text-primary-600"
|
||||||
) %>
|
)}
|
||||||
<%= password_input(f, :password_confirmation,
|
{password_input(f, :password_confirmation,
|
||||||
required: true,
|
required: true,
|
||||||
class: "mx-2 my-1 input input-primary col-span-2"
|
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",
|
for: "current_password_for_password",
|
||||||
class: "title text-lg text-primary-600"
|
class: "title text-lg text-primary-600"
|
||||||
) %>
|
)}
|
||||||
<%= password_input(f, :current_password,
|
{password_input(f, :current_password,
|
||||||
required: true,
|
required: true,
|
||||||
name: "current_password",
|
name: "current_password",
|
||||||
id: "current_password_for_password",
|
id: "current_password_for_password",
|
||||||
class: "mx-2 my-1 input input-primary col-span-2"
|
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"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
@ -105,45 +105,45 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={@locale_changeset}
|
for={@locale_changeset}
|
||||||
action={~p"/users/settings"}
|
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"
|
class: "title text-primary-600 text-lg text-center col-span-3"
|
||||||
) %>
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={@locale_changeset.action && not @locale_changeset.valid?()}
|
: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>
|
</div>
|
||||||
|
|
||||||
<%= hidden_input(f, :action, name: "action", value: "update_locale") %>
|
{hidden_input(f, :action, name: "action", value: "update_locale")}
|
||||||
|
|
||||||
<%= select(
|
{select(
|
||||||
f,
|
f,
|
||||||
:locale,
|
:locale,
|
||||||
[
|
[
|
||||||
{gettext("English"), "en_US"},
|
{"English", "en_US"},
|
||||||
{gettext("German"), "de"},
|
{"Deutsch", "de"},
|
||||||
{gettext("French"), "fr"},
|
{"Français", "fr"},
|
||||||
{gettext("Spanish"), "es"}
|
{"Español", "es"}
|
||||||
],
|
],
|
||||||
class: "mx-2 my-1 min-w-md input input-primary col-span-3"
|
class: "my-1 min-w-20 input input-primary col-span-3"
|
||||||
) %>
|
)}
|
||||||
<%= error_tag(f, :locale, "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",
|
class: "whitespace-nowrap mx-auto btn btn-primary col-span-3",
|
||||||
data: [qa: dgettext("prompts", "Are you sure you want to change your language?")]
|
data: [qa: dgettext("prompts", "Are you sure you want to change your language?")]
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
|
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
|
||||||
<%= dgettext("actions", "Export Data as JSON") %>
|
{dgettext("actions", "Export Data as JSON")}
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<.link
|
<.link
|
||||||
@ -152,7 +152,7 @@
|
|||||||
class="mx-4 my-2 btn btn-alert"
|
class="mx-4 my-2 btn btn-alert"
|
||||||
data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")}
|
data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")}
|
||||||
>
|
>
|
||||||
<%= dgettext("actions", "Delete User") %>
|
{dgettext("actions", "Delete User")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ defmodule CanneryWeb.ErrorHelpers do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use PhoenixHTMLHelpers
|
use PhoenixHTMLHelpers
|
||||||
import Phoenix.{Component, HTML.Form}
|
import Phoenix.Component
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Phoenix.{HTML.Form, LiveView.Rendered}
|
alias Phoenix.{HTML.Form, LiveView.Rendered}
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ defmodule CanneryWeb.ErrorHelpers do
|
|||||||
~H"""
|
~H"""
|
||||||
<span
|
<span
|
||||||
:for={error <- Keyword.get_values(@form.errors, @field)}
|
:for={error <- Keyword.get_values(@form.errors, @field)}
|
||||||
|
:if={used_input?(@form[@field])}
|
||||||
class={["invalid-feedback", @extra_class]}
|
class={["invalid-feedback", @extra_class]}
|
||||||
phx-feedback-for={input_name(@form, @field)}
|
|
||||||
>
|
>
|
||||||
<%= translate_error(error) %>
|
{translate_error(error)}
|
||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
@ -5,7 +5,7 @@ defmodule CanneryWeb.Gettext do
|
|||||||
By using [Gettext](https://hexdocs.pm/gettext),
|
By using [Gettext](https://hexdocs.pm/gettext),
|
||||||
your module gains a set of macros for translations, for example:
|
your module gains a set of macros for translations, for example:
|
||||||
|
|
||||||
import CanneryWeb.Gettext
|
use Gettext, backend: CanneryWeb.Gettext
|
||||||
|
|
||||||
# Simple translation
|
# Simple translation
|
||||||
gettext("Here is the string to translate")
|
gettext("Here is the string to translate")
|
||||||
@ -20,5 +20,5 @@ defmodule CanneryWeb.Gettext do
|
|||||||
|
|
||||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||||
"""
|
"""
|
||||||
use Gettext, otp_app: :cannery
|
use Gettext.Backend, otp_app: :cannery
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="flex flex-col justify-center items-center text-center space-y-8">
|
<div class="flex flex-col justify-center items-center text-center space-y-8">
|
||||||
<h2 class="title text-xl text-primary-600">
|
<h2 class="title text-xl text-primary-600">
|
||||||
<%= @title %>
|
{@title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center items-center">
|
<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>
|
<i class="fa-fw fa-sm fas fa-trash"></i>
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<h2 :if={@container.tags |> Enum.empty?()} class="title text-xl text-primary-600">
|
<h2 :if={@container.tags |> Enum.empty?()} class="title text-xl text-primary-600">
|
||||||
<%= gettext("No tags") %>
|
{gettext("No tags")}
|
||||||
<%= display_emoji("😔") %>
|
{display_emoji("😔")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -43,15 +43,17 @@
|
|||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
phx-submit="save"
|
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"
|
class: "text-center col-span-2 input input-primary",
|
||||||
) %>
|
id: "#{@id}-tag-select",
|
||||||
<%= error_tag(f, :tag_id, "col-span-3 text-center") %>
|
phx_hook: "SlimSelect"
|
||||||
|
)}
|
||||||
|
{error_tag(f, :tag_id, "col-span-3 text-center")}
|
||||||
|
|
||||||
<%= submit(dgettext("actions", "Add"),
|
{submit(dgettext("actions", "Add"),
|
||||||
class: "mx-auto btn btn-primary",
|
class: "mx-auto btn btn-primary",
|
||||||
phx_disable_with: dgettext("prompts", "Adding...")
|
phx_disable_with: dgettext("prompts", "Adding...")
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"container" => container_params}, socket) do
|
def handle_event("validate", %{"container" => container_params}, socket) do
|
||||||
{:noreply, socket |> assign_changeset(container_params)}
|
{:noreply, socket |> assign_changeset(container_params, :validate)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event(
|
def handle_event(
|
||||||
@ -32,14 +32,9 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
|
|||||||
|
|
||||||
defp assign_changeset(
|
defp assign_changeset(
|
||||||
%{assigns: %{action: action, container: container, current_user: user}} = socket,
|
%{assigns: %{action: action, container: container, current_user: user}} = socket,
|
||||||
container_params
|
container_params,
|
||||||
|
changeset_action \\ nil
|
||||||
) do
|
) do
|
||||||
changeset_action =
|
|
||||||
case action do
|
|
||||||
create when create in [:new, :clone] -> :insert
|
|
||||||
:edit -> :update
|
|
||||||
end
|
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
case action do
|
case action do
|
||||||
create when create in [:new, :clone] ->
|
create when create in [:new, :clone] ->
|
||||||
@ -50,10 +45,14 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
|
|||||||
end
|
end
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
|
if changeset_action do
|
||||||
case changeset |> Changeset.apply_action(changeset_action) do
|
case changeset |> Changeset.apply_action(changeset_action) do
|
||||||
{:ok, _data} -> changeset
|
{:ok, _data} -> changeset
|
||||||
{:error, changeset} -> changeset
|
{:error, changeset} -> changeset
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
socket |> assign(:changeset, changeset)
|
socket |> assign(:changeset, changeset)
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
||||||
<%= @title %>
|
{@title}
|
||||||
</h2>
|
</h2>
|
||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
@ -12,49 +12,53 @@
|
|||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:if={@changeset.action && not @changeset.valid?()}
|
:if={@changeset.action && not @changeset.valid?}
|
||||||
class="invalid-feedback col-span-3 text-center"
|
class="invalid-feedback col-span-3 text-center"
|
||||||
>
|
>
|
||||||
<%= changeset_errors(@changeset) %>
|
{changeset_errors(@changeset)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
|
{label(f, :name, gettext("Name"), class: "title text-lg text-primary-600")}
|
||||||
<%= text_input(f, :name,
|
{text_input(f, :name,
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
placeholder: gettext("My cool ammo can"),
|
maxlength: 255,
|
||||||
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") %>
|
{label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600")}
|
||||||
<%= textarea(f, :desc,
|
{textarea(f, :desc,
|
||||||
|
class: "input input-primary col-span-2",
|
||||||
id: "container-form-desc",
|
id: "container-form-desc",
|
||||||
class: "input input-primary col-span-2",
|
phx_debounce: 300,
|
||||||
placeholder: gettext("Metal ammo can with the anime girl sticker"),
|
phx_update: "ignore",
|
||||||
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") %>
|
{label(f, :type, gettext("Type"), class: "title text-lg text-primary-600")}
|
||||||
<%= text_input(f, :type,
|
{text_input(f, :type,
|
||||||
class: "input input-primary col-span-2",
|
class: "input input-primary col-span-2",
|
||||||
placeholder: gettext("Magazine, Clip, Ammo Box, etc"),
|
maxlength: 255,
|
||||||
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") %>
|
{label(f, :location, gettext("Location"), class: "title text-lg text-primary-600")}
|
||||||
<%= textarea(f, :location,
|
{textarea(f, :location,
|
||||||
|
class: "input input-primary col-span-2",
|
||||||
id: "container-form-location",
|
id: "container-form-location",
|
||||||
class: "input input-primary col-span-2",
|
phx_debounce: 300,
|
||||||
placeholder: gettext("On the bookshelf"),
|
phx_update: "ignore",
|
||||||
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",
|
class: "mx-auto btn btn-primary col-span-3",
|
||||||
phx_disable_with: dgettext("prompts", "Saving...")
|
phx_disable_with: dgettext("prompts", "Saving...")
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,6 +112,20 @@ defmodule CanneryWeb.ContainerLive.Index do
|
|||||||
{:noreply, socket |> push_patch(to: ~p"/containers/search/#{search_term}")}
|
{:noreply, socket |> push_patch(to: ~p"/containers/search/#{search_term}")}
|
||||||
end
|
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
|
defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do
|
||||||
socket |> assign(:containers, Containers.list_containers(current_user, search: search))
|
socket |> assign(:containers, Containers.list_containers(current_user, search: search))
|
||||||
end
|
end
|
||||||
|
@ -1,51 +1,51 @@
|
|||||||
<div class="flex flex-col space-y-8 justify-center items-center">
|
<div class="flex flex-col justify-center items-center space-y-8">
|
||||||
<h1 class="title text-2xl title-primary-500">
|
<h1 class="text-2xl title title-primary-500">
|
||||||
<%= gettext("Containers") %>
|
{gettext("Containers")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<%= if @containers |> Enum.empty?() and @search |> is_nil() do %>
|
<%= if @containers |> Enum.empty?() and @search |> is_nil() do %>
|
||||||
<h2 class="title text-xl text-primary-600">
|
<h2 class="text-xl title text-primary-600">
|
||||||
<%= gettext("No containers") %>
|
{gettext("No containers")}
|
||||||
<%= display_emoji("😔") %>
|
{display_emoji("😔")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<.link patch={~p"/containers/new"} class="btn btn-primary">
|
<.link patch={~p"/containers/new"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Add your first container!") %>
|
{dgettext("actions", "Add your first container!")}
|
||||||
</.link>
|
</.link>
|
||||||
<% else %>
|
<% else %>
|
||||||
<.link patch={~p"/containers/new"} class="btn btn-primary">
|
<.link patch={~p"/containers/new"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "New Container") %>
|
{dgettext("actions", "New Container")}
|
||||||
</.link>
|
</.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
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={%{}}
|
for={%{}}
|
||||||
as={:search}
|
as={:search}
|
||||||
phx-change="search"
|
phx-change="search"
|
||||||
phx-submit="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",
|
class: "grow input input-primary",
|
||||||
value: @search,
|
|
||||||
role: "search",
|
|
||||||
phx_debounce: 300,
|
phx_debounce: 300,
|
||||||
placeholder: gettext("Search containers")
|
placeholder: gettext("Search containers"),
|
||||||
) %>
|
role: "search",
|
||||||
|
value: @search
|
||||||
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<.toggle_button action="toggle_table" value={@view_table}>
|
<.toggle_button action="toggle_table" value={@view_table}>
|
||||||
<span class="title text-lg text-primary-600">
|
<span class="text-lg title text-primary-600">
|
||||||
<%= gettext("View as table") %>
|
{gettext("View as table")}
|
||||||
</span>
|
</span>
|
||||||
</.toggle_button>
|
</.toggle_button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @containers |> Enum.empty?() do %>
|
<%= if @containers |> Enum.empty?() do %>
|
||||||
<h2 class="title text-xl text-primary-600">
|
<h2 class="text-xl title text-primary-600">
|
||||||
<%= gettext("No containers") %>
|
{gettext("No containers")}
|
||||||
<%= display_emoji("😔") %>
|
{display_emoji("😔")}
|
||||||
</h2>
|
</h2>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= if @view_table do %>
|
<%= if @view_table do %>
|
||||||
@ -56,6 +56,20 @@
|
|||||||
containers={@containers}
|
containers={@containers}
|
||||||
current_user={@current_user}
|
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}>
|
<:tag_actions :let={container}>
|
||||||
<div class="mx-4 my-2">
|
<div class="mx-4 my-2">
|
||||||
<.link
|
<.link
|
||||||
@ -109,7 +123,7 @@
|
|||||||
</:actions>
|
</:actions>
|
||||||
</.live_component>
|
</.live_component>
|
||||||
<% else %>
|
<% 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
|
<.container_card
|
||||||
:for={container <- @containers}
|
:for={container <- @containers}
|
||||||
container={container}
|
container={container}
|
||||||
|
@ -78,6 +78,18 @@ defmodule CanneryWeb.ContainerLive.Show do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
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
|
def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
|
||||||
{:noreply, socket |> assign(:view_table, !view_table) |> render_container()}
|
{:noreply, socket |> assign(:view_table, !view_table) |> render_container()}
|
||||||
end
|
end
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
<div class="space-y-4 flex flex-col justify-center items-center">
|
<div class="flex flex-col justify-center items-center space-y-4">
|
||||||
<h1 class="title text-2xl title-primary-500">
|
<h1 class="text-2xl title title-primary-500">
|
||||||
<%= @container.name %>
|
{@container.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<span :if={@container.desc} class="rounded-lg title text-lg">
|
<span :if={@container.desc} class="text-lg rounded-lg title">
|
||||||
<%= gettext("Description:") %>
|
{gettext("Description:")}
|
||||||
<%= @container.desc %>
|
{@container.desc}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="text-lg rounded-lg title">
|
||||||
<%= gettext("Type:") %>
|
{gettext("Type:")}
|
||||||
<%= @container.type %>
|
{@container.type}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :if={@container.location} class="rounded-lg title text-lg">
|
<span :if={@container.location} class="text-lg rounded-lg title">
|
||||||
<%= gettext("Location:") %>
|
{gettext("Location:")}
|
||||||
<%= @container.location %>
|
{@container.location}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="text-lg rounded-lg title">
|
||||||
<%= gettext("Packs:") %>
|
{gettext("Packs:")}
|
||||||
<%= @packs_count %>
|
{@packs_count}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="rounded-lg title text-lg">
|
<span class="text-lg rounded-lg title">
|
||||||
<%= gettext("Rounds:") %>
|
{gettext("Rounds:")}
|
||||||
<%= @round_count %>
|
{@round_count}
|
||||||
</span>
|
</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
|
<.link
|
||||||
patch={~p"/container/edit/#{@container}"}
|
patch={~p"/container/edit/#{@container}"}
|
||||||
class="text-primary-600 link"
|
class="text-primary-600 link"
|
||||||
@ -52,17 +52,25 @@
|
|||||||
</.link>
|
</.link>
|
||||||
</div>
|
</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" />
|
<hr class="mb-4 hr" />
|
||||||
|
|
||||||
<%= if @container.tags |> Enum.empty?() do %>
|
<%= if @container.tags |> Enum.empty?() do %>
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<h2 class="title text-lg text-primary-600">
|
<h2 class="text-lg title text-primary-600">
|
||||||
<%= gettext("No tags for this container") %>
|
{gettext("No tags for this container")}
|
||||||
<%= display_emoji("😔") %>
|
{display_emoji("😔")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<.link patch={~p"/container/edit_tags/#{@container}"} class="btn btn-primary">
|
<.link patch={~p"/container/edit_tags/#{@container}"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "Why not add one?") %>
|
{dgettext("actions", "Why not add one?")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
@ -88,9 +96,9 @@
|
|||||||
phx-submit="change_class"
|
phx-submit="change_class"
|
||||||
class="flex items-center"
|
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,
|
f,
|
||||||
:class,
|
:class,
|
||||||
[
|
[
|
||||||
@ -99,22 +107,22 @@
|
|||||||
{gettext("Shotgun"), :shotgun},
|
{gettext("Shotgun"), :shotgun},
|
||||||
{gettext("Pistol"), :pistol}
|
{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
|
value: @class
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
<.toggle_button action="toggle_table" value={@view_table}>
|
<.toggle_button action="toggle_table" value={@view_table}>
|
||||||
<span class="title text-lg text-primary-600">
|
<span class="text-lg title text-primary-600">
|
||||||
<%= gettext("View as table") %>
|
{gettext("View as table")}
|
||||||
</span>
|
</span>
|
||||||
</.toggle_button>
|
</.toggle_button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full p-4">
|
<div class="p-4 w-full">
|
||||||
<%= if @packs |> Enum.empty?() do %>
|
<%= if @packs |> Enum.empty?() do %>
|
||||||
<h2 class="mx-4 title text-lg text-primary-600 text-center">
|
<h2 class="mx-4 text-lg text-center title text-primary-600">
|
||||||
<%= gettext("No ammo in this container") %>
|
{gettext("No ammo in this container")}
|
||||||
</h2>
|
</h2>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= if @view_table do %>
|
<%= if @view_table do %>
|
||||||
@ -127,11 +135,11 @@
|
|||||||
>
|
>
|
||||||
<:type :let={%{name: type_name} = type}>
|
<:type :let={%{name: type_name} = type}>
|
||||||
<.link navigate={~p"/type/#{type}"} class="link">
|
<.link navigate={~p"/type/#{type}"} class="link">
|
||||||
<%= type_name %>
|
{type_name}
|
||||||
</.link>
|
</.link>
|
||||||
</:type>
|
</:type>
|
||||||
<:actions :let={%{count: pack_count} = pack}>
|
<: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
|
<.link
|
||||||
navigate={~p"/ammo/show/#{pack}"}
|
navigate={~p"/ammo/show/#{pack}"}
|
||||||
class="text-primary-600 link"
|
class="text-primary-600 link"
|
||||||
|
@ -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
|
<img
|
||||||
src={~p"/images/cannery.svg"}
|
src={~p"/images/cannery.svg"}
|
||||||
alt={gettext("Cannery logo")}
|
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")}
|
title={gettext("isn't he cute >:3")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h1 class="title text-primary-600 text-2xl">
|
<h1 class="text-2xl title text-primary-600">
|
||||||
<%= gettext("Welcome to Cannery") %>
|
{gettext("Welcome to Cannery")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h2 class="title text-primary-600 text-lg">
|
<h2 class="text-lg title text-primary-600">
|
||||||
<%= gettext("The self-hosted firearm tracker website") %>
|
{gettext("The self-hosted firearm tracker website")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
@ -19,48 +19,48 @@
|
|||||||
<ul class="flex flex-col space-y-4 text-center">
|
<ul class="flex flex-col space-y-4 text-center">
|
||||||
<li class="flex flex-col justify-center items-center space-y-2">
|
<li class="flex flex-col justify-center items-center space-y-2">
|
||||||
<b class="whitespace-nowrap">
|
<b class="whitespace-nowrap">
|
||||||
<%= gettext("Easy to Use:") %>
|
{gettext("Easy to Use:")}
|
||||||
</b>
|
</b>
|
||||||
<p>
|
<p>
|
||||||
<%= gettext(
|
{gettext(
|
||||||
"Cannery lets you easily keep an eye on your ammo levels before and after range day"
|
"Cannery lets you easily keep an eye on your ammo levels before and after range day"
|
||||||
) %>
|
)}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex flex-col justify-center items-center space-y-2">
|
<li class="flex flex-col justify-center items-center space-y-2">
|
||||||
<b class="whitespace-nowrap">
|
<b class="whitespace-nowrap">
|
||||||
<%= gettext("Secure:") %>
|
{gettext("Secure:")}
|
||||||
</b>
|
</b>
|
||||||
<p>
|
<p>
|
||||||
<%= gettext("Self-host your own instance, or use an instance from someone you trust.") %>
|
{gettext("Self-host your own instance, or use an instance from someone you trust.")}
|
||||||
<%= gettext("Your data stays with you, period") %>
|
{gettext("Your data stays with you, period")}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex flex-col justify-center items-center space-y-2">
|
<li class="flex flex-col justify-center items-center space-y-2">
|
||||||
<b class="whitespace-nowrap">
|
<b class="whitespace-nowrap">
|
||||||
<%= gettext("Simple:") %>
|
{gettext("Simple:")}
|
||||||
</b>
|
</b>
|
||||||
<p>
|
<p>
|
||||||
<%= gettext("Access from any internet-capable device") %>
|
{gettext("Access from any internet-capable device")}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<ul class="flex flex-col space-y-2 text-center justify-center">
|
<ul class="flex flex-col justify-center space-y-2 text-center">
|
||||||
<h2 class="title text-primary-600 text-lg">
|
<h2 class="text-lg title text-primary-600">
|
||||||
<%= gettext("Instance Information") %>
|
{gettext("Instance Information")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<li class="flex flex-col justify-center space-x-2">
|
<li class="flex flex-col justify-center space-x-2">
|
||||||
<b>
|
<b>
|
||||||
<%= gettext("Admins:") %>
|
{gettext("Admins:")}
|
||||||
</b>
|
</b>
|
||||||
<p>
|
<p>
|
||||||
<%= if @admins |> Enum.empty?() do %>
|
<%= if @admins |> Enum.empty?() do %>
|
||||||
<.link href={~p"/users/register"} class="hover:underline">
|
<.link href={~p"/users/register"} class="hover:underline">
|
||||||
<%= dgettext("prompts", "Register to setup Cannery") %>
|
{dgettext("prompts", "Register to setup Cannery")}
|
||||||
</.link>
|
</.link>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex flex-wrap justify-center space-x-2">
|
<div class="flex flex-wrap justify-center space-x-2">
|
||||||
@ -69,7 +69,7 @@
|
|||||||
class="hover:underline"
|
class="hover:underline"
|
||||||
href={"mailto:#{email}"}
|
href={"mailto:#{email}"}
|
||||||
>
|
>
|
||||||
<%= email %>
|
{email}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -77,17 +77,17 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="flex flex-row justify-center space-x-2">
|
<li class="flex flex-row justify-center space-x-2">
|
||||||
<b><%= gettext("Registration:") %></b>
|
<b>{gettext("Registration:")}</b>
|
||||||
<p>
|
<p>
|
||||||
<%= case Accounts.registration_mode() do
|
{case Accounts.registration_mode() do
|
||||||
:public -> gettext("Public Signups")
|
:public -> gettext("Public Signups")
|
||||||
:invite_only -> gettext("Invite Only")
|
:invite_only -> gettext("Invite Only")
|
||||||
end %>
|
end}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="flex flex-row justify-center items-center space-x-2">
|
<li class="flex flex-row justify-center items-center space-x-2">
|
||||||
<b><%= gettext("Version:") %></b>
|
<b>{gettext("Version:")}</b>
|
||||||
<.link
|
<.link
|
||||||
href="https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/CHANGELOG.md"
|
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"
|
class="flex flex-row justify-center items-center space-x-2 hover:underline"
|
||||||
@ -95,7 +95,7 @@
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<%= @version %>
|
{@version}
|
||||||
</p>
|
</p>
|
||||||
<i class="fas fa-md fa-info-circle"></i>
|
<i class="fas fa-md fa-info-circle"></i>
|
||||||
</.link>
|
</.link>
|
||||||
@ -104,9 +104,9 @@
|
|||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<ul class="flex flex-col space-y-2 text-center justify-center">
|
<ul class="flex flex-col justify-center space-y-2 text-center">
|
||||||
<h2 class="title text-primary-600 text-lg">
|
<h2 class="text-lg title text-primary-600">
|
||||||
<%= gettext("Get involved!") %>
|
{gettext("Get involved!")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<li class="flex flex-col justify-center space-x-2">
|
<li class="flex flex-col justify-center space-x-2">
|
||||||
@ -116,7 +116,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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>
|
<i class="fas fa-md fa-code"></i>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
@ -127,7 +127,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<p><%= gettext("Help translate") %></p>
|
<p>{gettext("Help translate")}</p>
|
||||||
<i class="fas fa-md fa-language"></i>
|
<i class="fas fa-md fa-language"></i>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
@ -138,7 +138,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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>
|
<i class="fas fa-md fa-spider"></i>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -19,7 +19,7 @@ defmodule CanneryWeb.InviteLive.FormComponent do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"invite" => invite_params}, socket) do
|
def handle_event("validate", %{"invite" => invite_params}, socket) do
|
||||||
{:noreply, socket |> assign_changeset(invite_params)}
|
{:noreply, socket |> assign_changeset(invite_params, :validate)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
|
def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
|
||||||
@ -28,14 +28,9 @@ defmodule CanneryWeb.InviteLive.FormComponent do
|
|||||||
|
|
||||||
defp assign_changeset(
|
defp assign_changeset(
|
||||||
%{assigns: %{action: action, current_user: user, invite: invite}} = socket,
|
%{assigns: %{action: action, current_user: user, invite: invite}} = socket,
|
||||||
invite_params
|
invite_params,
|
||||||
|
changeset_action \\ nil
|
||||||
) do
|
) do
|
||||||
changeset_action =
|
|
||||||
case action do
|
|
||||||
:new -> :insert
|
|
||||||
:edit -> :update
|
|
||||||
end
|
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
case action do
|
case action do
|
||||||
:new -> Invite.create_changeset(user, "example_token", invite_params)
|
:new -> Invite.create_changeset(user, "example_token", invite_params)
|
||||||
@ -43,10 +38,14 @@ defmodule CanneryWeb.InviteLive.FormComponent do
|
|||||||
end
|
end
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
|
if changeset_action do
|
||||||
case changeset |> Changeset.apply_action(changeset_action) do
|
case changeset |> Changeset.apply_action(changeset_action) do
|
||||||
{:ok, _data} -> changeset
|
{:ok, _data} -> changeset
|
||||||
{:error, changeset} -> changeset
|
{:error, changeset} -> changeset
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
socket |> assign(:changeset, changeset)
|
socket |> assign(:changeset, changeset)
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
||||||
<%= @title %>
|
{@title}
|
||||||
</h2>
|
</h2>
|
||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
@ -12,29 +12,32 @@
|
|||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:if={@changeset.action && not @changeset.valid?()}
|
:if={@changeset.action && not @changeset.valid?}
|
||||||
class="invalid-feedback col-span-3 text-center"
|
class="invalid-feedback col-span-3 text-center"
|
||||||
>
|
>
|
||||||
<%= changeset_errors(@changeset) %>
|
{changeset_errors(@changeset)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label(f, :name, gettext("Name"),
|
{label(f, :name, gettext("Name"),
|
||||||
class: "title text-lg text-primary-600",
|
class: "title text-lg text-primary-600",
|
||||||
maxlength: 255
|
maxlength: 255
|
||||||
) %>
|
)}
|
||||||
<%= text_input(f, :name, class: "input input-primary col-span-2") %>
|
{text_input(f, :name,
|
||||||
<%= error_tag(f, :name, "col-span-3") %>
|
class: "input input-primary col-span-2",
|
||||||
|
phx_debounce: 300
|
||||||
|
)}
|
||||||
|
{error_tag(f, :name, "col-span-3")}
|
||||||
|
|
||||||
<%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %>
|
{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") %>
|
{number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2")}
|
||||||
<%= error_tag(f, :uses_left, "col-span-3") %>
|
{error_tag(f, :uses_left, "col-span-3")}
|
||||||
<span class="col-span-3 text-primary-400 italic text-center">
|
<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>
|
</span>
|
||||||
|
|
||||||
<%= submit(dgettext("actions", "Save"),
|
{submit(dgettext("actions", "Save"),
|
||||||
class: "mx-auto btn btn-primary col-span-3",
|
class: "mx-auto btn btn-primary col-span-3",
|
||||||
phx_disable_with: dgettext("prompts", "Saving...")
|
phx_disable_with: dgettext("prompts", "Saving...")
|
||||||
) %>
|
)}
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user