Compare commits
33 commits
75eb9aa316
...
main
Author | SHA1 | Date | |
---|---|---|---|
2624a32861 | |||
c6c52c783e | |||
433704e08a | |||
a3c3940563 | |||
573a1e9cfe | |||
db9f127e7b | |||
bc36586212 | |||
66bb2a8727 | |||
7a879e632a | |||
280cd5bc34 | |||
b3d1099d34 | |||
d4b810e14e | |||
ba5957cc93 | |||
94637a564c | |||
10f284da6f | |||
64e310b598 | |||
61796cf985 | |||
dceef941c7 | |||
68d60c120d | |||
6b787297bb | |||
24642d7c67 | |||
8cd984adc5 | |||
e7d93989d3 | |||
a99c5eea35 | |||
ada166fb41 | |||
6a5d2346ff | |||
f28c85e343 | |||
f27df8d676 | |||
ea74a89078 | |||
ce03eaaf2d | |||
3eff955672 | |||
d43daafdb7 | |||
721ba53c15 |
172 changed files with 3244 additions and 18012 deletions
45
.dockerignore
Normal file
45
.dockerignore
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# This file excludes paths from the Docker build context.
|
||||||
|
#
|
||||||
|
# By default, Docker's build context includes all files (and folders) in the
|
||||||
|
# current directory. Even if a file isn't copied into the container it is still sent to
|
||||||
|
# the Docker daemon.
|
||||||
|
#
|
||||||
|
# There are multiple reasons to exclude files from the build context:
|
||||||
|
#
|
||||||
|
# 1. Prevent nested folders from being copied into the container (ex: exclude
|
||||||
|
# /assets/node_modules when copying /assets)
|
||||||
|
# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
|
||||||
|
# 3. Avoid sending files containing sensitive information
|
||||||
|
#
|
||||||
|
# More information on using .dockerignore is available here:
|
||||||
|
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
|
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
|
||||||
|
#
|
||||||
|
# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
|
||||||
|
# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
|
||||||
|
.git
|
||||||
|
!.git/HEAD
|
||||||
|
!.git/refs
|
||||||
|
|
||||||
|
# Common development/test artifacts
|
||||||
|
/cover/
|
||||||
|
/doc/
|
||||||
|
/test/
|
||||||
|
/tmp/
|
||||||
|
.elixir_ls
|
||||||
|
|
||||||
|
# Mix artifacts
|
||||||
|
/_build/
|
||||||
|
/deps/
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Generated on crash by the VM
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Static artifacts - These should be fetched and built inside the Docker image
|
||||||
|
/assets/node_modules/
|
||||||
|
/priv/static/assets/
|
||||||
|
/priv/static/cache_manifest.json
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -35,7 +35,7 @@ npm-debug.log
|
||||||
# Since we are building assets from assets/,
|
# Since we are building assets from assets/,
|
||||||
# we ignore priv/static. You may want to comment
|
# we ignore priv/static. You may want to comment
|
||||||
# this depending on your deployment strategy.
|
# this depending on your deployment strategy.
|
||||||
/priv/static/
|
#/priv/static/
|
||||||
|
|
||||||
# Files matching config/*.secret.exs pattern contain sensitive
|
# Files matching config/*.secret.exs pattern contain sensitive
|
||||||
# data and you should not commit them into version control.
|
# data and you should not commit them into version control.
|
||||||
|
@ -50,3 +50,4 @@ npm-debug.log
|
||||||
|
|
||||||
# dev
|
# dev
|
||||||
TODO.md
|
TODO.md
|
||||||
|
NOTES.md
|
||||||
|
|
96
Dockerfile
Normal file
96
Dockerfile
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of
|
||||||
|
# Alpine to avoid DNS resolution issues in production.
|
||||||
|
#
|
||||||
|
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
|
||||||
|
# https://hub.docker.com/_/ubuntu?tab=tags
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# This file is based on these images:
|
||||||
|
#
|
||||||
|
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
|
||||||
|
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20230109-slim - for the release image
|
||||||
|
# - https://pkgs.org/ - resource for finding needed packages
|
||||||
|
# - Ex: hexpm/elixir:1.14.3-erlang-25.2.1-debian-bullseye-20230109-slim
|
||||||
|
#
|
||||||
|
ARG ELIXIR_VERSION=1.14.3
|
||||||
|
ARG OTP_VERSION=25.2.1
|
||||||
|
ARG DEBIAN_VERSION=bullseye-20230109-slim
|
||||||
|
|
||||||
|
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
||||||
|
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
||||||
|
|
||||||
|
FROM ${BUILDER_IMAGE} as builder
|
||||||
|
|
||||||
|
# install build dependencies
|
||||||
|
RUN apt-get update -y && apt-get install -y build-essential git curl && \
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && \
|
||||||
|
apt-get install -y nodejs && apt-get clean && \
|
||||||
|
rm -f /var/lib/apt/lists/*_* && npm i -g npm
|
||||||
|
|
||||||
|
# prepare build dir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# install hex + rebar
|
||||||
|
RUN mix local.hex --force && \
|
||||||
|
mix local.rebar --force
|
||||||
|
|
||||||
|
# set build ENV
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# install mix dependencies
|
||||||
|
COPY mix.exs mix.lock ./
|
||||||
|
RUN mix deps.get --only $MIX_ENV
|
||||||
|
RUN mkdir config
|
||||||
|
|
||||||
|
# copy compile-time config files before we compile dependencies
|
||||||
|
# to ensure any relevant config change will trigger the dependencies
|
||||||
|
# to be re-compiled.
|
||||||
|
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||||
|
RUN mix deps.compile
|
||||||
|
|
||||||
|
COPY priv priv
|
||||||
|
|
||||||
|
COPY lib lib
|
||||||
|
|
||||||
|
COPY assets assets
|
||||||
|
|
||||||
|
# install node modules
|
||||||
|
RUN npm --prefix assets install
|
||||||
|
# compile assets
|
||||||
|
RUN mix assets.deploy
|
||||||
|
|
||||||
|
# Compile the release
|
||||||
|
RUN mix compile
|
||||||
|
|
||||||
|
# Changes to config/runtime.exs don't require recompiling the code
|
||||||
|
COPY config/runtime.exs config/
|
||||||
|
|
||||||
|
COPY rel rel
|
||||||
|
RUN mix release
|
||||||
|
|
||||||
|
# start a new build stage so that the final image will only contain
|
||||||
|
# the compiled release and other runtime necessities
|
||||||
|
FROM ${RUNNER_IMAGE}
|
||||||
|
|
||||||
|
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
|
||||||
|
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
|
||||||
|
|
||||||
|
# Set the locale
|
||||||
|
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||||
|
|
||||||
|
ENV LANG en_US.UTF-8
|
||||||
|
ENV LANGUAGE en_US:en
|
||||||
|
ENV LC_ALL en_US.UTF-8
|
||||||
|
|
||||||
|
WORKDIR "/app"
|
||||||
|
RUN chown nobody /app
|
||||||
|
|
||||||
|
# set runner ENV
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# Only copy the final release from the build stage
|
||||||
|
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/shift73k ./
|
||||||
|
|
||||||
|
USER nobody
|
||||||
|
|
||||||
|
CMD ["/app/bin/server"]
|
155
README.md
155
README.md
|
@ -4,87 +4,86 @@ Calendaring app for shift-worker shift tracking, with support for CSV export and
|
||||||
|
|
||||||
Written in Elixir & Phoenix LiveView, with Bootstrap v5.
|
Written in Elixir & Phoenix LiveView, with Bootstrap v5.
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
To run in production, you'll need to provide several environment variable values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MIX_ENV=prod \
|
||||||
|
PHX_SERVER=true \
|
||||||
|
TZ=America/New_York \
|
||||||
|
DB_SOCK=[postgres unix socket path] \
|
||||||
|
DB_NAME=[postgres db name] \
|
||||||
|
DB_USER=[postgres db user] \
|
||||||
|
DB_PASS=[postgres db user password] \
|
||||||
|
SECRET_KEY_BASE=[phoenix secret key base] \
|
||||||
|
PHX_HOST=[server fqdn (e.g., shift.73k.us)] \
|
||||||
|
PORT=4000 \
|
||||||
|
SMTP_RELAY=[smtp server] \
|
||||||
|
SMTP_PORT=[smtp port] \
|
||||||
|
SMTP_USER=[smtp username] \
|
||||||
|
SMTP_PASS=[smtp user password] \
|
||||||
|
MAIL_REPLY_TO=reply@73k.us \
|
||||||
|
MAIL_FROM_FRIENDLY=Shift73k \
|
||||||
|
MAIL_FROM_ADDR=shift73k@73k.us \
|
||||||
|
ALLOW_REG=[open for registration? true/false] \
|
||||||
|
iex -S mix phx.server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuilding assets for production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# rebuild static assets:
|
||||||
|
MIX_ENV=prod mix phx.digest.clean --all
|
||||||
|
rm -rf ./priv/static/*
|
||||||
|
npm --prefix assets run build
|
||||||
|
MIX_ENV=prod mix phx.digest
|
||||||
|
# then do a new commit and push...
|
||||||
|
```
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Ability to edit shifts?
|
- [X] ~~*Proper modal to delete shifts?*~~ [2022-08-14]
|
||||||
- [ ] Proper modal to delete shifts?
|
- [X] ~~*move runtime config out of compile-time config files, to move towards supporting releases*~~ [2023-01-28]
|
||||||
- [ ] Allow all-day items for notes, or require hours even for sick days?
|
- [ ] bootstrap dark mode?
|
||||||
- [ ] Implement proper shift/template/assign tests (views etc)
|
- [ ] update tests, which are way out of date? Also I don't care?
|
||||||
|
|
||||||
## Deploying
|
## Deploying with docker
|
||||||
|
|
||||||
### New versions
|
The Dockerfile will enable building a new container. I do it all with docker compose, here's an example compose yml:
|
||||||
|
|
||||||
When improvements are made, we can update the deployed version like so:
|
```yaml
|
||||||
|
version: '3.9'
|
||||||
```shell
|
services:
|
||||||
cd /opt/shift73k
|
shift73k:
|
||||||
# update from master
|
build:
|
||||||
/usr/bin/git pull 73k master
|
context: ./shift73k # relative path from docker-compose.yml to shift73k repo
|
||||||
# fetch prod deps & compile
|
network: host
|
||||||
/usr/bin/mix deps.get --only prod
|
container_name: www-shift73k
|
||||||
MIX_ENV=prod /usr/bin/mix compile
|
restart: unless-stopped
|
||||||
# perform any migrations
|
volumes:
|
||||||
MIX_ENV=prod /usr/bin/mix ecto.migrate
|
- /etc/timezone:/etc/timezone:ro
|
||||||
# update node packages via package-lock.json
|
- /etc/localtime:/etc/localtime:ro
|
||||||
/usr/bin/npm --prefix /opt/shift73k/assets/ ci
|
- /srv/dck/postgres/sock/postgres:/var/run/postgresql # if using unix socket
|
||||||
# rebuild static assets:
|
# env_file: ./shift73k.env # optionally, put your env vars in a separate file
|
||||||
rm -rf /opt/shift73k/priv/static/*
|
environment:
|
||||||
/usr/bin/npm --prefix /opt/shift73k/assets/ run deploy
|
- PHX_SERVER=true
|
||||||
MIX_ENV=prod /usr/bin/mix phx.digest
|
- TZ=America/New_York
|
||||||
# rebuild release
|
- DB_SOCK=/var/run/postgresql # if using unix socket instead of db url
|
||||||
MIX_ENV=prod /usr/bin/mix release --overwrite
|
- DB_NAME=[postgres db name] # if using unix socket instead of db url
|
||||||
# restart service
|
- DB_USER=[postgres db user] # if using unix socket instead of db url
|
||||||
sudo /bin/systemctl restart shift73k.service
|
- DB_PASS=[postgres db user password] # if using unix socket instead of db url
|
||||||
```
|
- SECRET_KEY_BASE=[phoenix secret key base]
|
||||||
|
- PHX_HOST=[server fqdn (e.g., shift.73k.us)]
|
||||||
### systemd unit:
|
- PORT=4000
|
||||||
|
- SMTP_RELAY=[smtp server]
|
||||||
```ini
|
- SMTP_PORT=[smtp port]
|
||||||
[Unit]
|
- SMTP_USER=[smtp username]
|
||||||
Description=Shift73k service
|
- SMTP_PASS=[smtp user password]
|
||||||
After=local-fs.target network.target
|
- MAIL_REPLY_TO=reply@73k.us
|
||||||
|
- MAIL_FROM_FRIENDLY=Shift73k
|
||||||
[Service]
|
- MAIL_FROM_ADDR=shift73k@73k.us
|
||||||
Type=simple
|
- ALLOW_REG=[open for registration? true/false]
|
||||||
User=runuser
|
ports:
|
||||||
Group=runuser
|
- 4000:4000
|
||||||
WorkingDirectory=/opt/shift73k/_build/prod/rel/shift73k
|
|
||||||
ExecStart=/opt/shift73k/_build/prod/rel/shift73k/bin/shift73k start
|
|
||||||
ExecStop=/opt/shift73k/_build/prod/rel/shift73k/bin/shift73k stop
|
|
||||||
#EnvironmentFile=/etc/default/myApp.env
|
|
||||||
Environment=LANG=en_US.utf8
|
|
||||||
Environment=MIX_ENV=prod
|
|
||||||
#Environment=PORT=4000
|
|
||||||
LimitNOFILE=65535
|
|
||||||
UMask=0027
|
|
||||||
SyslogIdentifier=shift73k
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### nginx config:
|
|
||||||
|
|
||||||
```conf
|
|
||||||
upstream phoenix {
|
|
||||||
server 127.0.0.1:4000 max_fails=5 fail_timeout=60s;
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
location / {
|
|
||||||
allow all;
|
|
||||||
# Proxy Headers
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Cluster-Client-Ip $remote_addr;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_redirect off;
|
|
||||||
# WebSockets
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_pass http://phoenix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@
|
||||||
@import "bs-colors";
|
@import "bs-colors";
|
||||||
|
|
||||||
// Required || Configuration -- CONTINUED
|
// Required || Configuration -- CONTINUED
|
||||||
|
@import "../node_modules/bootstrap/scss/maps";
|
||||||
@import "../node_modules/bootstrap/scss/mixins";
|
@import "../node_modules/bootstrap/scss/mixins";
|
||||||
@import "../node_modules/bootstrap/scss/utilities";
|
@import "../node_modules/bootstrap/scss/utilities";
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,6 @@
|
||||||
// @import "../node_modules/@fontsource/lato/700-italic.css"; /* bold | italic */
|
// @import "../node_modules/@fontsource/lato/700-italic.css"; /* bold | italic */
|
||||||
// @import "../node_modules/@fontsource/lato/900.css"; /* black | normal */
|
// @import "../node_modules/@fontsource/lato/900.css"; /* black | normal */
|
||||||
// @import "../node_modules/@fontsource/lato/900-italic.css"; /* black | italic */
|
// @import "../node_modules/@fontsource/lato/900-italic.css"; /* black | italic */
|
||||||
|
|
||||||
|
/* Bootstrap Icons Font */
|
||||||
|
@import "../node_modules/bootstrap-icons/font/bootstrap-icons.css";
|
||||||
|
|
|
@ -32,6 +32,10 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
||||||
background-color: $navbar-light-color;
|
background-color: $navbar-light-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: $navbar-light-toggler-border-color 0 0 0 $navbar-toggler-focus-width;
|
||||||
|
// var(--bs-navbar-toggler-focus-width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +55,9 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
||||||
background-color: $navbar-dark-color;
|
background-color: $navbar-dark-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: $navbar-dark-toggler-border-color 0 0 0 $navbar-toggler-focus-width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
SVG ICON SYSTEM
|
|
||||||
per https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4
|
|
||||||
*/
|
|
||||||
.icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon svg,
|
|
||||||
.icon img {
|
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon.baseline svg,
|
|
||||||
.icon img {
|
|
||||||
top: 0.125em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
|
@ -4,9 +4,6 @@
|
||||||
/* Load Bootstrap v5 and customizations */
|
/* Load Bootstrap v5 and customizations */
|
||||||
@import "bs-load";
|
@import "bs-load";
|
||||||
|
|
||||||
/*SVG ICON SYSTEM*/
|
|
||||||
@import "svg-icons";
|
|
||||||
|
|
||||||
/* LiveView specific CSS */
|
/* LiveView specific CSS */
|
||||||
@import "phx-liveview";
|
@import "phx-liveview";
|
||||||
|
|
||||||
|
@ -43,7 +40,7 @@
|
||||||
/* style icon */
|
/* style icon */
|
||||||
.inner-addon > .icon {
|
.inner-addon > .icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0.5625rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,3 +65,11 @@
|
||||||
.shift-description p:last-child {
|
.shift-description p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
fix readonly form background
|
||||||
|
*/
|
||||||
|
.form-control[readonly] {
|
||||||
|
background-color: $input-disabled-bg;
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
const togglerBtn = document.getElementById("navbarSupportedContentToggler");
|
const togglerBtn = document.getElementById('navbarSupportedContentToggler');
|
||||||
const navbarContent = document.getElementById("navbarSupportedContent");
|
const navbarContent = document.getElementById('navbarSupportedContent');
|
||||||
|
|
||||||
navbarContent.addEventListener("show.bs.collapse", () => {
|
if (navbarContent != null) {
|
||||||
console.log("opening navbar content");
|
navbarContent.addEventListener('show.bs.collapse', () => {
|
||||||
togglerBtn.classList.toggle("is-active");
|
console.log('opening navbar content');
|
||||||
});
|
togglerBtn.classList.toggle('is-active');
|
||||||
|
});
|
||||||
|
|
||||||
navbarContent.addEventListener("hide.bs.collapse", () => {
|
navbarContent.addEventListener('hide.bs.collapse', () => {
|
||||||
console.log("closing navbar content");
|
console.log('closing navbar content');
|
||||||
togglerBtn.classList.toggle("is-active");
|
togglerBtn.classList.toggle('is-active');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,84 +1,24 @@
|
||||||
// We need to import the CSS so that webpack will load it.
|
// We import the main SCSS file, which performs all other SCSS imports,
|
||||||
// The MiniCssExtractPlugin is used to separate it out into
|
// and which vite.js will preprocess with sass.
|
||||||
// its own CSS file.
|
import '../css/app.scss'
|
||||||
import "../css/app.scss";
|
|
||||||
|
|
||||||
// Import icons for sprite-loader
|
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||||
// navbar brand icon
|
import 'phoenix_html'
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-week.svg"; // brand
|
|
||||||
// menus etc
|
|
||||||
import "../node_modules/bootstrap-icons/icons/person-circle.svg"; // accounts menu
|
|
||||||
import "../node_modules/bootstrap-icons/icons/person-plus.svg"; // new user / register
|
|
||||||
import "../node_modules/bootstrap-icons/icons/door-open.svg"; // log in
|
|
||||||
import "../node_modules/bootstrap-icons/icons/door-closed.svg"; // log out
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sliders.svg"; // new user / register
|
|
||||||
// forms etc
|
|
||||||
import "../node_modules/bootstrap-icons/icons/at.svg"; // email field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/key.svg"; // new password field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/key-fill.svg"; // pw confirm field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/lock.svg"; // current pw field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/shield.svg"; // role
|
|
||||||
// live tables
|
|
||||||
import "../node_modules/bootstrap-icons/icons/arrow-down-up.svg"; // sort
|
|
||||||
import "../node_modules/bootstrap-icons/icons/funnel.svg"; // filter
|
|
||||||
import "../node_modules/bootstrap-icons/icons/x-circle-fill.svg"; // clear filter
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sort-down-alt.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sort-up-alt.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/chevron-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/chevron-right.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/pencil.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/trash.svg";
|
|
||||||
// page headers
|
|
||||||
import "../node_modules/bootstrap-icons/icons/shield-lock.svg"; // reset password
|
|
||||||
import "../node_modules/bootstrap-icons/icons/arrow-repeat.svg"; // resend confirmation
|
|
||||||
import "../node_modules/@mdi/svg/svg/head-question-outline.svg"; // forgot password
|
|
||||||
import "../node_modules/bootstrap-icons/icons/people.svg"; // users management
|
|
||||||
// calendar/event icons
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-plus.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-date.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-event.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-range.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/clock-history.svg"; // shift template
|
|
||||||
import "../node_modules/bootstrap-icons/icons/tag.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/hourglass.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/map.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/geo.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/justify-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/plus-circle-dotted.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/clipboard-plus.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/star.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/star-fill.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/binoculars.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/binoculars-fill.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/eraser.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/save.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/asterisk.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/card-list.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/file-earmark-spreadsheet.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/box-arrow-in-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/link.svg";
|
|
||||||
|
|
||||||
// webpack automatically bundles all modules in your
|
// import Socket for Phoenix Channels
|
||||||
// entry points. Those entry points can be configured
|
|
||||||
// in "webpack.config.js".
|
|
||||||
//
|
|
||||||
// Import deps with the dep name or local files with a relative path, for example:
|
|
||||||
//
|
|
||||||
// import {Socket} from "phoenix"
|
|
||||||
// import socket from "./socket"
|
|
||||||
//
|
|
||||||
import "phoenix_html";
|
|
||||||
import { Socket } from "phoenix";
|
import { Socket } from "phoenix";
|
||||||
|
// import topbar for load progress in live reloading / liveview
|
||||||
import topbar from "topbar";
|
import topbar from "topbar";
|
||||||
|
// import LiveSocket for LiveView
|
||||||
import { LiveSocket } from "phoenix_live_view";
|
import { LiveSocket } from "phoenix_live_view";
|
||||||
|
|
||||||
|
|
||||||
// Bootstrap v5 js imports
|
// Bootstrap v5 js imports
|
||||||
import "bootstrap/js/dist/alert";
|
import 'bootstrap/js/dist/alert';
|
||||||
import "bootstrap/js/dist/collapse";
|
import 'bootstrap/js/dist/collapse';
|
||||||
import "bootstrap/js/dist/dropdown";
|
import 'bootstrap/js/dist/dropdown';
|
||||||
// Bootstrap helpers
|
// Bootstrap helpers
|
||||||
import "./_hamburger-helper";
|
import './_hamburger-helper';
|
||||||
import "./_form-validity";
|
import "./_form-validity";
|
||||||
// Bootstrap-liveview helpers
|
// Bootstrap-liveview helpers
|
||||||
import { AlertRemover } from "./_alert-remover";
|
import { AlertRemover } from "./_alert-remover";
|
||||||
|
|
17636
assets/package-lock.json
generated
17636
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,43 +1,30 @@
|
||||||
{
|
{
|
||||||
"repository": {},
|
"name": "vanilla",
|
||||||
"description": " ",
|
"private": true,
|
||||||
"license": "MIT",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "NODE_ENV=production webpack --mode production",
|
"dev": "vite",
|
||||||
"watch": "webpack --mode development --watch"
|
"build": "vite build",
|
||||||
},
|
"preview": "vite preview"
|
||||||
"dependencies": {
|
|
||||||
"@fontsource/lato": "^4.2.2",
|
|
||||||
"@mdi/svg": "^5.9.55",
|
|
||||||
"@popperjs/core": "^2.9.2",
|
|
||||||
"bootstrap": "^5.0.0-beta3",
|
|
||||||
"bootstrap-icons": "^1.4.1",
|
|
||||||
"hamburgers": "^1.1.3",
|
|
||||||
"heroicons": "^1.0.0",
|
|
||||||
"phoenix": "file:../deps/phoenix",
|
|
||||||
"phoenix_html": "file:../deps/phoenix_html",
|
|
||||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
|
||||||
"topbar": "^1.x"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.x",
|
"@fullhuman/postcss-purgecss": "^5.0.0",
|
||||||
"@babel/preset-env": "^7.x",
|
"@types/node": "^18.6.5",
|
||||||
"autoprefixer": "^10.2.5",
|
"@types/phoenix": "^1.5.4",
|
||||||
"babel-loader": "^8.x",
|
"autoprefixer": "^10.4.13",
|
||||||
"copy-webpack-plugin": "^8.x",
|
"cssnano": "^5.1.14",
|
||||||
"css-loader": "^5.x",
|
"phoenix_live_view": "^0.18.11",
|
||||||
"css-minimizer-webpack-plugin": "^1.x",
|
"sass": "^1.54.3",
|
||||||
"file-loader": "^6.2.0",
|
"vite": "^4.0.4"
|
||||||
"glob-all": "^3.2.1",
|
},
|
||||||
"mini-css-extract-plugin": "^1.x",
|
"dependencies": {
|
||||||
"postcss": "^8.2.9",
|
"@fontsource/lato": "^4.5.9",
|
||||||
"postcss-loader": "^5.2.0",
|
"bootstrap": "^5.2.0",
|
||||||
"postcss-scss": "^3.0.5",
|
"bootstrap-icons": "^1.9.1",
|
||||||
"purgecss-webpack-plugin": "^4.0.3",
|
"hamburgers": "^1.2.1",
|
||||||
"sass": "^1.x",
|
"phoenix": "^1.6.11",
|
||||||
"sass-loader": "^11.x",
|
"phoenix_html": "^3.2.0",
|
||||||
"svg-sprite-loader": "^6.x",
|
"topbar": "^1.x"
|
||||||
"webpack": "^5.x",
|
|
||||||
"webpack-cli": "^4.x"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
assets/postcss.config.cjs
Normal file
24
assets/postcss.config.cjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const autoprefixer = require('autoprefixer')
|
||||||
|
const purgecss = require('@fullhuman/postcss-purgecss')
|
||||||
|
const cssnano = require('cssnano')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
autoprefixer,
|
||||||
|
purgecss({
|
||||||
|
content: [
|
||||||
|
"./js/**/*.js",
|
||||||
|
"../lib/*_web.ex",
|
||||||
|
"../lib/*_web/**/*.*ex"
|
||||||
|
],
|
||||||
|
safelist: {
|
||||||
|
deep: [
|
||||||
|
/^phx/
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cssnano({
|
||||||
|
preset: 'default',
|
||||||
|
})
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: require("postcss-scss"),
|
|
||||||
|
|
||||||
plugins: [require("autoprefixer")],
|
|
||||||
};
|
|
37
assets/vite.config.js
Normal file
37
assets/vite.config.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig(({ command }) => {
|
||||||
|
const isDev = command !== "build";
|
||||||
|
if (isDev) {
|
||||||
|
// Terminate the watcher when Phoenix quits
|
||||||
|
process.stdin.on("close", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
publicDir: "static",
|
||||||
|
build: {
|
||||||
|
target: "esnext", // build for recent browsers
|
||||||
|
outDir: "../priv/static", // emit assets to priv/static
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: isDev, // enable source map in dev build
|
||||||
|
manifest: false, // do not generate manifest.json
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
app: "./js/app.js"
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: "assets/[name].js", // remove hash
|
||||||
|
chunkFileNames: "assets/[name].js",
|
||||||
|
assetFileNames: "assets/[name][extname]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -1,105 +0,0 @@
|
||||||
const path = require("path");
|
|
||||||
const glob = require("glob-all");
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
|
||||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
|
||||||
const SpriteLoaderPlugin = require("svg-sprite-loader/plugin");
|
|
||||||
const PurgecssPlugin = require("purgecss-webpack-plugin");
|
|
||||||
|
|
||||||
module.exports = (env, options) => {
|
|
||||||
const devMode = options.mode !== "production";
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry: {
|
|
||||||
app: glob.sync("./vendor/**/*.js").concat(["./js/app.js"]),
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, "../priv/static/js"),
|
|
||||||
filename: "[name].js",
|
|
||||||
publicPath: "/js/",
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: "babel-loader",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.[s]?css$/,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
"css-loader",
|
|
||||||
"sass-loader",
|
|
||||||
"postcss-loader",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: "file-loader",
|
|
||||||
options: {
|
|
||||||
esModule: false,
|
|
||||||
name: "[name].[ext]",
|
|
||||||
outputPath: "../fonts",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
loader: "svg-sprite-loader",
|
|
||||||
options: {
|
|
||||||
extract: true,
|
|
||||||
spriteFilename: "icons.svg",
|
|
||||||
publicPath: "../images/",
|
|
||||||
symbolId: (filePath) => {
|
|
||||||
if (filePath.includes("bootstrap-icons")) {
|
|
||||||
return `bi-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else if (filePath.includes("@mdi")) {
|
|
||||||
return `mdi-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else if (filePath.includes("heroicons")) {
|
|
||||||
if (filePath.includes("outline")) {
|
|
||||||
return `hio-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else {
|
|
||||||
return `his-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new MiniCssExtractPlugin({ filename: "../css/app.css" }),
|
|
||||||
new SpriteLoaderPlugin({ plainSprite: true }),
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [{ from: "static/", to: "../" }],
|
|
||||||
}),
|
|
||||||
].concat(
|
|
||||||
devMode
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
new PurgecssPlugin({
|
|
||||||
paths: glob.sync([
|
|
||||||
"../**/*.html.leex",
|
|
||||||
"../**/*.html.eex",
|
|
||||||
"../**/views/**/*.ex",
|
|
||||||
"../**/live/**/*.ex",
|
|
||||||
"./js/**/*.js",
|
|
||||||
]),
|
|
||||||
safelist: [/phx/, /topbar/],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
optimization: {
|
|
||||||
minimizer: ["...", new CssMinimizerPlugin()],
|
|
||||||
},
|
|
||||||
devtool: devMode ? "source-map" : undefined,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -5,19 +5,13 @@
|
||||||
# is restricted to this project.
|
# is restricted to this project.
|
||||||
|
|
||||||
# General application configuration
|
# General application configuration
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||||
|
|
||||||
config :shift73k,
|
config :shift73k,
|
||||||
ecto_repos: [Shift73k.Repo]
|
ecto_repos: [Shift73k.Repo]
|
||||||
|
|
||||||
# Custom application global variables
|
|
||||||
config :shift73k, :app_global_vars,
|
|
||||||
time_zone: "America/New_York",
|
|
||||||
mailer_reply_to: "reply_to@example.com",
|
|
||||||
mailer_from: "app_name@example.com"
|
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :shift73k, Shift73kWeb.Endpoint,
|
config :shift73k, Shift73kWeb.Endpoint,
|
||||||
url: [host: "localhost"],
|
url: [host: "localhost"],
|
||||||
|
@ -26,6 +20,18 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
pubsub_server: Shift73k.PubSub,
|
pubsub_server: Shift73k.PubSub,
|
||||||
live_view: [signing_salt: "2D4GC4ac"]
|
live_view: [signing_salt: "2D4GC4ac"]
|
||||||
|
|
||||||
|
# Configures the mailer
|
||||||
|
#
|
||||||
|
# By default it uses the "Local" adapter which stores the emails
|
||||||
|
# locally. You can see the emails in your browser, at "/dev/mailbox".
|
||||||
|
#
|
||||||
|
# For production it's recommended to configure a different adapter
|
||||||
|
# at the `config/runtime.exs`.
|
||||||
|
config :shift73k, Shift73k.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
|
# Swoosh API client is needed for adapters other than SMTP.
|
||||||
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
# Configures Elixir's Logger
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
format: "$time $metadata[$level] $message\n",
|
format: "$time $metadata[$level] $message\n",
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :shift73k, Shift73k.Repo,
|
config :shift73k, Shift73k.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
|
socket_dir: "/srv/dck/postgres/sock/postgres",
|
||||||
database: "shift73k_dev",
|
database: "shift73k_dev",
|
||||||
hostname: "localhost",
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true,
|
show_sensitive_data_on_connection_error: true,
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
|
@ -22,11 +23,7 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
node: [
|
node: [
|
||||||
"node_modules/webpack/bin/webpack.js",
|
"node_modules/vite/bin/vite.js",
|
||||||
"--mode",
|
|
||||||
"development",
|
|
||||||
"--watch",
|
|
||||||
"--watch-options-stdin",
|
|
||||||
cd: Path.expand("../assets", __DIR__)
|
cd: Path.expand("../assets", __DIR__)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -75,6 +72,3 @@ 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
|
||||||
|
|
||||||
# Import secret config
|
|
||||||
import_config "dev.secret.exs"
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# For production, don't forget to configure the url host
|
# For production, don't forget to configure the url host
|
||||||
# to something meaningful, Phoenix uses this information
|
# to something meaningful, Phoenix uses this information
|
||||||
|
@ -49,7 +49,3 @@ config :logger, level: :info
|
||||||
# force_ssl: [hsts: true]
|
# force_ssl: [hsts: true]
|
||||||
#
|
#
|
||||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||||
|
|
||||||
# Finally import the config/prod.secret.exs which loads secrets
|
|
||||||
# and configuration from environment variables.
|
|
||||||
import_config "prod.secret.exs"
|
|
||||||
|
|
133
config/runtime.exs
Normal file
133
config/runtime.exs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
# config/runtime.exs is executed for all environments, including
|
||||||
|
# during releases. It is executed after compilation and before the
|
||||||
|
# system starts, so it is typically used to load production configuration
|
||||||
|
# and secrets from environment variables or elsewhere. Do not define
|
||||||
|
# any compile-time configuration in here, as it won't be applied.
|
||||||
|
# The block below contains prod specific runtime configuration.
|
||||||
|
|
||||||
|
# ## Using releases
|
||||||
|
#
|
||||||
|
# If you use `mix release`, you need to explicitly enable the server
|
||||||
|
# by passing the PHX_SERVER=true when you start it:
|
||||||
|
#
|
||||||
|
# PHX_SERVER=true bin/shift73k 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 :shift73k, Shift73kWeb.Endpoint, server: true
|
||||||
|
end
|
||||||
|
|
||||||
|
if config_env() == :prod do
|
||||||
|
database_sock = System.get_env("DB_SOCK") || :false
|
||||||
|
database_name = System.get_env("DB_NAME") || :false
|
||||||
|
database_user = System.get_env("DB_USER") || :false
|
||||||
|
database_pass = System.get_env("DB_PASS") || :false
|
||||||
|
database_url = System.get_env("DATABASE_URL") || :false
|
||||||
|
|
||||||
|
if (!database_sock && !database_url) do
|
||||||
|
raise """
|
||||||
|
environment variable DATABASE_URL is missing.
|
||||||
|
can also configure with unix socket by providing
|
||||||
|
DB_SOCK, DB_NAME, DB_USER, and DB_PASS values.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
if (database_sock) do
|
||||||
|
if (!database_name) do
|
||||||
|
raise """
|
||||||
|
environment variable DB_NAME is missing.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
if (!database_user) do
|
||||||
|
raise """
|
||||||
|
environment variable DB_USER is missing.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
if (!database_pass) do
|
||||||
|
raise """
|
||||||
|
environment variable DB_PASS is missing.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
|
||||||
|
|
||||||
|
if (database_sock) do
|
||||||
|
config :shift73k, Shift73k.Repo,
|
||||||
|
username: database_user,
|
||||||
|
password: database_pass,
|
||||||
|
socket_dir: database_sock,
|
||||||
|
database: database_name,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||||
|
socket_options: maybe_ipv6
|
||||||
|
else
|
||||||
|
config :shift73k, Shift73k.Repo,
|
||||||
|
# ssl: true,
|
||||||
|
url: database_url,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||||
|
socket_options: maybe_ipv6
|
||||||
|
end
|
||||||
|
|
||||||
|
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||||
|
# A default value is used in config/dev.exs and config/test.exs but you
|
||||||
|
# want to use a different value for prod and you most likely don't want
|
||||||
|
# to check this value into version control, so we use an environment
|
||||||
|
# variable instead.
|
||||||
|
secret_key_base =
|
||||||
|
System.get_env("SECRET_KEY_BASE") ||
|
||||||
|
raise """
|
||||||
|
environment variable SECRET_KEY_BASE is missing.
|
||||||
|
You can generate one by calling: mix phx.gen.secret
|
||||||
|
"""
|
||||||
|
|
||||||
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
|
config :shift73k, Shift73kWeb.Endpoint,
|
||||||
|
url: [host: host, port: 443, scheme: "https"],
|
||||||
|
http: [ip: {0, 0, 0, 0}, port: port],
|
||||||
|
secret_key_base: secret_key_base
|
||||||
|
|
||||||
|
# ## Configuring the mailer
|
||||||
|
#
|
||||||
|
# In production you need to configure the mailer to use a different adapter.
|
||||||
|
# Also, you may need to configure the Swoosh API client of your choice if you
|
||||||
|
# are not using SMTP. Here is an example of the configuration:
|
||||||
|
#
|
||||||
|
# config :shift73k, Shift73k.Mailer,
|
||||||
|
# adapter: Swoosh.Adapters.Mailgun,
|
||||||
|
# api_key: System.get_env("MAILGUN_API_KEY"),
|
||||||
|
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||||
|
#
|
||||||
|
# For this example you need include a HTTP client required by Swoosh API client.
|
||||||
|
# Swoosh supports Hackney and Finch out of the box:
|
||||||
|
#
|
||||||
|
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
||||||
|
#
|
||||||
|
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
||||||
|
# Swoosh mailer config
|
||||||
|
config :shift73k, Shift73k.Mailer,
|
||||||
|
adapter: Swoosh.Adapters.SMTP,
|
||||||
|
relay: System.get_env("SMTP_RELAY"),
|
||||||
|
port: System.get_env("SMTP_PORT"),
|
||||||
|
username: System.get_env("SMTP_USER"),
|
||||||
|
password: System.get_env("SMTP_PASS"),
|
||||||
|
ssl: false,
|
||||||
|
tls: :always,
|
||||||
|
auth: :always,
|
||||||
|
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
||||||
|
retries: 1,
|
||||||
|
no_mx_lookups: false
|
||||||
|
|
||||||
|
config :shift73k, :app_global_vars,
|
||||||
|
time_zone: System.get_env("TZ") || "America/New_York",
|
||||||
|
mailer_reply_to: System.get_env("MAIL_REPLY_TO") || "reply_to@example.com",
|
||||||
|
mailer_from: {
|
||||||
|
System.get_env("MAIL_FROM_FRIENDLY") || "Shift73k",
|
||||||
|
System.get_env("MAIL_FROM_ADDR") || "app_name@example.com"
|
||||||
|
},
|
||||||
|
allow_registration: System.get_env("ALLOW_REG") || :true
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# Only in tests, remove the complexity from the password hashing algorithm
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
config :bcrypt_elixir, :log_rounds, 1
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
@ -13,7 +13,8 @@ config :shift73k, Shift73k.Repo,
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
database: "shift73k_test#{System.get_env("MIX_TEST_PARTITION")}",
|
database: "shift73k_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
pool: Ecto.Adapters.SQL.Sandbox
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
|
pool_size: 10
|
||||||
|
|
||||||
# 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.
|
||||||
|
@ -24,8 +25,8 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
# Print only warnings and errors during test
|
# Print only warnings and errors during test
|
||||||
config :logger, level: :warn
|
config :logger, level: :warn
|
||||||
|
|
||||||
# Bamboo test mailer config
|
# Swoosh test mailer config
|
||||||
config :shift73k, Shift73k.Mailer, adapter: Bamboo.TestAdapter
|
config :shift73k, Shift73k.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
# Import secret config
|
# Import secret config
|
||||||
import_config "test.secret.exs"
|
# import_config "test.secret.exs"
|
||||||
|
|
7
entrypoint.sh
Executable file
7
entrypoint.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/ash
|
||||||
|
|
||||||
|
export MIX_ENV="prod"
|
||||||
|
|
||||||
|
cd /app
|
||||||
|
mix ecto.migrate
|
||||||
|
exec mix phx.server
|
|
@ -7,12 +7,32 @@ defmodule Shift73k do
|
||||||
if it comes from the database, an external API or others.
|
if it comes from the database, an external API or others.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@app_vars Application.compile_env(:shift73k, :app_global_vars, time_zone: "America/New_York")
|
|
||||||
@app_time_zone @app_vars[:time_zone]
|
|
||||||
|
|
||||||
@weekdays [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
|
@weekdays [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
|
||||||
|
|
||||||
def app_time_zone, do: @app_time_zone
|
defp get_app_config_env do
|
||||||
|
Application.get_env(:shift73k, :app_global_vars,
|
||||||
|
time_zone: "America/New_York",
|
||||||
|
allow_registration: :true,
|
||||||
|
mailer_reply_to: "admin@example.com",
|
||||||
|
mailer_from: {"Shift73k", "shift73k@example.com"}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def weekdays, do: @weekdays
|
def weekdays, do: @weekdays
|
||||||
|
|
||||||
|
def get_app_time_zone, do:
|
||||||
|
get_app_config_env() |> Keyword.fetch!(:time_zone) |> IO.inspect(label: "time_zone", pretty: :true)
|
||||||
|
|
||||||
|
def get_app_mailer_from, do:
|
||||||
|
get_app_config_env() |> Keyword.fetch!(:mailer_from) |> IO.inspect(label: "mailer_from", pretty: :true)
|
||||||
|
|
||||||
|
def get_app_mailer_reply_to, do:
|
||||||
|
get_app_config_env() |> Keyword.fetch!(:mailer_reply_to) |> IO.inspect(label: "mailer_reply_to", pretty: :true)
|
||||||
|
|
||||||
|
def get_app_allow_reg, do:
|
||||||
|
get_app_config_env() |> Keyword.fetch!(:allow_registration) |> get_app_allow_reg()
|
||||||
|
|> IO.inspect(label: "allow_registration", pretty: :true)
|
||||||
|
def get_app_allow_reg("false"), do: :false
|
||||||
|
def get_app_allow_reg(:false), do: :false
|
||||||
|
def get_app_allow_reg(_not_false), do: :true
|
||||||
end
|
end
|
||||||
|
|
|
@ -108,6 +108,13 @@ defmodule Shift73k.Accounts do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def register_user(attrs) do
|
def register_user(attrs) do
|
||||||
|
# If attrs has atom keys, convert to string
|
||||||
|
# If attrs don't include role, put default role
|
||||||
|
attrs =
|
||||||
|
attrs
|
||||||
|
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||||
|
|> Map.put_new("role", registration_role())
|
||||||
|
|
||||||
%User{}
|
%User{}
|
||||||
|> User.registration_changeset(attrs)
|
|> User.registration_changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
|
@ -20,19 +20,19 @@ defmodule Shift73k.Accounts.User do
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
@foreign_key_type :binary_id
|
@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 :confirmed_at, :naive_datetime
|
||||||
field(:calendar_slug, :string, default: Ecto.UUID.generate())
|
field :calendar_slug, :string, default: Ecto.UUID.generate()
|
||||||
|
|
||||||
field(:role, Ecto.Enum, values: Keyword.keys(@roles), default: :user)
|
field :role, Ecto.Enum, values: Keyword.keys(@roles), default: :user
|
||||||
field(:week_start_at, Ecto.Enum, values: weekdays(), default: :monday)
|
field :week_start_at, Ecto.Enum, values: weekdays(), default: :monday
|
||||||
|
|
||||||
has_many(:shift_templates, ShiftTemplate)
|
has_many :shift_templates, ShiftTemplate
|
||||||
belongs_to(:fave_shift_template, ShiftTemplate)
|
belongs_to :fave_shift_template, ShiftTemplate
|
||||||
|
|
||||||
has_many(:shifts, Shift)
|
has_many :shifts, Shift
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,19 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
alias Shift73k.Mailer
|
alias Shift73k.Mailer
|
||||||
alias Shift73k.Mailer.UserEmail
|
alias Shift73k.Mailer.UserEmail
|
||||||
|
|
||||||
|
def deliver(user_email, subject, body) do
|
||||||
|
%Swoosh.Email{} = email = UserEmail.compose(user_email, subject, body)
|
||||||
|
case Mailer.deliver(email) do
|
||||||
|
{:ok, msg} -> {:ok, msg, email}
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to confirm account.
|
Deliver instructions to confirm account.
|
||||||
"""
|
"""
|
||||||
def deliver_confirmation_instructions(user, url) do
|
def deliver_confirmation_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Confirmation instructions", """
|
||||||
|> UserEmail.compose("Confirm Your Account", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -21,15 +28,13 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to reset a user password.
|
Deliver instructions to reset a user password.
|
||||||
"""
|
"""
|
||||||
def deliver_reset_password_instructions(user, url) do
|
def deliver_reset_password_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Reset password instructions", """
|
||||||
|> UserEmail.compose("Reset Your Password", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -43,15 +48,13 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to update a user email.
|
Deliver instructions to update a user email.
|
||||||
"""
|
"""
|
||||||
def deliver_update_email_instructions(user, url) do
|
def deliver_update_email_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Update email instructions", """
|
||||||
|> UserEmail.compose("Change Your E-mail", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -65,6 +68,5 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,10 +15,10 @@ defmodule Shift73k.Accounts.UserToken do
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
@foreign_key_type :binary_id
|
@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
|
||||||
field(:sent_to, :string)
|
field :sent_to, :string
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
|
|
||||||
timestamps(updated_at: false)
|
timestamps(updated_at: false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
defmodule Shift73k.Mailer do
|
defmodule Shift73k.Mailer do
|
||||||
use Bamboo.Mailer, otp_app: :shift73k
|
use Swoosh.Mailer, otp_app: :shift73k
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
defmodule Shift73k.Mailer.UserEmail do
|
defmodule Shift73k.Mailer.UserEmail do
|
||||||
import Bamboo.Email
|
import Swoosh.Email
|
||||||
|
import Shift73k, only: [get_app_mailer_from: 0, get_app_mailer_reply_to: 0]
|
||||||
|
|
||||||
@mailer_vars Application.compile_env(:shift73k, :app_global_vars,
|
|
||||||
mailer_reply_to: "admin@example.com",
|
|
||||||
mailer_from: {"Shift73k", "shift73k@example.com"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def compose(user, subject, body_text) do
|
def compose(user_email, subject, body_text) do
|
||||||
new_email()
|
new()
|
||||||
|> from(@mailer_vars[:mailer_from])
|
|> from(get_app_mailer_from())
|
||||||
|> to(user.email)
|
|> to(user_email)
|
||||||
|> put_header("Reply-To", @mailer_vars[:mailer_reply_to])
|
|> header("Reply-To", get_app_mailer_reply_to())
|
||||||
|> subject(subject)
|
|> subject(subject)
|
||||||
|> text_body(body_text)
|
|> text_body(body_text)
|
||||||
end
|
end
|
||||||
|
|
28
lib/shift73k/release.ex
Normal file
28
lib/shift73k/release.ex
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule Shift73k.Release do
|
||||||
|
@moduledoc """
|
||||||
|
Used for executing DB release tasks when run in production without Mix
|
||||||
|
installed.
|
||||||
|
"""
|
||||||
|
@app :shift73k
|
||||||
|
|
||||||
|
def migrate do
|
||||||
|
load_app()
|
||||||
|
|
||||||
|
for repo <- repos() do
|
||||||
|
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rollback(repo, version) do
|
||||||
|
load_app()
|
||||||
|
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp repos do
|
||||||
|
Application.fetch_env!(@app, :ecto_repos)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_app do
|
||||||
|
Application.load(@app)
|
||||||
|
end
|
||||||
|
end
|
|
@ -86,8 +86,12 @@ defmodule Shift73k.Shifts do
|
||||||
** (Ecto.NoResultsError)
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
def get_shift!(nil), do: nil
|
||||||
def get_shift!(id), do: Repo.get!(Shift, id)
|
def get_shift!(id), do: Repo.get!(Shift, id)
|
||||||
|
|
||||||
|
def get_shift(nil), do: nil
|
||||||
|
def get_shift(id), do: Repo.get(Shift, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a shift.
|
Creates a shift.
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Shift73k.Shifts.Shift do
|
||||||
field :time_start, :time
|
field :time_start, :time
|
||||||
field :time_end, :time
|
field :time_end, :time
|
||||||
|
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Shift73k, only: [app_time_zone: 0]
|
import Shift73k, only: [get_app_time_zone: 0]
|
||||||
|
|
||||||
alias Shift73k.Shifts
|
alias Shift73k.Shifts
|
||||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||||
|
@ -12,12 +12,12 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
field :subject, :string
|
field :subject, :string
|
||||||
field :description, :string
|
field :description, :string
|
||||||
field :location, :string
|
field :location, :string
|
||||||
field :time_zone, :string, default: app_time_zone()
|
field :time_zone, :string, default: get_app_time_zone()
|
||||||
field :time_start, :time, default: ~T[09:00:00]
|
field :time_start, :time, default: ~T[09:00:00]
|
||||||
field :time_end, :time, default: ~T[17:00:00]
|
field :time_end, :time, default: ~T[17:00:00]
|
||||||
|
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
has_one(:is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id)
|
has_one :is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -57,6 +57,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|> validate_not_nil([:time_zone])
|
||||||
|> validate_inclusion(:time_zone, Tzdata.zone_list(),
|
|> validate_inclusion(:time_zone, Tzdata.zone_list(),
|
||||||
message: "must be a valid IANA tz database time zone"
|
message: "must be a valid IANA tz database time zone"
|
||||||
)
|
)
|
||||||
|
@ -72,4 +73,14 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
|
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_not_nil(changeset, fields) do
|
||||||
|
Enum.reduce(fields, changeset, fn field, changeset ->
|
||||||
|
if get_field(changeset, field) == nil do
|
||||||
|
add_error(changeset, field, "nil")
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,7 +46,7 @@ defmodule Shift73kWeb do
|
||||||
def live_view do
|
def live_view do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.LiveView,
|
use Phoenix.LiveView,
|
||||||
layout: {Shift73kWeb.LayoutView, "live.html"}
|
layout: {Shift73kWeb.LayoutView, :live}
|
||||||
|
|
||||||
unquote(view_helpers())
|
unquote(view_helpers())
|
||||||
import Shift73kWeb.LiveHelpers
|
import Shift73kWeb.LiveHelpers
|
||||||
|
@ -100,14 +100,11 @@ defmodule Shift73kWeb do
|
||||||
use Phoenix.HTML
|
use Phoenix.HTML
|
||||||
|
|
||||||
# Import LiveView helpers (live_render, live_component, live_patch, etc)
|
# Import LiveView helpers (live_render, live_component, live_patch, etc)
|
||||||
import Phoenix.LiveView.Helpers
|
import Phoenix.Component
|
||||||
|
|
||||||
# Import basic rendering functionality (render, render_layout, etc)
|
# Import basic rendering functionality (render, render_layout, etc)
|
||||||
import Phoenix.View
|
import Phoenix.View
|
||||||
|
|
||||||
# Import SVG Icon helper
|
|
||||||
import Shift73kWeb.IconHelpers
|
|
||||||
|
|
||||||
import Shift73kWeb.ErrorHelpers
|
import Shift73kWeb.ErrorHelpers
|
||||||
import Shift73kWeb.Gettext
|
import Shift73kWeb.Gettext
|
||||||
alias Shift73kWeb.Router.Helpers, as: Routes
|
alias Shift73kWeb.Router.Helpers, as: Routes
|
||||||
|
|
|
@ -10,51 +10,50 @@ defmodule Shift73kWeb.Endpoint do
|
||||||
signing_salt: "9CKxo0VJ"
|
signing_salt: "9CKxo0VJ"
|
||||||
]
|
]
|
||||||
|
|
||||||
socket("/socket", Shift73kWeb.UserSocket,
|
socket "/socket", Shift73kWeb.UserSocket,
|
||||||
websocket: true,
|
websocket: true,
|
||||||
longpoll: false
|
longpoll: false
|
||||||
)
|
|
||||||
|
|
||||||
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
|
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
#
|
#
|
||||||
# You should set gzip to true if you are running phx.digest
|
# You should set gzip to true if you are running phx.digest
|
||||||
# when deploying your static files in production.
|
# when deploying your static files in production.
|
||||||
#
|
#
|
||||||
# file list generated by simple ls -1 assets/static/ - then copy/paste here
|
# file list generated by simple ls -1 priv/static/ - then copy/paste here
|
||||||
plug(Plug.Static,
|
plug Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :shift73k,
|
from: :shift73k,
|
||||||
gzip: (Mix.env() not in [:dev, :test]),
|
gzip: false
|
||||||
only: "priv/static" |> Path.expand() |> File.ls!()
|
|
||||||
)
|
# For using vite.js in dev, we need to instruct Phoenix to serve files at assets/src over the usual endpoint. This is only necessary in development.
|
||||||
|
if Mix.env() == :dev do
|
||||||
|
plug Plug.Static,
|
||||||
|
at: "/",
|
||||||
|
from: "assets",
|
||||||
|
gzip: false
|
||||||
|
end
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
if code_reloading? do
|
if code_reloading? do
|
||||||
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
|
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||||
plug(Phoenix.LiveReloader)
|
plug Phoenix.LiveReloader
|
||||||
plug(Phoenix.CodeReloader)
|
plug Phoenix.CodeReloader
|
||||||
plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k)
|
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k
|
||||||
end
|
end
|
||||||
|
|
||||||
plug(Phoenix.LiveDashboard.RequestLogger,
|
plug Plug.RequestId
|
||||||
param_key: "request_logger",
|
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||||
cookie_key: "request_logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
plug(Plug.RequestId)
|
plug Plug.Parsers,
|
||||||
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
|
|
||||||
|
|
||||||
plug(Plug.Parsers,
|
|
||||||
parsers: [:urlencoded, :multipart, :json],
|
parsers: [:urlencoded, :multipart, :json],
|
||||||
pass: ["*/*"],
|
pass: ["*/*"],
|
||||||
json_decoder: Phoenix.json_library()
|
json_decoder: Phoenix.json_library()
|
||||||
)
|
|
||||||
|
|
||||||
plug(Plug.MethodOverride)
|
plug Plug.MethodOverride
|
||||||
plug(Plug.Head)
|
plug Plug.Head
|
||||||
plug(Plug.Session, @session_options)
|
plug Plug.Session, @session_options
|
||||||
plug(Shift73kWeb.Router)
|
plug Shift73kWeb.Router
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Shift73kWeb.LiveHelpers do
|
defmodule Shift73kWeb.LiveHelpers do
|
||||||
import Phoenix.LiveView
|
import Phoenix.LiveView
|
||||||
import Phoenix.LiveView.Helpers
|
import Phoenix.Component
|
||||||
|
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
@ -19,27 +19,6 @@ defmodule Shift73kWeb.LiveHelpers do
|
||||||
"""
|
"""
|
||||||
def live_okreply(socket), do: {:ok, socket}
|
def live_okreply(socket), do: {:ok, socket}
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a component inside the `Shift73kWeb.ModalComponent` component.
|
|
||||||
|
|
||||||
The rendered modal receives a `:return_to` option to properly update
|
|
||||||
the URL when the modal is closed.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<%= live_modal @socket, Shift73kWeb.PropertyLive.FormComponent,
|
|
||||||
id: @property.id || :new,
|
|
||||||
action: @live_action,
|
|
||||||
property: @property,
|
|
||||||
return_to: Routes.property_index_path(@socket, :index) %>
|
|
||||||
"""
|
|
||||||
def live_modal(socket, component, opts) do
|
|
||||||
modal_opts = [id: :modal, component: component, opts: opts]
|
|
||||||
# dirty little workaround for elixir complaining about socket being unused
|
|
||||||
_socket = socket
|
|
||||||
live_component(socket, Shift73kWeb.ModalComponent, modal_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Loads default assigns for liveviews
|
Loads default assigns for liveviews
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,12 +3,12 @@ defmodule Shift73kWeb.ModalComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~H"""
|
||||||
<div id="<%= @id %>" class="modal fade"
|
<div id={@id} class="modal fade"
|
||||||
phx-hook="BsModal"
|
phx-hook="BsModal"
|
||||||
phx-window-keydown="hide"
|
phx-window-keydown="hide"
|
||||||
phx-key="escape"
|
phx-key="escape"
|
||||||
phx-target="#<%= @id %>"
|
phx-target={"#" <> to_string(@id)}
|
||||||
phx-page-loading>
|
phx-page-loading>
|
||||||
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
@ -16,10 +16,10 @@ defmodule Shift73kWeb.ModalComponent do
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
|
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
|
||||||
<button type="button" class="btn-close" phx-click="hide" phx-target="<%= @myself %>" aria-label="Close"></button>
|
<button type="button" class="btn-close" phx-click="hide" phx-target={@myself} aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= live_component @socket, @component, Keyword.put(@opts, :modal_id, @id) %>
|
<%= live_component @component, Keyword.put(@opts, :modal_id, @id) %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<p>Are you sure you want to delete all assigned shifts from the selected days?</p>
|
||||||
|
|
||||||
|
<%= for {y, data} <- @date_map do %>
|
||||||
|
<dt><%= y %></dt>
|
||||||
|
<% months = Map.keys(data) %>
|
||||||
|
<dd>
|
||||||
|
<%= for {m, i} <- Enum.with_index(months, 1) do %>
|
||||||
|
<%= data |> Map.get(m) |> hd() |> Calendar.strftime("%b") %>:
|
||||||
|
<% days = Map.get(data, m) %>
|
||||||
|
<%= for {d, i} <- Enum.with_index(days, 1) do %>
|
||||||
|
<%= d.day %><%= if i < length(days) do %>,<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if i < length(months) do %><br /><% end %>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm-delete-days-shifts",
|
||||||
|
phx_target: @myself
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,31 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<p>Are you sure you want to delete all assigned shifts from the selected days?</p>
|
|
||||||
|
|
||||||
<%= for {y, data} <- @date_map do %>
|
|
||||||
<dt><%= y %></dt>
|
|
||||||
<% months = Map.keys(data) %>
|
|
||||||
<dd>
|
|
||||||
<%= for {m, i} <- Enum.with_index(months, 1) do %>
|
|
||||||
<%= data |> Map.get(m) |> hd() |> Calendar.strftime("%b") %>:
|
|
||||||
<% days = Map.get(data, m) %>
|
|
||||||
<%= for {d, i} <- Enum.with_index(days, 1) do %>
|
|
||||||
<%= d.day %><%= if i < length(days) do %>,<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= if i < length(months) do %><br /><% end %>
|
|
||||||
<% end %>
|
|
||||||
</dd>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm-delete-days-shifts",
|
|
||||||
phx_target: @myself
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Shift73kWeb.ShiftAssignLive.Index do
|
defmodule Shift73kWeb.ShiftAssignLive.Index do
|
||||||
use Shift73kWeb, :live_view
|
use Shift73kWeb, :live_view
|
||||||
|
import Shift73k, only: [get_app_time_zone: 0]
|
||||||
|
|
||||||
alias Shift73k.Repo
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Shifts
|
alias Shift73k.Shifts
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
<%= if @delete_days_shifts do %>
|
<%= if @delete_days_shifts do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftAssignLive.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftAssignLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: "delete-days-shifts-#{@current_user.id}",
|
id: "delete-days-shifts-#{@current_user.id}",
|
||||||
title: "Delete Shifts From Selected Days",
|
title: "Delete Shifts From Selected Days",
|
||||||
delete_days_shifts: @delete_days_shifts,
|
delete_days_shifts: @delete_days_shifts,
|
||||||
current_user: @current_user
|
current_user: @current_user
|
||||||
%>
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %>
|
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||||
Schedule Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="row justify-content-center mt-4">
|
<div class="row justify-content-center mt-4">
|
||||||
|
@ -24,14 +28,19 @@
|
||||||
%>
|
%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<button type="button" class="ms-2 btn btn-primary text-nowrap <%= if @show_template_btn_active, do: "active" %>" id="#templateDetailsBtn" phx-click="toggle-template-details" phx-value-target_id="#templateDetailsCol">
|
<% details_button_class = "ms-2 btn btn-primary text-nowrap"
|
||||||
<%= icon_div @socket, (@show_template_btn_active && "bi-binoculars-fill" || "bi-binoculars"), [class: "icon baseline"] %>
|
details_button_class = if @show_template_btn_active, do: "#{details_button_class} active", else: details_button_class %>
|
||||||
|
<button type="button" class={details_button_class} id="#templateDetailsBtn" phx-click="toggle-template-details" phx-value-target_id="#templateDetailsCol">
|
||||||
|
<i class={@show_template_btn_active && "bi bi-binoculars-fill me-sm-1" || "bi bi-binoculars me-sm-1"}></i>
|
||||||
<span class="d-none d-sm-inline">Details</span>
|
<span class="d-none d-sm-inline">Details</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-9 col-xl-8 col-xxl-7 <%= @show_template_details && "collapse show" || "collapse" %>" id="#templateDetailsCol" phx-hook="BsCollapse">
|
|
||||||
|
<% template_details_div_class = "col-12 col-lg-9 col-xl-8 col-xxl-7 collapse"
|
||||||
|
template_details_div_class = if @show_template_details, do: "#{template_details_div_class} show", else: template_details_div_class %>
|
||||||
|
<div class={template_details_div_class} id="#templateDetailsCol" phx-hook="BsCollapse">
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
@ -41,8 +50,8 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :subject, "Subject/Title", class: "form-label" %>
|
<%= label stf, :subject, "Subject/Title", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :subject) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :subject)}>
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-tag icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :subject,
|
<%= text_input stf, :subject,
|
||||||
value: input_value(stf, :subject),
|
value: input_value(stf, :subject),
|
||||||
class: input_class(stf, :subject, "form-control"),
|
class: input_class(stf, :subject, "form-control"),
|
||||||
|
@ -57,25 +66,27 @@
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
<div class="row gx-2 gx-sm-3">
|
<div class="row gx-2 gx-sm-3">
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(stf, :time_start) %>">
|
<div class="col-6" phx-feedback-for={input_id(stf, :time_start)}>
|
||||||
<%= label stf, :time_start, "Start", class: "form-label" %>
|
<%= label stf, :time_start, "Start", class: "form-label" %>
|
||||||
<%= time_input stf, :time_start,
|
<%= time_input stf, :time_start,
|
||||||
precision: :minute,
|
precision: :minute,
|
||||||
value: input_value(stf, :time_start),
|
value: input_value(stf, :time_start),
|
||||||
class: input_class(stf, :time_start, "form-control"),
|
class: input_class(stf, :time_start, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
aria_describedby: error_ids(stf, :time_start)
|
aria_describedby: error_ids(stf, :time_start),
|
||||||
|
required: true
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(stf, :time_end) %>">
|
<div class="col-6" phx-feedback-for={input_id(stf, :time_end)}>
|
||||||
<%= label stf, :time_end, "End", class: "form-label" %>
|
<%= label stf, :time_end, "End", class: "form-label" %>
|
||||||
<%= time_input stf, :time_end,
|
<%= time_input stf, :time_end,
|
||||||
precision: :minute,
|
precision: :minute,
|
||||||
value: input_value(stf, :time_end),
|
value: input_value(stf, :time_end),
|
||||||
class: input_class(stf, :time_end, "form-control"),
|
class: input_class(stf, :time_end, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
aria_describedby: error_ids(stf, :time_end)
|
aria_describedby: error_ids(stf, :time_end),
|
||||||
|
required: true
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -83,18 +94,18 @@
|
||||||
|
|
||||||
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
||||||
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_start) %>">
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(stf, :time_start)}>
|
||||||
<%= error_tag stf, :time_start %>
|
<%= error_tag stf, :time_start %>
|
||||||
</div>
|
</div>
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_end) %>">
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(stf, :time_end)}>
|
||||||
<%= error_tag stf, :time_end %>
|
<%= error_tag stf, :time_end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :location, class: "form-label" %>
|
<%= label stf, :location, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :location) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :location)}>
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-geo icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :location,
|
<%= text_input stf, :location,
|
||||||
value: input_value(stf, :location),
|
value: input_value(stf, :location),
|
||||||
class: input_class(stf, :location, "form-control"),
|
class: input_class(stf, :location, "form-control"),
|
||||||
|
@ -108,18 +119,19 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :time_zone, class: "form-label" %>
|
<%= label stf, :time_zone, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3 mb-md-0" phx-feedback-for="<%= input_id(stf, :time_zone) %>">
|
<div class="inner-addon left-addon mb-3 mb-md-0" phx-feedback-for={input_id(stf, :time_zone)}>
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :time_zone,
|
<%= text_input stf, :time_zone,
|
||||||
value: input_value(stf, :time_zone),
|
value: input_value(stf, :time_zone),
|
||||||
class: input_class(stf, :time_zone, "form-control"),
|
class: input_class(stf, :time_zone, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
phx_debounce: 250,
|
phx_debounce: 250,
|
||||||
list: "tz_list"
|
list: "tz_list",
|
||||||
|
placeholder: "Default: #{get_app_time_zone()}"
|
||||||
%>
|
%>
|
||||||
<datalist id="tz_list">
|
<datalist id="tz_list">
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
<option value="<%= tz_name %>"></option>
|
<option value={tz_name}></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
end
|
end
|
||||||
</datalist>
|
</datalist>
|
||||||
|
@ -132,7 +144,7 @@
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<%= label stf, :description, class: "form-label" %>
|
<%= label stf, :description, class: "form-label" %>
|
||||||
<div phx-feedback-for="<%= input_id(stf, :description) %>">
|
<div phx-feedback-for={input_id(stf, :description)}>
|
||||||
|
|
||||||
<%= textarea stf, :description,
|
<%= textarea stf, :description,
|
||||||
value: input_value(stf, :description),
|
value: input_value(stf, :description),
|
||||||
|
@ -174,17 +186,17 @@
|
||||||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
|
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" disabled={if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
<i class="bi bi-asterisk me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Today</span>
|
<span class="d-none d-sm-inline">Today</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Prev</span>
|
<span class="d-none d-sm-inline">Prev</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||||
<span class="d-none d-sm-inline">Next</span>
|
<span class="d-none d-sm-inline">Next</span>
|
||||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -204,7 +216,7 @@
|
||||||
<%= for week <- @week_rows do %>
|
<%= for week <- @week_rows do %>
|
||||||
<tr>
|
<tr>
|
||||||
<%= for day <- week do %>
|
<%= for day <- week do %>
|
||||||
<td class="<%= day_color(day, @current_date, @cursor_date, @selected_days) %>" phx-click="select-day" phx-value-day="<%= day %>">
|
<td class={day_color(day, @current_date, @cursor_date, @selected_days)} phx-click="select-day" phx-value-day={day}>
|
||||||
|
|
||||||
<%= Calendar.strftime(day, "%d") %><%= if day.month != @cursor_date.month, do: "-#{Calendar.strftime(day, "%b")}" %>
|
<%= Calendar.strftime(day, "%d") %><%= if day.month != @cursor_date.month, do: "-#{Calendar.strftime(day, "%b")}" %>
|
||||||
|
|
||||||
|
@ -236,19 +248,16 @@
|
||||||
<div class="row justify-content-center justify-content-lg-end my-5">
|
<div class="row justify-content-center justify-content-lg-end my-5">
|
||||||
<div class="col-12 col-sm-10 col-md-8 col-lg-auto d-flex flex-column-reverse flex-lg-row">
|
<div class="col-12 col-sm-10 col-md-8 col-lg-auto d-flex flex-column-reverse flex-lg-row">
|
||||||
|
|
||||||
<button class="btn btn-outline-danger mb-1 mb-lg-0 me-lg-1" phx-click="delete-days-shifts" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
|
<button class="btn btn-outline-danger mb-1 mb-lg-0 me-lg-1" phx-click="delete-days-shifts" disabled={if Enum.empty?(@selected_days), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete shifts from selected days
|
||||||
Delete shifts from selected days
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-outline-dark mb-1 mb-lg-0 me-lg-1" phx-click="clear-days" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
|
<button class="btn btn-outline-dark mb-1 mb-lg-0 me-lg-1" phx-click="clear-days" disabled={if Enum.empty?(@selected_days), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-eraser", [class: "icon baseline"] %>
|
<i class="bi bi-eraser me-1"></i> De-select all selected
|
||||||
De-select all selected
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-primary mb-1 mb-lg-0" phx-click="save-days" <%= if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: "disabled" %>>
|
<button class="btn btn-primary mb-1 mb-lg-0" phx-click="save-days" disabled={if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-save", [class: "icon baseline"] %>
|
<i class="bi bi-save me-1"></i> Assign shifts to selected days
|
||||||
Assign shifts to selected days
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
|
@ -2,10 +2,8 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @socket, "bi-box-arrow-in-left", [class: "icon baseline"] %>
|
<i class="bi bi-box-arrow-in-left me-1"></i> Import Shifts
|
||||||
Import Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="lead">If you have an iCal/ics formatted calendar hosted elsewhere, provide its URL here to import its events.</p>
|
<p class="lead">If you have an iCal/ics formatted calendar hosted elsewhere, provide its URL here to import its events.</p>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
@ -17,7 +15,7 @@
|
||||||
<% valid_class = @url_validated && "is-valid" || "" %>
|
<% valid_class = @url_validated && "is-valid" || "" %>
|
||||||
<%= label iimf, :ics_url, "iCal/ics URL", class: "form-label" %>
|
<%= label iimf, :ics_url, "iCal/ics URL", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-link", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-link icon is-left text-muted fs-5"></i>
|
||||||
<%= url_input iimf, :ics_url,
|
<%= url_input iimf, :ics_url,
|
||||||
class: show_url_error && "form-control is-invalid" || "form-control #{valid_class}",
|
class: show_url_error && "form-control is-invalid" || "form-control #{valid_class}",
|
||||||
phx_debounce: 500,
|
phx_debounce: 500,
|
||||||
|
@ -33,9 +31,9 @@
|
||||||
|
|
||||||
<%= label iimf, :time_zone, class: "form-label" %>
|
<%= label iimf, :time_zone, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input iimf, :time_zone,
|
<%= text_input iimf, :time_zone,
|
||||||
value: Shift73k.app_time_zone(),
|
value: Shift73k.get_app_time_zone(),
|
||||||
class: @tz_valid && "form-control" || "form-control is-invalid",
|
class: @tz_valid && "form-control" || "form-control is-invalid",
|
||||||
phx_debounce: 250,
|
phx_debounce: 250,
|
||||||
aria_describedby: "ics-import-tz-error",
|
aria_describedby: "ics-import-tz-error",
|
||||||
|
@ -43,7 +41,7 @@
|
||||||
%>
|
%>
|
||||||
<datalist id="tz_list">
|
<datalist id="tz_list">
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
<option value="<%= tz_name %>"></option>
|
<option value={tz_name}></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</datalist>
|
</datalist>
|
||||||
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
46
lib/shift73k_web/live/shift_live/delete_component.ex
Normal file
46
lib/shift73k_web/live/shift_live/delete_component.ex
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule Shift73kWeb.ShiftLive.DeleteComponent do
|
||||||
|
use Shift73kWeb, :live_component
|
||||||
|
|
||||||
|
alias Shift73k.Shifts
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> live_okreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("confirm", %{"id" => id, "subject" => subject, "datetime" => datetime}, socket) do
|
||||||
|
shift = Shifts.get_shift(id)
|
||||||
|
|
||||||
|
if (shift) do
|
||||||
|
shift
|
||||||
|
|> Shifts.delete_shift()
|
||||||
|
|> case do
|
||||||
|
{:ok, _} ->
|
||||||
|
flash = {:info, "Shift deleted successfully: \"#{subject}\""}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
handle_error(socket, subject, datetime)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_error(socket, subject, datetime) do
|
||||||
|
flash =
|
||||||
|
{:error,
|
||||||
|
"Some error trying to delete shift \"#{subject} (#{datetime})\". Possibly already deleted? Reloading list..."}
|
||||||
|
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
end
|
22
lib/shift73k_web/live/shift_live/delete_component.html.heex
Normal file
22
lib/shift73k_web/live/shift_live/delete_component.html.heex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<% shift_datetime = "#{Calendar.strftime(@delete_shift.date, "%A, %b %-d")}, #{format_shift_time(@delete_shift.time_start)} — #{format_shift_time(@delete_shift.time_end)}" %>
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_shift.subject %> (<%= shift_datetime %>)?"
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_shift.id,
|
||||||
|
phx_value_subject: @delete_shift.subject,
|
||||||
|
phx_value_datetime: shift_datetime %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -22,6 +22,7 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
socket
|
socket
|
||||||
|> init_today(Date.utc_today())
|
|> init_today(Date.utc_today())
|
||||||
|> update_agenda()
|
|> update_agenda()
|
||||||
|
|> assign_modal_close_handlers()
|
||||||
|> assign(:delete_shift, nil)
|
|> assign(:delete_shift, nil)
|
||||||
|> apply_action(socket.assigns.live_action, params)
|
|> apply_action(socket.assigns.live_action, params)
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
@ -33,6 +34,11 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assign_modal_close_handlers(socket) do
|
||||||
|
to = Routes.shift_index_path(socket, :index)
|
||||||
|
assign(socket, modal_return_to: to, modal_close_action: :return)
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :index, _params) do
|
defp apply_action(socket, :index, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "My Shifts")
|
|> assign(:page_title, "My Shifts")
|
||||||
|
@ -76,6 +82,14 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
|> assign_known_shifts()
|
|> assign_known_shifts()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("delete-modal", %{"id" => id}, socket) do
|
||||||
|
socket
|
||||||
|
|> assign(:modal_close_action, :delete_shift)
|
||||||
|
|> assign(:delete_shift, Shifts.get_shift!(id))
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
shift = Shifts.get_shift!(id)
|
shift = Shifts.get_shift!(id)
|
||||||
|
@ -94,6 +108,28 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
|
||||||
|
socket
|
||||||
|
|> copy_flash()
|
||||||
|
|> push_patch(to: socket.assigns.modal_return_to)
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
|
||||||
|
socket
|
||||||
|
|> assign(assign_key, nil)
|
||||||
|
|> assign_modal_close_handlers()
|
||||||
|
|> assign_known_shifts()
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||||
|
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
defp new_nav_cursor("now", _cursor_date), do: Date.utc_today()
|
defp new_nav_cursor("now", _cursor_date), do: Date.utc_today()
|
||||||
|
|
||||||
defp new_nav_cursor(nav, cursor_date) do
|
defp new_nav_cursor(nav, cursor_date) do
|
||||||
|
|
|
@ -1,40 +1,48 @@
|
||||||
|
<%= if @delete_shift do %>
|
||||||
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
|
id: @delete_shift.id,
|
||||||
|
title: "Delete Shift Template",
|
||||||
|
delete_shift: @delete_shift
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<div class="row justify-content-start justify-content-sm-center">
|
<div class="row justify-content-start justify-content-sm-center">
|
||||||
<div class="col-md-10 col-xl-10">
|
<div class="col-md-10 col-xl-10">
|
||||||
|
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-card-list", [class: "icon baseline"] %>
|
<i class="bi bi-card-list me-1"></i> My Shifts
|
||||||
My Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row justify-content-start justify-content-sm-center">
|
<div class="row justify-content-start justify-content-sm-center">
|
||||||
<div class="col-md-10 col-xl-10">
|
<div class="col-md-10 col-xl-10">
|
||||||
|
|
||||||
|
|
||||||
<%# month navigation %>
|
<%# month navigation %>
|
||||||
<div class="d-flex justify-content-between align-items-end my-4">
|
<div class="d-flex justify-content-between align-items-end my-4">
|
||||||
<h3 class="text-muted mb-0">
|
<h3 class="text-muted mb-0">
|
||||||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
|
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" disabled={if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
<i class="bi bi-asterisk me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Today</span>
|
<span class="d-none d-sm-inline">Today</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Prev</span>
|
<span class="d-none d-sm-inline">Prev</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||||
<span class="d-none d-sm-inline">Next</span>
|
<span class="d-none d-sm-inline">Next</span>
|
||||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<%= for day <- Enum.to_list(@date_range) do %>
|
<%= for day <- Enum.to_list(@date_range) do %>
|
||||||
<%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
|
<%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
|
||||||
<div class="border-top mt-4 mb-4"></div>
|
<div class="border-top mt-4 mb-4"></div>
|
||||||
|
@ -45,24 +53,21 @@
|
||||||
<% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
|
<% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
|
||||||
<%= if !Enum.empty?(day_shifts) do %>
|
<%= if !Enum.empty?(day_shifts) do %>
|
||||||
|
|
||||||
|
|
||||||
<%= for shift <- day_shifts do %>
|
<%= for shift <- day_shifts do %>
|
||||||
|
|
||||||
|
<div class="card mt-2 mb-4 col-12 ms-sm-3 ms-md-4 col-lg-10 ms-lg-5 col-xxl-8" id={"shift-#{shift.id}"}>
|
||||||
<div class="card mt-2 mb-4 col-12 ms-sm-3 ms-md-4 col-lg-10 ms-lg-5 col-xxl-8" id="shift-<%= shift.id %>">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline text-muted me-1"] %>
|
<i class="bi bi-tag text-muted me-1"></i>
|
||||||
<%= shift.subject %>
|
<%= shift.subject %>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
|
||||||
<table class="table table-borderless table-nonfluid table-sm">
|
<table class="table table-borderless table-nonfluid table-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-hourglass text-muted"></i>
|
||||||
<span class="visually-hidden">Hours:</span>
|
<span class="visually-hidden">Hours:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -79,7 +84,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-geo text-muted"></i>
|
||||||
<span class="visually-hidden">Location:</span>
|
<span class="visually-hidden">Location:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -92,7 +97,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-justify-left text-muted"></i>
|
||||||
<span class="visually-hidden">Description:</span>
|
<span class="visually-hidden">Description:</span>
|
||||||
</th>
|
</th>
|
||||||
<td class="shift-description">
|
<td class="shift-description">
|
||||||
|
@ -106,51 +111,22 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<%#= if Roles.can?(@current_user, template, :edit) do %>
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={shift.id}>
|
||||||
<%#= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
<%#= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
</button>
|
||||||
<%# Edit %>
|
|
||||||
<%# end %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%#= if Roles.can?(@current_user, template, :delete) do %>
|
|
||||||
<%# <button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id=" %>
|
|
||||||
<%#= shift.id %>
|
|
||||||
<%# "> %>
|
|
||||||
<%#= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
|
||||||
<%# Delete %>
|
|
||||||
<%# </button> %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%= button to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"], class: "btn btn-outline-danger btn-sm text-nowrap" do %>
|
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
|
||||||
Delete
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="text-muted"><em>Nothing scheduled</em></p>
|
<p class="text-muted"><em>Nothing scheduled</em></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_shift_template.subject %>
|
||||||
|
(<%= format_shift_time(@delete_shift_template.time_start) %>
|
||||||
|
—
|
||||||
|
<%= format_shift_time(@delete_shift_template.time_end) %>)"?
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_shift_template.id,
|
||||||
|
phx_value_subject: @delete_shift_template.subject %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,19 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
Are you sure you want to delete "<%= @delete_shift_template.subject %>
|
|
||||||
(<%= format_shift_time(@delete_shift_template.time_start) %>
|
|
||||||
—
|
|
||||||
<%= format_shift_time(@delete_shift_template.time_end) %>)"?
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_value_id: @delete_shift_template.id,
|
|
||||||
phx_value_subject: @delete_shift_template.subject %>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
|
defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
|
||||||
use Shift73kWeb, :live_component
|
use Shift73kWeb, :live_component
|
||||||
|
import Shift73k, only: [get_app_time_zone: 0]
|
||||||
|
|
||||||
alias Shift73k.Shifts.Templates
|
alias Shift73k.Shifts.Templates
|
||||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save" phx-target={@myself} id="shift_template-form">
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<%= label f, :subject, "Subject/Title", class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :subject)}>
|
||||||
|
<i class="bi bi-tag icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :subject,
|
||||||
|
value: input_value(f, :subject),
|
||||||
|
class: input_class(f, :subject, "form-control"),
|
||||||
|
autofocus: true,
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :subject)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :subject %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row gx-2 gx-sm-3">
|
||||||
|
|
||||||
|
<div class="col-6" phx-feedback-for={input_id(f, :time_start)}>
|
||||||
|
<%= label f, :time_start, "Start", class: "form-label" %>
|
||||||
|
<%= time_input f, :time_start,
|
||||||
|
precision: :minute,
|
||||||
|
value: input_value(f, :time_start),
|
||||||
|
class: input_class(f, :time_start, "form-control"),
|
||||||
|
aria_describedby: error_ids(f, :time_start),
|
||||||
|
required: true
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6" phx-feedback-for={input_id(f, :time_end)}>
|
||||||
|
<%= label f, :time_end, "End", class: "form-label" %>
|
||||||
|
<%= time_input f, :time_end,
|
||||||
|
precision: :minute,
|
||||||
|
value: input_value(f, :time_end),
|
||||||
|
class: input_class(f, :time_end, "form-control"),
|
||||||
|
aria_describedby: error_ids(f, :time_end),
|
||||||
|
required: true
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
||||||
|
|
||||||
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(f, :time_start)}>
|
||||||
|
<%= error_tag f, :time_start %>
|
||||||
|
</div>
|
||||||
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(f, :time_end)}>
|
||||||
|
<%= error_tag f, :time_end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :location, class: "form-label mt-3" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :location)}>
|
||||||
|
<i class="bi bi-geo icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :location,
|
||||||
|
value: input_value(f, :location),
|
||||||
|
class: input_class(f, :location, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :location)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :location %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :description, class: "form-label" %>
|
||||||
|
<div class="mb-3" phx-feedback-for={input_id(f, :description)}>
|
||||||
|
|
||||||
|
<%= textarea f, :description,
|
||||||
|
value: input_value(f, :description),
|
||||||
|
class: input_class(f, :description, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :description)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :description %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :time_zone, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :time_zone)}>
|
||||||
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :time_zone,
|
||||||
|
value: input_value(f, :time_zone),
|
||||||
|
class: input_class(f, :time_zone, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
list: "tz_list",
|
||||||
|
placeholder: "Default: #{get_app_time_zone()}"
|
||||||
|
%>
|
||||||
|
<datalist id="tz_list">
|
||||||
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
|
<option value={tz_name}></option>
|
||||||
|
<% end %>
|
||||||
|
</datalist>
|
||||||
|
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
||||||
|
<%= error_tag f, :time_zone %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= submit "Save",
|
||||||
|
class: "btn btn-primary ",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,116 +0,0 @@
|
||||||
<%= f = form_for @changeset, "#",
|
|
||||||
id: "shift_template-form",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_change: "validate",
|
|
||||||
phx_submit: "save" %>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<%= label f, :subject, "Subject/Title", class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :subject) %>">
|
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :subject,
|
|
||||||
value: input_value(f, :subject),
|
|
||||||
class: input_class(f, :subject, "form-control"),
|
|
||||||
autofocus: true,
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :subject)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :subject %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row gx-2 gx-sm-3">
|
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_start) %>">
|
|
||||||
<%= label f, :time_start, "Start", class: "form-label" %>
|
|
||||||
<%= time_input f, :time_start,
|
|
||||||
precision: :minute,
|
|
||||||
value: input_value(f, :time_start),
|
|
||||||
class: input_class(f, :time_start, "form-control"),
|
|
||||||
aria_describedby: error_ids(f, :time_start)
|
|
||||||
%>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_end) %>">
|
|
||||||
<%= label f, :time_end, "End", class: "form-label" %>
|
|
||||||
<%= time_input f, :time_end,
|
|
||||||
precision: :minute,
|
|
||||||
value: input_value(f, :time_end),
|
|
||||||
class: input_class(f, :time_end, "form-control"),
|
|
||||||
aria_describedby: error_ids(f, :time_end)
|
|
||||||
%>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
|
||||||
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_start) %>">
|
|
||||||
<%= error_tag f, :time_start %>
|
|
||||||
</div>
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_end) %>">
|
|
||||||
<%= error_tag f, :time_end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :location, class: "form-label mt-3" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :location) %>">
|
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :location,
|
|
||||||
value: input_value(f, :location),
|
|
||||||
class: input_class(f, :location, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :location)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :location %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :description, class: "form-label" %>
|
|
||||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :description) %>">
|
|
||||||
|
|
||||||
<%= textarea f, :description,
|
|
||||||
value: input_value(f, :description),
|
|
||||||
class: input_class(f, :description, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :description)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :description %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :time_zone, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :time_zone) %>">
|
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :time_zone,
|
|
||||||
value: input_value(f, :time_zone),
|
|
||||||
class: input_class(f, :time_zone, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
list: "tz_list"
|
|
||||||
%>
|
|
||||||
<datalist id="tz_list">
|
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
|
||||||
<option value="<%= tz_name %>"></option>
|
|
||||||
<% end %>
|
|
||||||
</datalist>
|
|
||||||
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
|
||||||
<%= error_tag f, :time_zone %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= submit "Save",
|
|
||||||
class: "btn btn-primary ",
|
|
||||||
disabled: !@changeset.valid?,
|
|
||||||
aria_disabled: !@changeset.valid? && "true" || false,
|
|
||||||
phx_disable_with: "Saving..."
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
|
@ -1,17 +1,29 @@
|
||||||
<%= if @live_action in [:new, :edit, :clone] do %>
|
<%= if @live_action in [:new, :edit, :clone] do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.FormComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftTemplateLive.FormComponent}
|
||||||
|
opts={[
|
||||||
id: @shift_template.id || :new,
|
id: @shift_template.id || :new,
|
||||||
title: @page_title,
|
title: @page_title,
|
||||||
action: @live_action,
|
action: @live_action,
|
||||||
shift_template: @shift_template,
|
shift_template: @shift_template,
|
||||||
current_user: @current_user %>
|
current_user: @current_user
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @delete_shift_template do %>
|
<%= if @delete_shift_template do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftTemplateLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: @delete_shift_template.id,
|
id: @delete_shift_template.id,
|
||||||
title: "Delete Shift Template",
|
title: "Delete Shift Template",
|
||||||
delete_shift_template: @delete_shift_template %>
|
delete_shift_template: @delete_shift_template
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,12 +32,10 @@
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center">
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-clock-history", [class: "icon baseline"] %>
|
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||||
My Shift Templates
|
|
||||||
</h2>
|
</h2>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %>
|
||||||
<%= icon_div @socket, "bi-plus-circle-dotted", [class: "icon baseline"] %>
|
<i class="bi bi-plus-circle-dotted me-1"></i> New Shift Template
|
||||||
New Shift Template
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -39,13 +49,13 @@
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<h5 class="card-header d-flex justify-content-between align-items-center">
|
<h5 class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span class="visually-hidden">Subject:</span>
|
<span class="visually-hidden">Subject:</span>
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline me-1"] %>
|
<i class="bi bi-tag me-1"></i>
|
||||||
<div class="w-100"><%= template.subject %></div>
|
<div class="w-100"><%= template.subject %></div>
|
||||||
<%= if template.id == @current_user.fave_shift_template_id do %>
|
<% fav_icon_data = if template.id == @current_user.fave_shift_template_id, do:
|
||||||
<%= icon_div @socket, "bi-star-fill", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Unset as favorite", phx_click: "unset-user-fave-shift-template", class: "cursor-pointer"] %>
|
{"bi-star-fill", "Unset as favorite", "unset-user-fave-shift-template"}, else:
|
||||||
<% else %>
|
{"bi-star", "Set as favorite", "set-user-fave-shift-template"}
|
||||||
<%= icon_div @socket, "bi-star", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Set as favorite", phx_click: "set-user-fave-shift-template", phx_value_id: template.id, class: "cursor-pointer"] %>
|
%>
|
||||||
<% end %>
|
<i class={"bi #{elem(fav_icon_data, 0)} text-primary align-self-start ms-2 cursor-pointer"} role="img" aria-hidden="false" aria-label={elem(fav_icon_data, 1)} phx-click={elem(fav_icon_data, 2)} phx-value-id={template.id}></i>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
@ -53,7 +63,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-hourglass text-muted"></i>
|
||||||
<span class="visually-hidden">Hours:</span>
|
<span class="visually-hidden">Hours:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -70,7 +80,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-geo text-muted"></i>
|
||||||
<span class="visually-hidden">Location:</span>
|
<span class="visually-hidden">Location:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -83,7 +93,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-justify-left text-muted"></i>
|
||||||
<span class="visually-hidden">Description:</span>
|
<span class="visually-hidden">Description:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -99,24 +109,19 @@
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :edit) do %>
|
<%= if Roles.can?(@current_user, template, :edit) do %>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :clone) do %>
|
<%= if Roles.can?(@current_user, template, :clone) do %>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :clone, template), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :clone, template), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-clipboard-plus", [class: "icon baseline"] %>
|
<i class="bi bi-clipboard-plus me-1"></i> Clone
|
||||||
Clone
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%#= button "" %>
|
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :delete) do %>
|
<%= if Roles.can?(@current_user, template, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= template.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={template.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Shift73kWeb.UserLive.Registration do
|
defmodule Shift73kWeb.UserLive.Registration do
|
||||||
use Shift73kWeb, :live_view
|
use Shift73kWeb, :live_view
|
||||||
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ defmodule Shift73kWeb.UserLive.Registration do
|
||||||
user_id: nil,
|
user_id: nil,
|
||||||
user_return_to: Map.get(session, "user_return_to", "/"),
|
user_return_to: Map.get(session, "user_return_to", "/"),
|
||||||
messages: [
|
messages: [
|
||||||
success: "Welcome! Your new account has been created, and you've been logged in.",
|
success: "Welcome! Your new account has been created, and you've been logged in."
|
||||||
info:
|
|
||||||
"Some features may be unavailable until you confirm your email address. Check your inbox for instructions."
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -35,19 +33,33 @@ defmodule Shift73kWeb.UserLive.Registration do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"user" => user_params}, socket) do
|
def handle_event("save", %{"user" => user_params}, socket) do
|
||||||
|
is_first_user = !Repo.exists?(User)
|
||||||
user_params
|
user_params
|
||||||
|> Map.put("role", Accounts.registration_role())
|
|
||||||
|> Accounts.register_user()
|
|> Accounts.register_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
{:ok, %Bamboo.Email{}} =
|
# If this is the first user, we just confirm them
|
||||||
|
if is_first_user do
|
||||||
|
user |> User.confirm_changeset() |> Repo.update()
|
||||||
|
else
|
||||||
|
# Otherwise, all new users require email confirmation so we wend instructions
|
||||||
|
{:ok, _, %Swoosh.Email{} = _captured_email} =
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
login_params =
|
||||||
|
if is_first_user do
|
||||||
|
socket.assigns.login_params
|
||||||
|
else
|
||||||
|
put_in(socket.assigns.login_params, [:messages, :info], "Some features may be unavailable until you confirm your email address. Check your inbox for instructions.")
|
||||||
|
end
|
||||||
|
|> put_in([:user_id], user.id)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
|
|> assign(login_params: login_params)
|
||||||
|> assign(trigger_submit: true)
|
|> assign(trigger_submit: true)
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2><%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %>
|
<h2>
|
||||||
Register</h2>
|
<i class="bi bi-journal-plus me-1"></i> Register
|
||||||
|
</h2>
|
||||||
<p class="lead">Create an account to manage your work shifts with us.</p>
|
<p class="lead">Create an account to manage your work shifts with us.</p>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %>
|
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="reg_form">
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
class: input_class(f, :email, "form-control"),
|
class: input_class(f, :email, "form-control"),
|
||||||
|
@ -23,8 +24,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -44,7 +45,7 @@
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
</.form>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
|
@ -1,9 +1,12 @@
|
||||||
defmodule Shift73kWeb.UserLive.ResetPassword do
|
defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||||
use Shift73kWeb, :live_view
|
use Shift73kWeb, :live_view
|
||||||
|
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
|
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, session, socket) do
|
def mount(_params, session, socket) do
|
||||||
user = Accounts.get_user!(session["user_id"])
|
user = Accounts.get_user!(session["user_id"])
|
||||||
|
@ -37,4 +40,6 @@ defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||||
|> assign(changeset: changeset)}
|
|> assign(changeset: changeset)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_registration, do: get_app_allow_reg()
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @socket, "bi-shield-lock", [class: "icon baseline"] %>
|
<i class="bi bi-shield-lock me-1"></i> Reset password
|
||||||
Reset password
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Hi <%= @user.email %> — tell us your new password, please.</p>
|
<p class="lead">Hi <%= @user.email %> — tell us your new password, please.</p>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "pw_reset_form"], fn f -> %>
|
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="pw_reset_form">
|
||||||
|
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -23,8 +22,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password_confirmation)}>
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
class: input_class(f, :password_confirmation, "form-control"),
|
class: input_class(f, :password_confirmation, "form-control"),
|
||||||
|
@ -43,10 +42,12 @@
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
</.form>
|
||||||
|
|
||||||
<p class="mt-3 is-pulled-right">
|
<p class="mt-3 is-pulled-right">
|
||||||
|
<%= if allow_registration() do %>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
16
lib/shift73k_web/live/user/settings.html.heex
Normal file
16
lib/shift73k_web/live/user/settings.html.heex
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-11">
|
||||||
|
|
||||||
|
<h2 class="mb-3">
|
||||||
|
<i class="bi bi-sliders me-1"></i> User Settings
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="row justify-content-center justify-content-md-start">
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.Email} id={"email-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.Password} id={"password-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.WeekStart} id={"week_start-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.CalendarUrl} id={"calendar_url-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,15 +0,0 @@
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-11">
|
|
||||||
|
|
||||||
<h2 class="mb-3"><%= icon_div @socket, "bi-sliders", [class: "icon baseline"] %>
|
|
||||||
User Settings</h2>
|
|
||||||
|
|
||||||
<div class="row justify-content-center justify-content-md-start">
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Email, id: "email-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Password, id: "password-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.WeekStart, id: "week_start-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.CalendarUrl, id: "calendar_url-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-6 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-6 mt-1">
|
||||||
|
|
||||||
<h3>iCal Subscribe URL</h3>
|
<h3>iCal Subscribe URL</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col mb-3">
|
<div class="col mb-3">
|
||||||
<label class="form-label">Use this URL to subscribe in calendar software</label>
|
<label class="form-label">Use this URL to subscribe in calendar software</label>
|
||||||
<input type="text" class="form-control" value="<%= Routes.user_shifts_ics_url(@socket, :index, @current_user.calendar_slug) %>" readonly onclick="this.focus();this.select()" />
|
<input type="text" class="form-control" value={Routes.user_shifts_ics_url(@socket, :index, @current_user.calendar_slug)} readonly onclick="this.focus();this.select()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Change email</h3>
|
<h3>Change email</h3>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
class: input_class(f, :email, "form-control"),
|
class: input_class(f, :email, "form-control"),
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
id: "user_email_current_password",
|
id: "user_email_current_password",
|
|
@ -1,12 +1,12 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Change password</h3>
|
<h3>Change password</h3>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||||
|
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -18,8 +18,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password_confirmation)}>
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
class: input_class(f, :password_confirmation, "form-control"),
|
class: input_class(f, :password_confirmation, "form-control"),
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
id: "user_password_current_password",
|
id: "user_password_current_password",
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Calendar view</h3>
|
<h3>Calendar view</h3>
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<%= label cvf, :week_start_at, "Week starts at", class: "form-label" %>
|
<%= label cvf, :week_start_at, "Week starts at", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-calendar2-range", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-calendar2-range icon is-left text-muted fs-5"></i>
|
||||||
<%= select cvf, :week_start_at, week_start_options(),
|
<%= select cvf, :week_start_at, week_start_options(),
|
||||||
value: @current_user.week_start_at,
|
value: @current_user.week_start_at,
|
||||||
class: "form-select"
|
class: "form-select"
|
|
@ -9,9 +9,13 @@ defmodule Shift73kWeb.UserManagement.DeleteComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
|
def handle_event("confirm", %{"id" => id, "email" => email} = params, socket) do
|
||||||
id
|
IO.inspect(params)
|
||||||
|> Accounts.get_user()
|
|
||||||
|
user = Accounts.get_user(id)
|
||||||
|
IO.inspect(user)
|
||||||
|
|
||||||
|
user
|
||||||
|> Accounts.delete_user()
|
|> Accounts.delete_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_user.email %>"?
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_user.id,
|
||||||
|
phx_value_email: @delete_user.email %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
Are you sure you want to delete "<%= @delete_user.email %>"?
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_value_id: @delete_user.id,
|
|
||||||
phx_value_email: @delete_user.email %>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -33,7 +33,7 @@ defmodule Shift73kWeb.UserManagement.FormComponent do
|
||||||
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
||||||
case Accounts.register_user(user_params) do
|
case Accounts.register_user(user_params) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
{:ok, %Bamboo.Email{}} =
|
{:ok, _, %Swoosh.Email{} = _captured_email} =
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<%= form_for @changeset, "#", [
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_change: "validate",
|
||||||
|
phx_submit: "save"
|
||||||
|
], fn f -> %>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<%= label f, :email, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
|
<%= email_input f, :email,
|
||||||
|
value: input_value(f, :email),
|
||||||
|
class: input_class(f, :email, "form-control"),
|
||||||
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
maxlength: User.max_email,
|
||||||
|
autofocus: true,
|
||||||
|
phx_debounce: "250",
|
||||||
|
aria_describedby: error_ids(f, :email)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :email %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= label f, :password, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
|
<%= password_input f, :password,
|
||||||
|
value: input_value(f, :password),
|
||||||
|
class: input_class(f, :password, "form-control"),
|
||||||
|
maxlength: User.max_password,
|
||||||
|
aria_describedby: error_ids(f, :password)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :password %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
||||||
|
<%= label f, :role, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3">
|
||||||
|
<i class="bi bi-shield icon is-left text-muted fs-5"></i>
|
||||||
|
<%= select f, :role, Enum.map(User.roles(), fn {k, _v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
||||||
|
<span class="valid-feedback text-primary" style="display: block;">
|
||||||
|
<%= role_description(input_value(f, :role)) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= submit "Save",
|
||||||
|
class: "btn btn-primary ",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,62 +0,0 @@
|
||||||
<%= form_for @changeset, "#", [
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_change: "validate",
|
|
||||||
phx_submit: "save"
|
|
||||||
], fn f -> %>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= email_input f, :email,
|
|
||||||
value: input_value(f, :email),
|
|
||||||
class: input_class(f, :email, "form-control"),
|
|
||||||
placeholder: "e.g., babka@73k.us",
|
|
||||||
maxlength: User.max_email,
|
|
||||||
autofocus: true,
|
|
||||||
phx_debounce: "250",
|
|
||||||
aria_describedby: error_ids(f, :email)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :email %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= password_input f, :password,
|
|
||||||
value: input_value(f, :password),
|
|
||||||
class: input_class(f, :password, "form-control"),
|
|
||||||
maxlength: User.max_password,
|
|
||||||
aria_describedby: error_ids(f, :password)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :password %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
|
||||||
<%= label f, :role, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3">
|
|
||||||
<%= icon_div @socket, "bi-shield", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= select f, :role, Enum.map(User.roles(), fn {k, _v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
|
||||||
<span class="valid-feedback text-primary" style="display: block;">
|
|
||||||
<%= role_description(input_value(f, :role)) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= submit "Save",
|
|
||||||
class: "btn btn-primary ",
|
|
||||||
disabled: !@changeset.valid?,
|
|
||||||
aria_disabled: !@changeset.valid? && "true" || false,
|
|
||||||
phx_disable_with: "Saving..."
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% end %>
|
|
|
@ -198,7 +198,7 @@ defmodule Shift73kWeb.UserManagementLive.Index do
|
||||||
|
|
||||||
def dt_out(ndt) do
|
def dt_out(ndt) do
|
||||||
ndt
|
ndt
|
||||||
|> DateTime.from_naive!(Shift73k.app_time_zone())
|
|> DateTime.from_naive!(Shift73k.get_app_time_zone())
|
||||||
|> Calendar.strftime("%Y %b %-d, %-I:%M %p")
|
|> Calendar.strftime("%Y %b %-d, %-I:%M %p")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
<%= if @live_action in [:new, :edit] do %>
|
<%= if @live_action in [:new, :edit] do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.UserManagement.FormComponent}
|
||||||
|
opts={[
|
||||||
id: @user.id || :new,
|
id: @user.id || :new,
|
||||||
title: @page_title,
|
title: @page_title,
|
||||||
action: @live_action,
|
action: @live_action,
|
||||||
user: @user,
|
user: @user,
|
||||||
current_user: @current_user %>
|
current_user: @current_user
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @delete_user do %>
|
<%= if @delete_user do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.UserManagement.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: @delete_user.id,
|
id: @delete_user.id,
|
||||||
title: "Delete User",
|
title: "Delete User",
|
||||||
delete_user: @delete_user
|
delete_user: @delete_user
|
||||||
%>
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<h2 class="mb-3">
|
<h2 class="mb-3">
|
||||||
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
|
<i class="bi bi-people me-1"></i> Listing Users
|
||||||
Listing Users
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<%# filtering and new item creation %>
|
<%# filtering and new item creation %>
|
||||||
|
@ -27,8 +37,7 @@
|
||||||
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
|
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
||||||
class: "btn btn-primary mb-3 mb-md-0" do %>
|
class: "btn btn-primary mb-3 mb-md-0" do %>
|
||||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
|
<i class="bi bi-person-plus me-1"></i> New User
|
||||||
New User
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,10 +45,10 @@
|
||||||
<%= form_for :sort, "#", [phx_change: "sort-by-change"], fn srt -> %>
|
<%= form_for :sort, "#", [phx_change: "sort-by-change"], fn srt -> %>
|
||||||
<%= label srt, :sort_by, class: "visually-hidden" %>
|
<%= label srt, :sort_by, class: "visually-hidden" %>
|
||||||
<div class="input-group inner-addon left-addon mb-3 mb-md-0">
|
<div class="input-group inner-addon left-addon mb-3 mb-md-0">
|
||||||
<%= icon_div @socket, "bi-arrow-down-up", [class: "icon is-left text-muted fs-5", style: "z-index:1001;"], [style: "padding: 1px;"] %>
|
<i class="bi bi-arrow-down-up icon is-left text-muted fs-5" style="z-index:1001;"></i>
|
||||||
<%= Phoenix.HTML.Form.select srt, :sort_by, ["Email": "email", "Role": "role", "Created at": "inserted_at"], value: @query.sort_by, class: "form-select" %>
|
<%= Phoenix.HTML.Form.select srt, :sort_by, ["Email": "email", "Role": "role", "Created at": "inserted_at"], value: @query.sort_by, class: "form-select rounded-start" %>
|
||||||
<button class="btn btn-primary" type="button" aria-label="Change sort order" phx-click="sort-order-change">
|
<button class="btn btn-primary" type="button" aria-label="Change sort order" phx-click="sort-order-change">
|
||||||
<%= icon_div @socket, (@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"), [class: "icon baseline"] %>
|
<i class={if @query.sort_order == "desc", do: "bi bi-sort-up-alt", else: "bi bi-sort-down-alt"}></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -49,9 +58,9 @@
|
||||||
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
||||||
<%= label flt, :filter, class: "visually-hidden" %>
|
<%= label flt, :filter, class: "visually-hidden" %>
|
||||||
<div class="inner-addon left-addon right-addon">
|
<div class="inner-addon left-addon right-addon">
|
||||||
<%= icon_div @socket, "bi-funnel", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-funnel icon is-left text-muted fs-5"></i>
|
||||||
<%= if @query.filter != "" do %>
|
<%= if @query.filter != "" do %>
|
||||||
<%= icon_div @socket, "bi-x-circle-fill", [class: "icon is-right text-primary fs-5"], [role: "img", aria_hidden: false, aria_label: "Clear filter", class: "cursor-pointer pe-auto", phx_click: "filter-clear"] %>
|
<i class="bi bi-x-circle-fill icon is-right text-primary fs-5 cursor-pointer pe-auto" role="img" aria-hidden="false" aria-label="Clear filter" phx-click="filter-clear"></i>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= text_input flt, :filter,
|
<%= text_input flt, :filter,
|
||||||
name: "filter",
|
name: "filter",
|
||||||
|
@ -92,21 +101,19 @@
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="d-inline d-sm-block col-auto">
|
<dd class="d-inline d-sm-block col-auto">
|
||||||
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
|
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
|
||||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :edit) do %>
|
<%= if Roles.can?(@current_user, user, :edit) do %>
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :delete) do %>
|
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={user.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -126,28 +133,25 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||||
Email
|
Email
|
||||||
<%= if @query.sort_by == "email", do: icon_div @socket,
|
<%= if @query.sort_by == "email" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||||
Role
|
Role
|
||||||
<%= if @query.sort_by == "role", do: icon_div @socket,
|
<%= if @query.sort_by == "role" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||||
Created at
|
Created at
|
||||||
<%= if @query.sort_by == "inserted_at", do: icon_div @socket,
|
<%= if @query.sort_by == "inserted_at" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col">Confirmed?</th>
|
<th scope="col">Confirmed?</th>
|
||||||
|
@ -168,27 +172,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= for user <- @page.entries do %>
|
<%= for user <- @page.entries do %>
|
||||||
<tr id="user-<%= user.id %>">
|
<tr id={"user-#{user.id}"}>
|
||||||
<td class="align-middle"><%= user.email %></td>
|
<td class="align-middle"><%= user.email %></td>
|
||||||
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
||||||
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
|
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
|
||||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle text-end text-nowrap">
|
<td class="align-middle text-end text-nowrap">
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :edit) do %>
|
<%= if Roles.can?(@current_user, user, :edit) do %>
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :delete) do %>
|
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={user.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -227,47 +229,47 @@
|
||||||
<ul class="pagination mb-0">
|
<ul class="pagination mb-0">
|
||||||
|
|
||||||
<%# previous page button %>
|
<%# previous page button %>
|
||||||
<% icon = icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
|
||||||
<%= if @page.page_number == 1 do %>
|
<%= if @page.page_number == 1 do %>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-left"></i></span>
|
||||||
<span class="visually-hidden">Previous</span>
|
<span class="visually-hidden">Previous</span>
|
||||||
|
</li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number="<%= @page.page_number - 1 %>" phx-click="page-change"><%= icon %></a>
|
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number={@page.page_number - 1} phx-click="page-change"><i class="bi bi-chevron-left"></i></a>
|
||||||
<% end %>
|
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%# page buttons %>
|
<%# page buttons %>
|
||||||
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
||||||
<%= cond do %>
|
<%= cond do %>
|
||||||
<%= page_num < 1 -> %>
|
<% page_num < 1 -> %>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link" aria-hidden="true">…</span>
|
<span class="page-link" aria-hidden="true">…</span>
|
||||||
<span class="visually-hidden" role="img" aria-label="ellipses">…</span>
|
<span class="visually-hidden" role="img" aria-label="ellipses">…</span>
|
||||||
</li>
|
</li>
|
||||||
<% page_num == @page.page_number -> %>
|
<% page_num == @page.page_number -> %>
|
||||||
<li class="page-item active" aria-current="page">
|
<li class="page-item active" aria-current="page">
|
||||||
<span class="page-link"><%= page_num %></a>
|
<span class="page-link"><%= page_num %></span>
|
||||||
</li>
|
</li>
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" phx-value-page_number="<%= page_num %>" phx-click="page-change"><%= page_num %></a>
|
<a class="page-link" href="#" phx-value-page_number={page_num} phx-click="page-change"><%= page_num %></a>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%# next page button %>
|
<%# next page button %>
|
||||||
<% icon = icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %>
|
|
||||||
<%= if @page.page_number == @page.total_pages do %>
|
<%= if @page.page_number == @page.total_pages do %>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-right"></i></span>
|
||||||
<span class="visually-hidden">Next</span>
|
<span class="visually-hidden">Next</span>
|
||||||
|
</li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" aria-label="Next" phx-value-page_number="<%= @page.page_number + 1 %>" phx-click="page-change"><%= icon %></a>
|
<a class="page-link" href="#" aria-label="Next" phx-value-page_number={@page.page_number + 1} phx-click="page-change"><i class="bi bi-chevron-right"></i></a>
|
||||||
<% end %>
|
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
34
lib/shift73k_web/plugs/ensure_allow_registration_plug.ex
Normal file
34
lib/shift73k_web/plugs/ensure_allow_registration_plug.ex
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule Shift73kWeb.EnsureAllowRegistrationPlug do
|
||||||
|
@moduledoc """
|
||||||
|
This plug ensures that there is at least one known User.
|
||||||
|
"""
|
||||||
|
import Plug.Conn
|
||||||
|
import Phoenix.Controller
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
|
|
||||||
|
alias Shift73k.Repo
|
||||||
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec init(any()) :: any()
|
||||||
|
def init(config), do: config
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
|
||||||
|
def call(conn, _opts) do
|
||||||
|
# If there aren't even any users, or registration is allowed
|
||||||
|
if !Repo.exists?(User) || get_app_allow_reg() do
|
||||||
|
# We will allow registration
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
# Otherwise,
|
||||||
|
# if app is configured to not allow registration,
|
||||||
|
# and there is a user,
|
||||||
|
# then we redirect to root URL
|
||||||
|
conn
|
||||||
|
|> redirect(to: "/")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -27,8 +27,7 @@ defmodule Shift73kWeb.EnsureRolePlug do
|
||||||
def call(conn, roles) do
|
def call(conn, roles) do
|
||||||
user_token = get_session(conn, :user_token)
|
user_token = get_session(conn, :user_token)
|
||||||
|
|
||||||
(user_token &&
|
(user_token && Accounts.get_user_by_session_token(user_token))
|
||||||
Accounts.get_user_by_session_token(user_token))
|
|
||||||
|> has_role?(roles)
|
|> has_role?(roles)
|
||||||
|> maybe_halt(conn)
|
|> maybe_halt(conn)
|
||||||
end
|
end
|
||||||
|
|
30
lib/shift73k_web/plugs/ensure_user_exist_plug.ex
Normal file
30
lib/shift73k_web/plugs/ensure_user_exist_plug.ex
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
defmodule Shift73kWeb.EnsureUserExistPlug do
|
||||||
|
@moduledoc """
|
||||||
|
This plug ensures that there is at least one known User.
|
||||||
|
"""
|
||||||
|
import Plug.Conn
|
||||||
|
import Phoenix.Controller
|
||||||
|
|
||||||
|
alias Shift73k.Repo
|
||||||
|
alias Shift73k.Accounts.User
|
||||||
|
alias Shift73kWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec init(any()) :: any()
|
||||||
|
def init(config), do: config
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
|
||||||
|
def call(conn, _opts) do
|
||||||
|
# If there aren't even any users,
|
||||||
|
if !Repo.exists?(User) do
|
||||||
|
# We're just going to redirect to registration
|
||||||
|
conn
|
||||||
|
|> redirect(to: Routes.user_registration_path(conn, :new))
|
||||||
|
|> halt()
|
||||||
|
else
|
||||||
|
# Otherwise we proceed as normal
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,98 +2,100 @@ defmodule Shift73kWeb.Router do
|
||||||
use Shift73kWeb, :router
|
use Shift73kWeb, :router
|
||||||
import Shift73kWeb.UserAuth
|
import Shift73kWeb.UserAuth
|
||||||
alias Shift73kWeb.EnsureRolePlug
|
alias Shift73kWeb.EnsureRolePlug
|
||||||
|
alias Shift73kWeb.EnsureUserExistPlug
|
||||||
|
alias Shift73kWeb.EnsureAllowRegistrationPlug
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug(:accepts, ["html"])
|
plug :accepts, ["html"]
|
||||||
plug(:fetch_session)
|
plug :fetch_session
|
||||||
plug(:fetch_live_flash)
|
plug :fetch_live_flash
|
||||||
plug(:put_root_layout, {Shift73kWeb.LayoutView, :root})
|
plug :put_root_layout, {Shift73kWeb.LayoutView, :root}
|
||||||
plug(:protect_from_forgery)
|
plug :protect_from_forgery
|
||||||
plug(:put_secure_browser_headers)
|
plug :put_secure_browser_headers
|
||||||
plug(:fetch_current_user)
|
plug :fetch_current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :ensure_role_user do
|
||||||
plug(:accepts, ["json"])
|
plug EnsureRolePlug, [:admin, :manager, :user]
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :user do
|
pipeline :ensure_user_exist do
|
||||||
plug(EnsureRolePlug, [:admin, :manager, :user])
|
plug EnsureUserExistPlug
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :manager do
|
pipeline :ensure_allow_registration do
|
||||||
plug(EnsureRolePlug, [:admin, :manager])
|
plug EnsureAllowRegistrationPlug
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :admin do
|
pipeline :ensure_role_manager do
|
||||||
plug(EnsureRolePlug, :admin)
|
plug EnsureRolePlug, [:admin, :manager]
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
pipeline :ensure_role_admin do
|
||||||
pipe_through([:browser])
|
plug EnsureRolePlug, :admin
|
||||||
|
|
||||||
get("/", Redirector, to: "/assign")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Enables the Swoosh mailbox preview in development.
|
||||||
# scope "/api", Shift73kWeb do
|
|
||||||
# pipe_through :api
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Enables LiveDashboard only for development
|
|
||||||
#
|
#
|
||||||
# If you want to use the LiveDashboard in production, you should put
|
# Note that preview only shows emails that were sent by the same
|
||||||
# it behind authentication and allow only admins to access it.
|
# node running the Phoenix server.
|
||||||
# If your application does not have an admins-only section yet,
|
if Mix.env() == :dev do
|
||||||
# you can use Plug.BasicAuth to set up some basic authentication
|
scope "/dev" do
|
||||||
# as long as you are also using SSL (which you should anyway).
|
pipe_through :browser
|
||||||
if Mix.env() in [:dev, :test] do
|
|
||||||
import Phoenix.LiveDashboard.Router
|
|
||||||
|
|
||||||
scope "/" do
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||||
pipe_through(:browser)
|
|
||||||
live_dashboard("/dashboard", metrics: Shift73kWeb.Telemetry)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :redirect_if_user_is_authenticated])
|
pipe_through([:browser, :ensure_user_exist])
|
||||||
|
|
||||||
|
get "/", Redirector, to: "/assign"
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", Shift73kWeb do
|
||||||
|
pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_allow_registration]
|
||||||
|
|
||||||
|
get "/users/register", UserRegistrationController, :new
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", Shift73kWeb do
|
||||||
|
pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_user_exist]
|
||||||
|
|
||||||
# session routes, irrelevant if user is authenticated
|
# session routes, irrelevant if user is authenticated
|
||||||
get("/users/register", UserRegistrationController, :new)
|
get "/users/log_in", UserSessionController, :new
|
||||||
get("/users/log_in", UserSessionController, :new)
|
post "/users/log_in", UserSessionController, :create
|
||||||
post("/users/log_in", UserSessionController, :create)
|
get "/users/reset_password", UserResetPasswordController, :new
|
||||||
get("/users/reset_password", UserResetPasswordController, :new)
|
post "/users/reset_password", UserResetPasswordController, :create
|
||||||
post("/users/reset_password", UserResetPasswordController, :create)
|
get "/users/reset_password/:token", UserResetPasswordController, :edit
|
||||||
get("/users/reset_password/:token", UserResetPasswordController, :edit)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user])
|
pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
# user settings (change email, password, calendar week start, etc)
|
# user settings (change email, password, calendar week start, etc)
|
||||||
live("/users/settings", UserLive.Settings, :edit)
|
live "/users/settings", UserLive.Settings, :edit
|
||||||
|
|
||||||
# confirm email by token
|
# confirm email by token
|
||||||
get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
|
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser])
|
pipe_through [:browser, :ensure_user_exist]
|
||||||
|
|
||||||
# session paths
|
# session paths
|
||||||
delete("/users/log_out", UserSessionController, :delete)
|
delete "/users/log_out", UserSessionController, :delete
|
||||||
get("/users/force_logout", UserSessionController, :force_logout)
|
get "/users/force_logout", UserSessionController, :force_logout
|
||||||
get("/users/confirm", UserConfirmationController, :new)
|
get "/users/confirm", UserConfirmationController, :new
|
||||||
post("/users/confirm", UserConfirmationController, :create)
|
post "/users/confirm", UserConfirmationController, :create
|
||||||
get("/users/confirm/:token", UserConfirmationController, :confirm)
|
get "/users/confirm/:token", UserConfirmationController, :confirm
|
||||||
|
|
||||||
# ics/ical route for user's shifts
|
# ics/ical route for user's shifts
|
||||||
get("/ics/:slug", UserShiftsIcsController, :index)
|
get "/ics/:slug", UserShiftsIcsController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user, :user])
|
pipe_through [:browser, :require_authenticated_user, :ensure_role_user]
|
||||||
|
|
||||||
live "/templates", ShiftTemplateLive.Index, :index
|
live "/templates", ShiftTemplateLive.Index, :index
|
||||||
live "/templates/new", ShiftTemplateLive.Index, :new
|
live "/templates/new", ShiftTemplateLive.Index, :new
|
||||||
|
@ -111,16 +113,16 @@ defmodule Shift73kWeb.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
# scope "/", Shift73kWeb do
|
# scope "/", Shift73kWeb do
|
||||||
# pipe_through([:browser, :require_authenticated_user, :admin])
|
# pipe_through([:browser, :require_authenticated_user, :ensure_role_admin])
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# Users Management
|
# Users Management
|
||||||
scope "/users", Shift73kWeb do
|
scope "/users", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user, :manager, :require_email_confirmed])
|
pipe_through [:browser, :require_authenticated_user, :ensure_role_manager, :require_email_confirmed]
|
||||||
|
|
||||||
live("/", UserManagementLive.Index, :index)
|
live "/", UserManagementLive.Index, :index
|
||||||
live("/new", UserManagementLive.Index, :new)
|
live "/new", UserManagementLive.Index, :new
|
||||||
live("/edit/:id", UserManagementLive.Index, :edit)
|
live "/edit/:id", UserManagementLive.Index, :edit
|
||||||
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-4">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1 class="fs-4 my-0 py-0 lh-base">
|
|
||||||
<%= link to: "/", class: "navbar-brand fs-4" do %>
|
|
||||||
<%= icon_div @conn, "bi-calendar2-week", [class: "icon baseline me-1"] %>
|
|
||||||
<span class="fw-light">Shift73k</span>
|
|
||||||
<% end %>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<%= if @current_user do %>
|
|
||||||
<button class="hamburger hamburger--squeeze collapsed navbar-toggler" id="navbarSupportedContentToggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="hamburger-box d-flex">
|
|
||||||
<span class="hamburger-inner"></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<% else %>
|
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-block d-lg-none") do %>
|
|
||||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
|
||||||
Log in
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
|
|
||||||
<%# nav LEFT items %>
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
|
|
||||||
<%#= if @current_user do %>
|
|
||||||
<%# <li class="nav-item"> %>
|
|
||||||
<%#= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "nav-link") do %>
|
|
||||||
<%#= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
|
|
||||||
<%# Templates %>
|
|
||||||
<%# end %>
|
|
||||||
<%# </li> %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%# normal navbar link example %>
|
|
||||||
<%# <li class="nav-item"> %>
|
|
||||||
<%#= link "Properties", nav_link_opts(@conn, to: Routes.property_index_path(@conn, :index), class: "nav-link") %>
|
|
||||||
<%# </li> %>
|
|
||||||
|
|
||||||
<%# ACTIVE page link example %>
|
|
||||||
<%# <li class="nav-item">
|
|
||||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
<%# DISABLED page link example %>
|
|
||||||
<%# <li class="nav-item">
|
|
||||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
<%# normal dropdown menu example %>
|
|
||||||
<%# <li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownExample" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdownExample">
|
|
||||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
|
||||||
</ul>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<%# nav RIGHT items %>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
|
|
||||||
<%= if @current_user do %>
|
|
||||||
|
|
||||||
<%= render "navbar/_shifts_menu.html", assigns %>
|
|
||||||
|
|
||||||
<%= render "navbar/_user_menu.html", assigns %>
|
|
||||||
|
|
||||||
<% else %>
|
|
||||||
|
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-none d-lg-block") do %>
|
|
||||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
|
||||||
Log in
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
70
lib/shift73k_web/templates/layout/_navbar.html.heex
Normal file
70
lib/shift73k_web/templates/layout/_navbar.html.heex
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-4">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1 class="fs-4 my-0 py-0 lh-base">
|
||||||
|
<%= link to: "/", class: "navbar-brand fs-4" do %>
|
||||||
|
<i class="bi bi-calendar2-week me-1"></i>
|
||||||
|
<span class="fw-light">Shift73k</span>
|
||||||
|
<% end %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<%# If there's a current user,
|
||||||
|
OR if there are users & we allow registration,
|
||||||
|
THEN we will show a full menu configuration %>
|
||||||
|
|
||||||
|
<%= if @current_user || (Repo.exists?(User) && allow_registration()) do %>
|
||||||
|
|
||||||
|
<button class="hamburger hamburger--squeeze collapsed navbar-toggler" id="navbarSupportedContentToggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="hamburger-box d-flex">
|
||||||
|
<span class="hamburger-inner"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
|
||||||
|
<%# nav LEFT items %>
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<%# nav RIGHT items %>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
|
||||||
|
<%= if @current_user do %>
|
||||||
|
|
||||||
|
<%= render "navbar/_shifts_menu.html", assigns %>
|
||||||
|
|
||||||
|
<%= render "navbar/_user_menu.html", assigns %>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
|
||||||
|
<%= render "navbar/_nouser_menu.html", assigns %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# If there's no current user,
|
||||||
|
AND:
|
||||||
|
There are no users -- [REGISTER]
|
||||||
|
OR no registration allowed -- [LOG IN] %>
|
||||||
|
<% else %>
|
||||||
|
|
||||||
|
<%= if !Repo.exists?(User) || allow_registration() do %>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "btn btn-outline-light") do %>
|
||||||
|
<i class="bi bi-journal-plus"></i> Register
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %>
|
||||||
|
<i class="bi bi-door-open"></i> Log in
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
10
lib/shift73k_web/templates/layout/_preamble.html.heex
Normal file
10
lib/shift73k_web/templates/layout/_preamble.html.heex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<%= if dev_env?() do %>
|
||||||
|
<script type="module" src="http://localhost:3000/@vite/client"></script>
|
||||||
|
<script type="module" src="http://localhost:3000/js/app.js"></script>
|
||||||
|
<% else %>
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-300-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-400-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-700-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
|
||||||
|
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
|
||||||
|
<% end %>
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
||||||
<%= for {kind, class} <- alert_kinds() do %>
|
<%= for {kind, class} <- alert_kinds() do %>
|
||||||
<%= if flash_content = get_flash(@conn, kind) do %>
|
<%= if flash_content = get_flash(@conn, kind) do %>
|
||||||
<div class="alert <%= class %> alert-dismissible fade show" role="alert">
|
<div class={"alert #{class} alert-dismissible fade show"} role="alert">
|
||||||
<%= flash_content %>
|
<%= flash_content %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
||||||
<%= for {kind, class} <- alert_kinds() do %>
|
<%= for {kind, class} <- alert_kinds() do %>
|
||||||
<%= if flash_content = live_flash(@flash, kind) do %>
|
<%= if flash_content = live_flash(@flash, kind) do %>
|
||||||
<div class="alert <%= class %> alert-dismissible fade show" role="alert" id="lv-alert-<%= kind %>" phx-hook="AlertRemover" data-key="<%= kind %>">
|
<div class={"alert #{class} alert-dismissible fade show"} role="alert" id={"lv-alert-#{kind}"} phx-hook="AlertRemover" data-key={"#{kind}"}>
|
||||||
<%= flash_content %>
|
<%= flash_content %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownNoUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-person-circle me-1"></i> Hello?
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownNoUserMenu">
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
|
<i class="bi bi-journal-plus me-1"></i> Register
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
|
<i class="bi bi-door-open me-1"></i> Log in
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</li>
|
|
@ -1,28 +1,24 @@
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownShiftsMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline me-1"] %>
|
<i class="bi bi-calendar2 me-1"></i> Shifts
|
||||||
Shifts
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownShiftsMenu">
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-calendar2-plus", [class: "icon baseline me-1"] %>
|
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||||
Schedule Shifts
|
|
||||||
<% end %>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_index_path(@conn, :index), class: "dropdown-item") do %>
|
|
||||||
<%= icon_div @conn, "bi-card-list", [class: "icon baseline me-1"] %>
|
|
||||||
My Scheduled Shifts
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
|
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||||
My Shift Templates
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.shift_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
|
<i class="bi bi-card-list me-1"></i> My Scheduled Shifts
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -30,14 +26,12 @@
|
||||||
<%# user_shifts_csv_path %>
|
<%# user_shifts_csv_path %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline me-1"] %>
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||||
CSV Export
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_import_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_import_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-box-arrow-in-left", [class: "icon baseline me-1"] %>
|
<i class="bi bi-box-arrow-in-left me-1"></i> iCal Import
|
||||||
iCal Import
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<%= icon_div @conn, "bi-person-circle", [class: "icon baseline me-1"] %>
|
<i class="bi bi-person-circle me-1"></i> Hello!
|
||||||
<%= @current_user && "Hello!" || "Hello?" %>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
||||||
|
@ -12,22 +11,19 @@
|
||||||
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-people", [class: "icon baseline me-1"] %>
|
<i class="bi bi-people me-1"></i> Users
|
||||||
Users
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-sliders", [class: "icon baseline me-1"] %>
|
<i class="bi bi-sliders me-1"></i> Settings
|
||||||
Settings
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-door-closed", [class: "icon baseline me-1"] %>
|
<i class="bi bi-door-closed me-1"></i> Log out
|
||||||
Log out
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
30
lib/shift73k_web/templates/layout/root.html.heex
Normal file
30
lib/shift73k_web/templates/layout/root.html.heex
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<%= csrf_meta_tag() %>
|
||||||
|
<Phoenix.Component.live_title suffix=" · Shift73k">
|
||||||
|
<%= assigns[:page_title] || "Hi!" %>
|
||||||
|
</Phoenix.Component.live_title>
|
||||||
|
<%= render "_preamble.html", assigns %>
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href={Routes.static_path(@conn, "/apple-touch-icon.png")}>
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href={Routes.static_path(@conn, "/favicon-32x32.png")}>
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href={Routes.static_path(@conn, "/favicon-16x16.png")}>
|
||||||
|
<link rel="manifest" href={Routes.static_path(@conn, "/site.webmanifest")}>
|
||||||
|
<link rel="mask-icon" href={Routes.static_path(@conn, "/safari-pinned-tab.svg")} color="#78868a">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Shift73k">
|
||||||
|
<meta name="application-name" content="Shift73k">
|
||||||
|
<meta name="msapplication-TileColor" content="#ee6c4d">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<%= render "_navbar.html", assigns %>
|
||||||
|
|
||||||
|
<%= @inner_content %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,32 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<%= csrf_meta_tag() %>
|
|
||||||
<%= live_title_tag assigns[:page_title] || "", suffix: assigns[:page_title] && " · Shift73k" || "Shift73k" %>
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-300-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-400-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-700-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= Routes.static_path(@conn, "/apple-touch-icon.png") %>">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= Routes.static_path(@conn, "/favicon-32x32.png") %>">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= Routes.static_path(@conn, "/favicon-16x16.png") %>">
|
|
||||||
<link rel="manifest" href="<%= Routes.static_path(@conn, "/site.webmanifest") %>">
|
|
||||||
<link rel="mask-icon" href="<%= Routes.static_path(@conn, "/safari-pinned-tab.svg") %>" color="#78868a">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Shift73k">
|
|
||||||
<meta name="application-name" content="Shift73k">
|
|
||||||
<meta name="msapplication-TileColor" content="#ee6c4d">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
<link rel="icon" href="favicon.ico">
|
|
||||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
|
||||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<%= render "_navbar.html", assigns %>
|
|
||||||
|
|
||||||
<%= @inner_content %>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-arrow-repeat", [class: "icon baseline"] %>
|
<i class="bi bi-arrow-repeat me-1"></i> Resend confirmation instructions
|
||||||
Resend confirmation instructions
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">We'll send you another email with instructions to confirm your email address.</p>
|
<p class="lead">We'll send you another email with instructions to confirm your email address.</p>
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: @current_user && @current_user.email || "",
|
value: @current_user && @current_user.email || "",
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
@ -30,6 +29,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
<%= if allow_registration() do %>
|
||||||
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "mdi-head-question-outline", [class: "icon baseline"] %>
|
<i class="bi bi-patch-question me-1"></i> Forgot your password?
|
||||||
Forgot your password?
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">We'll send you an email with instructions to reset your password.</p>
|
<p class="lead">We'll send you an email with instructions to reset your password.</p>
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
|
@ -28,7 +27,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
<%= if allow_registration() do %>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2><%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
<h2>
|
||||||
Log in</h2>
|
<i class="bi bi-door-open me-1"></i> Log in
|
||||||
|
</h2>
|
||||||
<p class="lead">Who goes there?</p>
|
<p class="lead">Who goes there?</p>
|
||||||
|
|
||||||
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "needs-validation", novalidate: true], fn f -> %>
|
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "needs-validation", novalidate: true], fn f -> %>
|
||||||
<%= if @error_message do %>
|
<%= if @error_message do %>
|
||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
<div class="alert alert-danger alert-dismissible fade show mt-4" role="alert">
|
||||||
<%= @error_message %>
|
<%= @error_message %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,11 +16,12 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
maxlength: User.max_email,
|
maxlength: User.max_email,
|
||||||
|
autofocus: true,
|
||||||
required: true
|
required: true
|
||||||
%>
|
%>
|
||||||
<span class="invalid-feedback">must be a valid email address</span>
|
<span class="invalid-feedback">must be a valid email address</span>
|
||||||
|
@ -27,7 +29,7 @@
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
required: true
|
required: true
|
||||||
|
@ -47,7 +49,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
<%= if allow_registration() do %>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline"] %>
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||||
CSV Export
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.</p>
|
<p class="lead">Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.</p>
|
||||||
|
|
||||||
|
@ -27,6 +26,7 @@
|
||||||
value: Date.beginning_of_month(today),
|
value: Date.beginning_of_month(today),
|
||||||
min: min_date,
|
min: min_date,
|
||||||
max: max_date,
|
max: max_date,
|
||||||
|
required: true,
|
||||||
class: "form-control"
|
class: "form-control"
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
value: Date.end_of_month(today),
|
value: Date.end_of_month(today),
|
||||||
min: min_date,
|
min: min_date,
|
||||||
max: max_date,
|
max: max_date,
|
||||||
|
required: true,
|
||||||
class: "form-control"
|
class: "form-control"
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline"] %>
|
<i class="bi bi-calendar2 me-1"></i> User Shifts ICS
|
||||||
User Shifts ICS
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Shifts for user: <%= @user.email %></p>
|
<p class="lead">Shifts for user: <%= @user.email %></p>
|
||||||
<p>Calendar slug: <%= @slug %></p>
|
<p>Calendar slug: <%= @slug %></p>
|
|
@ -1,38 +0,0 @@
|
||||||
defmodule Shift73kWeb.IconHelpers do
|
|
||||||
@moduledoc """
|
|
||||||
Generate SVG sprite use tags for SVG icons
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Phoenix.HTML
|
|
||||||
alias Shift73kWeb.Router.Helpers, as: Routes
|
|
||||||
|
|
||||||
def icon_div(conn, name, div_opts \\ [], svg_opts \\ []) do
|
|
||||||
content_tag(:div, tag_opts(name, div_opts)) do
|
|
||||||
icon_svg(conn, name, svg_opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def icon_svg(conn, name, opts \\ []) do
|
|
||||||
opts = aria_hidden?(opts)
|
|
||||||
|
|
||||||
content_tag(:svg, tag_opts(name, opts)) do
|
|
||||||
~E"""
|
|
||||||
<%= if title = Keyword.get(opts, :aria_label), do: content_tag(:title, title) %>
|
|
||||||
<%= tag(:use, "xlink:href": Routes.static_path(conn, "/images/icons.svg##{name}")) %>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp tag_opts(name, opts) do
|
|
||||||
Keyword.update(opts, :class, name, fn c -> "#{c} #{name}" end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp aria_hidden?(opts) do
|
|
||||||
case Keyword.get(opts, :aria_hidden) do
|
|
||||||
"false" -> Keyword.drop(opts, [:aria_hidden])
|
|
||||||
false -> Keyword.drop(opts, [:aria_hidden])
|
|
||||||
"true" -> opts
|
|
||||||
_ -> Keyword.put(opts, :aria_hidden, "true")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,9 +1,20 @@
|
||||||
defmodule Shift73kWeb.LayoutView do
|
defmodule Shift73kWeb.LayoutView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
alias Shift73kWeb.Roles
|
alias Shift73kWeb.Roles
|
||||||
|
|
||||||
|
|
||||||
|
# With a Vite.js-based workflow, we will import different asset files in development
|
||||||
|
# and in production builds. Therefore, we will need a way to conditionally render
|
||||||
|
# <script> tags based on Mix environment. However, since Mix is not available in
|
||||||
|
# releases, we need to cache the Mix environment at compile time. To this end:
|
||||||
|
@env Mix.env() # remember value at compile time
|
||||||
|
def dev_env?, do: @env == :dev
|
||||||
|
|
||||||
|
def allow_registration, do: get_app_allow_reg()
|
||||||
|
|
||||||
def nav_link_opts(conn, opts) do
|
def nav_link_opts(conn, opts) do
|
||||||
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
||||||
false -> opts
|
false -> opts
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule Shift73kWeb.UserConfirmationView do
|
defmodule Shift73kWeb.UserConfirmationView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
def allow_registration, do: get_app_allow_reg()
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule Shift73kWeb.UserResetPasswordView do
|
defmodule Shift73kWeb.UserResetPasswordView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
def allow_registration, do: get_app_allow_reg()
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule Shift73kWeb.UserSessionView do
|
defmodule Shift73kWeb.UserSessionView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
|
import Shift73k, only: [get_app_allow_reg: 0]
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
def allow_registration, do: get_app_allow_reg()
|
||||||
end
|
end
|
||||||
|
|
42
mix.exs
42
mix.exs
|
@ -4,10 +4,10 @@ defmodule Shift73k.MixProject do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :shift73k,
|
app: :shift73k,
|
||||||
version: "0.1.0",
|
version: "0.2.1",
|
||||||
elixir: "~> 1.7",
|
elixir: "~> 1.12",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
compilers: Mix.compilers(),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps()
|
deps: deps()
|
||||||
|
@ -33,33 +33,30 @@ defmodule Shift73k.MixProject do
|
||||||
# Type `mix help deps` for examples and options.
|
# Type `mix help deps` for examples and options.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:bcrypt_elixir, "~> 2.0"},
|
{:bcrypt_elixir, "~> 3.0"},
|
||||||
{:phoenix, "~> 1.5.8"},
|
{:phoenix, "~> 1.6.11"},
|
||||||
{:phoenix_ecto, "~> 4.1"},
|
{:phoenix_ecto, "~> 4.4"},
|
||||||
{:ecto_sql, "~> 3.4"},
|
{:ecto_sql, "~> 3.6"},
|
||||||
{:postgrex, ">= 0.0.0"},
|
{:postgrex, ">= 0.0.0"},
|
||||||
{:phoenix_live_view, "~> 0.15.0"},
|
{:phoenix_html, "~> 3.0"},
|
||||||
{:floki, ">= 0.27.0", only: :test},
|
|
||||||
{:phoenix_html, "~> 2.11"},
|
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_dashboard, "~> 0.4"},
|
{:phoenix_live_view, "~> 0.18"},
|
||||||
{:telemetry_metrics, "~> 0.4"},
|
{:floki, ">= 0.30.0", only: :test},
|
||||||
{:telemetry_poller, "~> 0.4"},
|
{:swoosh, "~> 1.7"},
|
||||||
{:gettext, "~> 0.11"},
|
{:gen_smtp, "~> 1.2"},
|
||||||
{:jason, "~> 1.0"},
|
{:telemetry_metrics, "~> 0.6"},
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:telemetry_poller, "~> 1.0"},
|
||||||
{:bamboo, "~> 2.0"},
|
{:jason, "~> 1.2"},
|
||||||
{:bamboo_smtp, "~> 4.0"},
|
{:plug_cowboy, "~> 2.5"},
|
||||||
{:scrivener_ecto, "~> 2.0"},
|
{:scrivener_ecto, "~> 2.0"},
|
||||||
{:tzdata, "~> 1.1"},
|
{:tzdata, "~> 1.1"},
|
||||||
{:nimble_csv, "~> 1.0"},
|
{:nimble_csv, "~> 1.0"},
|
||||||
{:icalendar, "~> 1.1"},
|
{:icalendar, "~> 1.1"},
|
||||||
{:httpoison, "~> 1.7"},
|
{:httpoison, "~> 2.0"},
|
||||||
|
|
||||||
# Additional packages
|
# Additional packages
|
||||||
|
|
||||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.5", only: [:dev, :test], runtime: false}
|
||||||
{:sobelow, "~> 0.8", only: :dev}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,7 +71,8 @@ defmodule Shift73k.MixProject do
|
||||||
setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
|
setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
|
||||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
|
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||||
|
"assets.deploy": ["cmd npm --prefix assets run build", "phx.digest"]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
88
mix.lock
88
mix.lock
|
@ -1,60 +1,56 @@
|
||||||
%{
|
%{
|
||||||
"bamboo": {:hex, :bamboo, "2.0.2", "0e2914d2bea0de3b1743384c24ffbe20fbb58094376a49f1cf5d9ed9959abd82", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "058d57cf4fcdac19413aa72732eb43c88954fb151a1cb6a382014e0cddbf6314"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
|
||||||
"bamboo_smtp": {:hex, :bamboo_smtp, "4.0.0", "0cc7df161d5d440d280a6d2eb20bf80bc45ea77161728a229e5ab339dcd087cd", [:mix], [{:bamboo, "~> 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.1.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "2412015092121b9f24f3f2e654bcd98e5c5f9afb323a94f8defa22e70ba8f23d"},
|
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"},
|
"castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||||
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
"comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
|
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||||
"credo": {:hex, :credo, "1.5.5", "e8f422026f553bc3bebb81c8e8bf1932f498ca03339856c7fec63d3faac8424b", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dd8623ab7091956a855dc9f3062486add9c52d310dfd62748779c4315d8247de"},
|
"credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
|
||||||
"db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"},
|
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
|
||||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||||
"ecto": {:hex, :ecto, "3.6.0", "df6b00f7278b458108044da4cff365dde31f6f2f621cf7dc0bf857b26be3bd20", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3035603f5b308ea7731b854493e5b5c1565e4d1e073186c3963b9689304f1d08"},
|
"ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
|
||||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.6.0", "5cb277b086618a644f2c5450316202a885716bb7726b9f13b74cb0708bea3a8f", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.6.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3486d6e29ee4a0e7a381390c9c289bfbbaf5dc1971e269c579799d2300e5bd5"},
|
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||||
"floki": {:hex, :floki, "0.30.1", "75d35526d3a1459920b6e87fdbc2e0b8a3670f965dd0903708d2b267e0904c55", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e9c03524447d1c4cbfccd672d739b8c18453eee377846b119d4fd71b1a176bb8"},
|
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
|
||||||
"gen_smtp": {:hex, :gen_smtp, "1.1.0", "b0c92138f69e2f73e1eb791075e93e952efcbc231a536740749b02a1a57155a3", [:rebar3], [{:hut, "1.3.0", [hex: :hut, repo: "hexpm", optional: false]}, {:ranch, ">= 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e53a13c4bb662331bdf4aa47f00982ef49ce2b4e5c703240542cb7b28f23546a"},
|
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
|
||||||
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
|
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||||
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
|
"httpoison": {:hex, :httpoison, "2.0.0", "d38b091f5e481e45cc700aba8121ce49b66d348122a097c9fbc2dc6876d88090", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "f1253bf455be73a4c3f6ae3407e7e3cf6fc91934093e056d737a0566126e2930"},
|
||||||
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
"icalendar": {:hex, :icalendar, "1.1.2", "5d0afff5d0143c5bd43f18ae32a777bf0fb9a724543ab05229a460d368f0a5e7", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2060f8e353fdf3047e95a3f012583dc3c0bbd7ca1010e32ed9e9fc5760ad4292"},
|
||||||
"icalendar": {:hex, :icalendar, "1.1.0", "898a8640abb32d161d990e419999004718a7a4b48be31f48db248f90ca33fa6e", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "a131f45fbabd2ee5a22e6bc49ea91e81131158394e7169274cee866263640dca"},
|
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
|
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||||
"nimble_csv": {:hex, :nimble_csv, "1.1.0", "b1dba4a86be9e03065c9de829050468e591f569100332db949e7ce71be0afc25", [:mix], [], "hexpm", "e986755bc302832cac429be6deda0fc9d82d3c82b47abefb68b3c17c9d949a3f"},
|
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||||
"phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"},
|
"phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
|
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"},
|
||||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
|
||||||
"phx_gen_auth": {:hex, :phx_gen_auth, "0.6.0", "4ffbfa5b34ad8178c3dfcb996fed776df425903595cbc8d56a9ae5bc53136810", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9a801c0f0bc251d8d91d62cecba0ebb6a90b8580fa8843029d931d15164e6ad9"},
|
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||||
"postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"},
|
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||||
"swoosh": {:hex, :swoosh, "1.3.2", "608819a638d527d0fd6e8892db431edd3f93c0452499880f94a80bbbcf8377e6", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "2c77c2eef273de37283bdc09c6506d466928f897600ea258fbb4765bbffd6ab2"},
|
"swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
|
||||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||||
"timex": {:hex, :timex, "3.7.3", "df8a2ea814749d700d6878ab9eacac9fdb498ecee2f507cb0002ec172bc24d0f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8691c1d86ca3a7bc14a156e2199dc8927be95d1a8f0e3b69e4bb2d6262c53ac6"},
|
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
|
||||||
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
|
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,28 +5,28 @@ defmodule Shift73k.Repo.Migrations.CreateUsersAuthTables do
|
||||||
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
|
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
|
||||||
|
|
||||||
create table(:users, primary_key: false) do
|
create table(:users, primary_key: false) do
|
||||||
add(:id, :binary_id, primary_key: true)
|
add :id, :binary_id, primary_key: true
|
||||||
add(:email, :citext, null: false)
|
add :email, :citext, null: false
|
||||||
add(:hashed_password, :string, null: false)
|
add :hashed_password, :string, null: false
|
||||||
add(:role, :string, null: false)
|
add :role, :string, null: false
|
||||||
add(:confirmed_at, :naive_datetime)
|
add :confirmed_at, :naive_datetime
|
||||||
add(:week_start_at, :string, null: false)
|
add :week_start_at, :string, null: false
|
||||||
add(:calendar_slug, :string, null: false)
|
add :calendar_slug, :string, null: false
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
create(unique_index(:users, [:email, :calendar_slug]))
|
create unique_index(:users, [:email, :calendar_slug])
|
||||||
|
|
||||||
create table(:users_tokens, primary_key: false) do
|
create table(:users_tokens, primary_key: false) do
|
||||||
add(:id, :binary_id, primary_key: true)
|
add :id, :binary_id, primary_key: true
|
||||||
add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false)
|
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
|
||||||
add(:token, :binary, null: false)
|
add :token, :binary, null: false
|
||||||
add(:context, :string, null: false)
|
add :context, :string, null: false
|
||||||
add(:sent_to, :string)
|
add :sent_to, :string
|
||||||
timestamps(updated_at: false)
|
timestamps(updated_at: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
create(index(:users_tokens, [:user_id]))
|
create index(:users_tokens, [:user_id])
|
||||||
create(unique_index(:users_tokens, [:context, :token]))
|
create unique_index(:users_tokens, [:context, :token])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Shift73k.Repo.Migrations.AddUserDefaultShiftColumn do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add(:fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :nilify_all))
|
add :fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :nilify_all)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue