Compare commits
No commits in common. "main" and "33d36ed71372c3ec864e56de5a0d36ede99d8940" have entirely different histories.
main
...
33d36ed713
182 changed files with 18424 additions and 3674 deletions
.dockerignore.gitignoreDockerfileREADME.md
assets
.babelrc
css
js
package-lock.jsonpackage.jsonpostcss.config.cjspostcss.config.jsvite.config.jswebpack.config.jsconfig
entrypoint.shlib
shift73k.ex
mix.exsmix.lockshift73k
shift73k_web.exshift73k_web
endpoint.ex
live
live_helpers.exmodal_component.ex
shift_assign_live
shift_import_live
shift_live
shift_template_live
delete_component.html.heexdelete_component.html.leexform_component.exform_component.html.heexform_component.html.leexindex.html.leex
user
registration.exregistration.html.leexreset_password.exreset_password.html.leexsettings.html.heexsettings.html.leex
settings
user_management
plugs
router.extemplates
layout
_navbar.html.eex_navbar.html.heex_preamble.html.heexapp.html.eexlive.html.leex
navbar
root.html.heexroot.html.leexuser_confirmation
user_reset_password
user_session
user_shifts_csv
user_shifts_ics
views
priv/repo/migrations
|
@ -1,45 +0,0 @@
|
|||
# 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/,
|
||||
# we ignore priv/static. You may want to comment
|
||||
# this depending on your deployment strategy.
|
||||
#/priv/static/
|
||||
/priv/static/
|
||||
|
||||
# Files matching config/*.secret.exs pattern contain sensitive
|
||||
# data and you should not commit them into version control.
|
||||
|
@ -50,4 +50,3 @@ npm-debug.log
|
|||
|
||||
# dev
|
||||
TODO.md
|
||||
NOTES.md
|
||||
|
|
96
Dockerfile
96
Dockerfile
|
@ -1,96 +0,0 @@
|
|||
# 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"]
|
149
README.md
149
README.md
|
@ -4,86 +4,81 @@ Calendaring app for shift-worker shift tracking, with support for CSV export and
|
|||
|
||||
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
|
||||
|
||||
- [X] ~~*Proper modal to delete shifts?*~~ [2022-08-14]
|
||||
- [X] ~~*move runtime config out of compile-time config files, to move towards supporting releases*~~ [2023-01-28]
|
||||
- [ ] bootstrap dark mode?
|
||||
- [ ] update tests, which are way out of date? Also I don't care?
|
||||
- [ ] Ability to edit shifts?
|
||||
- [ ] Proper modal to delete shifts?
|
||||
- [ ] Allow all-day items for notes, or require hours even for sick days?
|
||||
|
||||
## Deploying with docker
|
||||
## Deploying
|
||||
|
||||
The Dockerfile will enable building a new container. I do it all with docker compose, here's an example compose yml:
|
||||
### New versions
|
||||
|
||||
```yaml
|
||||
version: '3.9'
|
||||
services:
|
||||
shift73k:
|
||||
build:
|
||||
context: ./shift73k # relative path from docker-compose.yml to shift73k repo
|
||||
network: host
|
||||
container_name: www-shift73k
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /srv/dck/postgres/sock/postgres:/var/run/postgresql # if using unix socket
|
||||
# env_file: ./shift73k.env # optionally, put your env vars in a separate file
|
||||
environment:
|
||||
- PHX_SERVER=true
|
||||
- TZ=America/New_York
|
||||
- DB_SOCK=/var/run/postgresql # if using unix socket instead of db url
|
||||
- DB_NAME=[postgres db name] # if using unix socket instead of db url
|
||||
- DB_USER=[postgres db user] # if using unix socket instead of db url
|
||||
- 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)]
|
||||
- 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]
|
||||
ports:
|
||||
- 4000:4000
|
||||
When improvements are made, we can update the deployed version like so:
|
||||
|
||||
```shell
|
||||
cd /opt/shift73k
|
||||
git pull
|
||||
mix deps.get --only prod
|
||||
MIX_ENV=prod mix compile
|
||||
# might not be needed:
|
||||
MIX_ENV=prod mix ecto.migrate
|
||||
# rebuild static assets:
|
||||
rm -rf priv/static/
|
||||
npm run deploy --prefix ./assets
|
||||
MIX_ENV=prod mix phx.digest
|
||||
MIX_ENV=prod mix release --overwrite
|
||||
# test starting it:
|
||||
MIX_ENV=prod _build/prod/rel/shift73k/bin/shift73k start
|
||||
```
|
||||
|
||||
### systemd unit:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Shift73k service
|
||||
After=local-fs.target network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=runuser
|
||||
Group=runuser
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
5
assets/.babelrc
Normal file
5
assets/.babelrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
|
@ -9,7 +9,6 @@
|
|||
@import "bs-colors";
|
||||
|
||||
// Required || Configuration -- CONTINUED
|
||||
@import "../node_modules/bootstrap/scss/maps";
|
||||
@import "../node_modules/bootstrap/scss/mixins";
|
||||
@import "../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
|
|
|
@ -9,6 +9,3 @@
|
|||
// @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-italic.css"; /* black | italic */
|
||||
|
||||
/* Bootstrap Icons Font */
|
||||
@import "../node_modules/bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
|
|
@ -32,10 +32,6 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,9 +51,6 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
|||
background-color: $navbar-dark-color;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: $navbar-dark-toggler-border-color 0 0 0 $navbar-toggler-focus-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
assets/css/_svg-icons.scss
Normal file
21
assets/css/_svg-icons.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
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,6 +4,9 @@
|
|||
/* Load Bootstrap v5 and customizations */
|
||||
@import "bs-load";
|
||||
|
||||
/*SVG ICON SYSTEM*/
|
||||
@import "svg-icons";
|
||||
|
||||
/* LiveView specific CSS */
|
||||
@import "phx-liveview";
|
||||
|
||||
|
@ -40,7 +43,7 @@
|
|||
/* style icon */
|
||||
.inner-addon > .icon {
|
||||
position: absolute;
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: 0.5625rem 0.5rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -65,11 +68,3 @@
|
|||
.shift-description p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
fix readonly form background
|
||||
*/
|
||||
.form-control[readonly] {
|
||||
background-color: $input-disabled-bg;
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
const togglerBtn = document.getElementById('navbarSupportedContentToggler');
|
||||
const navbarContent = document.getElementById('navbarSupportedContent');
|
||||
const togglerBtn = document.getElementById("navbarSupportedContentToggler");
|
||||
const navbarContent = document.getElementById("navbarSupportedContent");
|
||||
|
||||
if (navbarContent != null) {
|
||||
navbarContent.addEventListener('show.bs.collapse', () => {
|
||||
console.log('opening navbar content');
|
||||
togglerBtn.classList.toggle('is-active');
|
||||
});
|
||||
|
||||
navbarContent.addEventListener('hide.bs.collapse', () => {
|
||||
console.log('closing navbar content');
|
||||
togglerBtn.classList.toggle('is-active');
|
||||
});
|
||||
}
|
||||
navbarContent.addEventListener("show.bs.collapse", () => {
|
||||
console.log("opening navbar content");
|
||||
togglerBtn.classList.toggle("is-active");
|
||||
});
|
||||
|
||||
navbarContent.addEventListener("hide.bs.collapse", () => {
|
||||
console.log("closing navbar content");
|
||||
togglerBtn.classList.toggle("is-active");
|
||||
});
|
||||
|
|
|
@ -1,24 +1,84 @@
|
|||
// We import the main SCSS file, which performs all other SCSS imports,
|
||||
// and which vite.js will preprocess with sass.
|
||||
import '../css/app.scss'
|
||||
// We need to import the CSS so that webpack will load it.
|
||||
// The MiniCssExtractPlugin is used to separate it out into
|
||||
// its own CSS file.
|
||||
import "../css/app.scss";
|
||||
|
||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||
import 'phoenix_html'
|
||||
// Import icons for sprite-loader
|
||||
// navbar brand icon
|
||||
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";
|
||||
|
||||
// import Socket for Phoenix Channels
|
||||
// webpack automatically bundles all modules in your
|
||||
// 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 topbar for load progress in live reloading / liveview
|
||||
import topbar from "topbar";
|
||||
// import LiveSocket for LiveView
|
||||
import { LiveSocket } from "phoenix_live_view";
|
||||
|
||||
|
||||
// Bootstrap v5 js imports
|
||||
import 'bootstrap/js/dist/alert';
|
||||
import 'bootstrap/js/dist/collapse';
|
||||
import 'bootstrap/js/dist/dropdown';
|
||||
import "bootstrap/js/dist/alert";
|
||||
import "bootstrap/js/dist/collapse";
|
||||
import "bootstrap/js/dist/dropdown";
|
||||
// Bootstrap helpers
|
||||
import './_hamburger-helper';
|
||||
import "./_hamburger-helper";
|
||||
import "./_form-validity";
|
||||
// Bootstrap-liveview helpers
|
||||
import { AlertRemover } from "./_alert-remover";
|
||||
|
|
18098
assets/package-lock.json
generated
18098
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,30 +1,43 @@
|
|||
{
|
||||
"name": "vanilla",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"repository": {},
|
||||
"description": " ",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fullhuman/postcss-purgecss": "^5.0.0",
|
||||
"@types/node": "^18.6.5",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^5.1.14",
|
||||
"phoenix_live_view": "^0.18.11",
|
||||
"sass": "^1.54.3",
|
||||
"vite": "^4.0.4"
|
||||
"deploy": "NODE_ENV=production webpack --mode production",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.9",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap-icons": "^1.9.1",
|
||||
"hamburgers": "^1.2.1",
|
||||
"phoenix": "^1.6.11",
|
||||
"phoenix_html": "^3.2.0",
|
||||
"@fontsource/lato": "^4.2.1",
|
||||
"@mdi/svg": "^5.9.55",
|
||||
"@popperjs/core": "^2.8.4",
|
||||
"bootstrap": "^5.0.0-beta3",
|
||||
"bootstrap-icons": "^1.4.0",
|
||||
"hamburgers": "^1.1.3",
|
||||
"heroicons": "^0.4.2",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"topbar": "^1.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.x",
|
||||
"@babel/preset-env": "^7.x",
|
||||
"autoprefixer": "^10.2.4",
|
||||
"babel-loader": "^8.x",
|
||||
"copy-webpack-plugin": "^8.x",
|
||||
"css-loader": "^5.x",
|
||||
"css-minimizer-webpack-plugin": "^1.x",
|
||||
"file-loader": "^6.2.0",
|
||||
"glob-all": "^3.2.1",
|
||||
"mini-css-extract-plugin": "^1.x",
|
||||
"postcss": "^8.2.6",
|
||||
"postcss-loader": "^5.0.0",
|
||||
"postcss-scss": "^3.0.4",
|
||||
"purgecss-webpack-plugin": "^4.0.2",
|
||||
"sass": "^1.x",
|
||||
"sass-loader": "^11.x",
|
||||
"svg-sprite-loader": "^6.x",
|
||||
"webpack": "^5.x",
|
||||
"webpack-cli": "^4.x"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
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',
|
||||
})
|
||||
],
|
||||
};
|
5
assets/postcss.config.js
Normal file
5
assets/postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
parser: require("postcss-scss"),
|
||||
|
||||
plugins: [require("autoprefixer")],
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
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]"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
105
assets/webpack.config.js
Normal file
105
assets/webpack.config.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
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,13 +5,19 @@
|
|||
# is restricted to this project.
|
||||
|
||||
# General application configuration
|
||||
import Config
|
||||
use Mix.Config
|
||||
|
||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||
|
||||
config :shift73k,
|
||||
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
|
||||
config :shift73k, Shift73kWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
|
@ -20,18 +26,6 @@ config :shift73k, Shift73kWeb.Endpoint,
|
|||
pubsub_server: Shift73k.PubSub,
|
||||
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
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# Configure your database
|
||||
config :shift73k, Shift73k.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
socket_dir: "/srv/dck/postgres/sock/postgres",
|
||||
database: "shift73k_dev",
|
||||
stacktrace: true,
|
||||
hostname: "localhost",
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
|
@ -23,7 +22,11 @@ config :shift73k, Shift73kWeb.Endpoint,
|
|||
check_origin: false,
|
||||
watchers: [
|
||||
node: [
|
||||
"node_modules/vite/bin/vite.js",
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
"development",
|
||||
"--watch",
|
||||
"--watch-options-stdin",
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
]
|
||||
|
@ -72,3 +75,6 @@ config :phoenix, :stacktrace_depth, 20
|
|||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
# Import secret config
|
||||
import_config "dev.secret.exs"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
|
@ -49,3 +49,7 @@ config :logger, level: :info
|
|||
# force_ssl: [hsts: true]
|
||||
#
|
||||
# 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"
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
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 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# Only in tests, remove the complexity from the password hashing algorithm
|
||||
config :bcrypt_elixir, :log_rounds, 1
|
||||
|
@ -13,8 +13,7 @@ config :shift73k, Shift73k.Repo,
|
|||
password: "postgres",
|
||||
database: "shift73k_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
hostname: "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
|
@ -25,8 +24,8 @@ config :shift73k, Shift73kWeb.Endpoint,
|
|||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
# Swoosh test mailer config
|
||||
config :shift73k, Shift73k.Mailer, adapter: Swoosh.Adapters.Test
|
||||
# Bamboo test mailer config
|
||||
config :shift73k, Shift73k.Mailer, adapter: Bamboo.TestAdapter
|
||||
|
||||
# Import secret config
|
||||
# import_config "test.secret.exs"
|
||||
import_config "test.secret.exs"
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/ash
|
||||
|
||||
export MIX_ENV="prod"
|
||||
|
||||
cd /app
|
||||
mix ecto.migrate
|
||||
exec mix phx.server
|
|
@ -7,32 +7,12 @@ defmodule Shift73k do
|
|||
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]
|
||||
|
||||
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 app_time_zone, do: @app_time_zone
|
||||
|
||||
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
|
||||
|
|
|
@ -108,13 +108,6 @@ defmodule Shift73k.Accounts 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.registration_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|
|
|
@ -20,19 +20,19 @@ defmodule Shift73k.Accounts.User do
|
|||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :password, :string, virtual: true
|
||||
field :hashed_password, :string
|
||||
field :confirmed_at, :naive_datetime
|
||||
field :calendar_slug, :string, default: Ecto.UUID.generate()
|
||||
field(:email, :string)
|
||||
field(:password, :string, virtual: true)
|
||||
field(:hashed_password, :string)
|
||||
field(:confirmed_at, :naive_datetime)
|
||||
field(:calendar_slug, :string, default: Ecto.UUID.generate())
|
||||
|
||||
field :role, Ecto.Enum, values: Keyword.keys(@roles), default: :user
|
||||
field :week_start_at, Ecto.Enum, values: weekdays(), default: :monday
|
||||
field(:role, Ecto.Enum, values: Keyword.keys(@roles), default: :user)
|
||||
field(:week_start_at, Ecto.Enum, values: weekdays(), default: :monday)
|
||||
|
||||
has_many :shift_templates, ShiftTemplate
|
||||
belongs_to :fave_shift_template, ShiftTemplate
|
||||
has_many(:shift_templates, ShiftTemplate)
|
||||
belongs_to(:fave_shift_template, ShiftTemplate)
|
||||
|
||||
has_many :shifts, Shift
|
||||
has_many(:shifts, Shift)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
|
@ -2,19 +2,12 @@ defmodule Shift73k.Accounts.UserNotifier do
|
|||
alias Shift73k.Mailer
|
||||
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 """
|
||||
Deliver instructions to confirm account.
|
||||
"""
|
||||
def deliver_confirmation_instructions(user, url) do
|
||||
deliver(user.email, "Confirmation instructions", """
|
||||
user
|
||||
|> UserEmail.compose("Confirm Your Account", """
|
||||
|
||||
==============================
|
||||
|
||||
|
@ -28,13 +21,15 @@ defmodule Shift73k.Accounts.UserNotifier do
|
|||
|
||||
==============================
|
||||
""")
|
||||
|> Mailer.deliver_later()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deliver instructions to reset a user password.
|
||||
"""
|
||||
def deliver_reset_password_instructions(user, url) do
|
||||
deliver(user.email, "Reset password instructions", """
|
||||
user
|
||||
|> UserEmail.compose("Reset Your Password", """
|
||||
|
||||
==============================
|
||||
|
||||
|
@ -48,13 +43,15 @@ defmodule Shift73k.Accounts.UserNotifier do
|
|||
|
||||
==============================
|
||||
""")
|
||||
|> Mailer.deliver_later()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deliver instructions to update a user email.
|
||||
"""
|
||||
def deliver_update_email_instructions(user, url) do
|
||||
deliver(user.email, "Update email instructions", """
|
||||
user
|
||||
|> UserEmail.compose("Change Your E-mail", """
|
||||
|
||||
==============================
|
||||
|
||||
|
@ -68,5 +65,6 @@ defmodule Shift73k.Accounts.UserNotifier do
|
|||
|
||||
==============================
|
||||
""")
|
||||
|> Mailer.deliver_later()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,10 +15,10 @@ defmodule Shift73k.Accounts.UserToken do
|
|||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users_tokens" do
|
||||
field :token, :binary
|
||||
field :context, :string
|
||||
field :sent_to, :string
|
||||
belongs_to :user, Shift73k.Accounts.User
|
||||
field(:token, :binary)
|
||||
field(:context, :string)
|
||||
field(:sent_to, :string)
|
||||
belongs_to(:user, Shift73k.Accounts.User)
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
defmodule Shift73k.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :shift73k
|
||||
use Bamboo.Mailer, otp_app: :shift73k
|
||||
end
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
defmodule Shift73k.Mailer.UserEmail do
|
||||
import Swoosh.Email
|
||||
import Shift73k, only: [get_app_mailer_from: 0, get_app_mailer_reply_to: 0]
|
||||
import Bamboo.Email
|
||||
|
||||
@mailer_vars Application.compile_env(:shift73k, :app_global_vars,
|
||||
mailer_reply_to: "admin@example.com",
|
||||
mailer_from: {"Shift73k", "shift73k@example.com"}
|
||||
)
|
||||
|
||||
def compose(user_email, subject, body_text) do
|
||||
new()
|
||||
|> from(get_app_mailer_from())
|
||||
|> to(user_email)
|
||||
|> header("Reply-To", get_app_mailer_reply_to())
|
||||
def compose(user, subject, body_text) do
|
||||
new_email()
|
||||
|> from(@mailer_vars[:mailer_from])
|
||||
|> to(user.email)
|
||||
|> put_header("Reply-To", @mailer_vars[:mailer_reply_to])
|
||||
|> subject(subject)
|
||||
|> text_body(body_text)
|
||||
end
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
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,12 +86,8 @@ defmodule Shift73k.Shifts do
|
|||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_shift!(nil), do: nil
|
||||
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 """
|
||||
Creates a shift.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule Shift73k.Shifts.Shift do
|
|||
field :time_start, :time
|
||||
field :time_end, :time
|
||||
|
||||
belongs_to :user, Shift73k.Accounts.User
|
||||
belongs_to(:user, Shift73k.Accounts.User)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Shift73k, only: [get_app_time_zone: 0]
|
||||
import Shift73k, only: [app_time_zone: 0]
|
||||
|
||||
alias Shift73k.Shifts
|
||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||
|
@ -12,12 +12,12 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
|||
field :subject, :string
|
||||
field :description, :string
|
||||
field :location, :string
|
||||
field :time_zone, :string, default: get_app_time_zone()
|
||||
field :time_zone, :string, default: app_time_zone()
|
||||
field :time_start, :time, default: ~T[09:00:00]
|
||||
field :time_end, :time, default: ~T[17:00:00]
|
||||
|
||||
belongs_to :user, Shift73k.Accounts.User
|
||||
has_one :is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id
|
||||
belongs_to(:user, Shift73k.Accounts.User)
|
||||
has_one(:is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -57,7 +57,6 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
|||
[]
|
||||
end
|
||||
end)
|
||||
|> validate_not_nil([:time_zone])
|
||||
|> validate_inclusion(:time_zone, Tzdata.zone_list(),
|
||||
message: "must be a valid IANA tz database time zone"
|
||||
)
|
||||
|
@ -73,14 +72,4 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
|||
|> Map.from_struct()
|
||||
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
|
||||
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
|
||||
|
|
|
@ -46,7 +46,7 @@ defmodule Shift73kWeb do
|
|||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {Shift73kWeb.LayoutView, :live}
|
||||
layout: {Shift73kWeb.LayoutView, "live.html"}
|
||||
|
||||
unquote(view_helpers())
|
||||
import Shift73kWeb.LiveHelpers
|
||||
|
@ -100,11 +100,14 @@ defmodule Shift73kWeb do
|
|||
use Phoenix.HTML
|
||||
|
||||
# Import LiveView helpers (live_render, live_component, live_patch, etc)
|
||||
import Phoenix.Component
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
|
||||
# Import SVG Icon helper
|
||||
import Shift73kWeb.IconHelpers
|
||||
|
||||
import Shift73kWeb.ErrorHelpers
|
||||
import Shift73kWeb.Gettext
|
||||
alias Shift73kWeb.Router.Helpers, as: Routes
|
||||
|
|
|
@ -10,50 +10,51 @@ defmodule Shift73kWeb.Endpoint do
|
|||
signing_salt: "9CKxo0VJ"
|
||||
]
|
||||
|
||||
socket "/socket", Shift73kWeb.UserSocket,
|
||||
socket("/socket", Shift73kWeb.UserSocket,
|
||||
websocket: true,
|
||||
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.
|
||||
#
|
||||
# You should set gzip to true if you are running phx.digest
|
||||
# when deploying your static files in production.
|
||||
#
|
||||
# file list generated by simple ls -1 priv/static/ - then copy/paste here
|
||||
plug Plug.Static,
|
||||
# file list generated by simple ls -1 assets/static/ - then copy/paste here
|
||||
plug(Plug.Static,
|
||||
at: "/",
|
||||
from: :shift73k,
|
||||
gzip: false
|
||||
|
||||
# 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
|
||||
gzip: (Mix.env() not in [:dev, :test]),
|
||||
only: "priv/static" |> Path.expand() |> File.ls!()
|
||||
)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k
|
||||
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
|
||||
plug(Phoenix.LiveReloader)
|
||||
plug(Phoenix.CodeReloader)
|
||||
plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k)
|
||||
end
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
plug(Phoenix.LiveDashboard.RequestLogger,
|
||||
param_key: "request_logger",
|
||||
cookie_key: "request_logger"
|
||||
)
|
||||
|
||||
plug Plug.Parsers,
|
||||
plug(Plug.RequestId)
|
||||
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
|
||||
|
||||
plug(Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
)
|
||||
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
plug Shift73kWeb.Router
|
||||
plug(Plug.MethodOverride)
|
||||
plug(Plug.Head)
|
||||
plug(Plug.Session, @session_options)
|
||||
plug(Shift73kWeb.Router)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Shift73kWeb.LiveHelpers do
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.Component
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
@ -19,6 +19,27 @@ defmodule Shift73kWeb.LiveHelpers do
|
|||
"""
|
||||
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 """
|
||||
Loads default assigns for liveviews
|
||||
"""
|
||||
|
|
|
@ -3,12 +3,12 @@ defmodule Shift73kWeb.ModalComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id} class="modal fade"
|
||||
~L"""
|
||||
<div id="<%= @id %>" class="modal fade"
|
||||
phx-hook="BsModal"
|
||||
phx-window-keydown="hide"
|
||||
phx-key="escape"
|
||||
phx-target={"#" <> to_string(@id)}
|
||||
phx-target="#<%= @id %>"
|
||||
phx-page-loading>
|
||||
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
@ -16,10 +16,10 @@ defmodule Shift73kWeb.ModalComponent do
|
|||
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
|
||||
<%= live_component @component, Keyword.put(@opts, :modal_id, @id) %>
|
||||
<%= live_component @socket, @component, Keyword.put(@opts, :modal_id, @id) %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,31 @@
|
|||
<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,6 +1,5 @@
|
|||
defmodule Shift73kWeb.ShiftAssignLive.Index do
|
||||
use Shift73kWeb, :live_view
|
||||
import Shift73k, only: [get_app_time_zone: 0]
|
||||
|
||||
alias Shift73k.Repo
|
||||
alias Shift73k.Shifts
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<%= if @delete_days_shifts do %>
|
||||
<.live_component
|
||||
module={Shift73kWeb.ModalComponent}
|
||||
id="modal"
|
||||
component={Shift73kWeb.ShiftAssignLive.DeleteComponent}
|
||||
opts={[
|
||||
<%= live_modal @socket, Shift73kWeb.ShiftAssignLive.DeleteComponent,
|
||||
id: "delete-days-shifts-#{@current_user.id}",
|
||||
title: "Delete Shifts From Selected Days",
|
||||
delete_days_shifts: @delete_days_shifts,
|
||||
current_user: @current_user
|
||||
]}
|
||||
/>
|
||||
%>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h2 class="mb-3 mb-sm-0">
|
||||
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||
<%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %>
|
||||
Schedule Shifts
|
||||
</h2>
|
||||
|
||||
<div class="row justify-content-center mt-4">
|
||||
|
@ -28,19 +24,14 @@
|
|||
%>
|
||||
<% end %>
|
||||
|
||||
<% details_button_class = "ms-2 btn btn-primary text-nowrap"
|
||||
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>
|
||||
<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">
|
||||
<%= icon_div @socket, (@show_template_btn_active && "bi-binoculars-fill" || "bi-binoculars"), [class: "icon baseline"] %>
|
||||
<span class="d-none d-sm-inline">Details</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<% 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="col-12 col-lg-9 col-xl-8 col-xxl-7 <%= @show_template_details && "collapse show" || "collapse" %>" id="#templateDetailsCol" phx-hook="BsCollapse">
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
|
||||
|
@ -50,8 +41,8 @@
|
|||
|
||||
<div class="col-12 col-md-6">
|
||||
<%= label stf, :subject, "Subject/Title", class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :subject)}>
|
||||
<i class="bi bi-tag icon is-left text-muted fs-5"></i>
|
||||
<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"] %>
|
||||
<%= text_input stf, :subject,
|
||||
value: input_value(stf, :subject),
|
||||
class: input_class(stf, :subject, "form-control"),
|
||||
|
@ -66,27 +57,25 @@
|
|||
<div class="col-12 col-md-6 mb-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" %>
|
||||
<%= time_input stf, :time_start,
|
||||
precision: :minute,
|
||||
value: input_value(stf, :time_start),
|
||||
class: input_class(stf, :time_start, "form-control"),
|
||||
disabled: @shift_template.id != @custom_shift.id,
|
||||
aria_describedby: error_ids(stf, :time_start),
|
||||
required: true
|
||||
aria_describedby: error_ids(stf, :time_start)
|
||||
%>
|
||||
</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" %>
|
||||
<%= time_input stf, :time_end,
|
||||
precision: :minute,
|
||||
value: input_value(stf, :time_end),
|
||||
class: input_class(stf, :time_end, "form-control"),
|
||||
disabled: @shift_template.id != @custom_shift.id,
|
||||
aria_describedby: error_ids(stf, :time_end),
|
||||
required: true
|
||||
aria_describedby: error_ids(stf, :time_end)
|
||||
%>
|
||||
</div>
|
||||
|
||||
|
@ -94,18 +83,18 @@
|
|||
|
||||
<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 %>
|
||||
</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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<%= label stf, :location, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :location)}>
|
||||
<i class="bi bi-geo icon is-left text-muted fs-5"></i>
|
||||
<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"] %>
|
||||
<%= text_input stf, :location,
|
||||
value: input_value(stf, :location),
|
||||
class: input_class(stf, :location, "form-control"),
|
||||
|
@ -119,19 +108,18 @@
|
|||
|
||||
<div class="col-12 col-md-6">
|
||||
<%= 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)}>
|
||||
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||
<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"] %>
|
||||
<%= text_input stf, :time_zone,
|
||||
value: input_value(stf, :time_zone),
|
||||
class: input_class(stf, :time_zone, "form-control"),
|
||||
disabled: @shift_template.id != @custom_shift.id,
|
||||
phx_debounce: 250,
|
||||
list: "tz_list",
|
||||
placeholder: "Default: #{get_app_time_zone()}"
|
||||
list: "tz_list"
|
||||
%>
|
||||
<datalist id="tz_list">
|
||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||
<option value={tz_name}></option>
|
||||
<option value="<%= tz_name %>"></option>
|
||||
<% end %>
|
||||
end
|
||||
</datalist>
|
||||
|
@ -144,7 +132,7 @@
|
|||
|
||||
<div class="col-12">
|
||||
<%= 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,
|
||||
value: input_value(stf, :description),
|
||||
|
@ -186,17 +174,17 @@
|
|||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||
</h3>
|
||||
<div>
|
||||
<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}>
|
||||
<i class="bi bi-asterisk me-sm-1"></i>
|
||||
<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" %>>
|
||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
||||
<span class="d-none d-sm-inline">Today</span>
|
||||
</button>
|
||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
||||
<span class="d-none d-sm-inline">Prev</span>
|
||||
</button>
|
||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||
<span class="d-none d-sm-inline">Next</span>
|
||||
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -216,7 +204,7 @@
|
|||
<%= for week <- @week_rows do %>
|
||||
<tr>
|
||||
<%= 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")}" %>
|
||||
|
||||
|
@ -248,16 +236,19 @@
|
|||
<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">
|
||||
|
||||
<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}>
|
||||
<i class="bi bi-trash me-1"></i> Delete shifts from selected days
|
||||
<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" %>>
|
||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
||||
Delete shifts from selected days
|
||||
</button>
|
||||
|
||||
<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}>
|
||||
<i class="bi bi-eraser me-1"></i> De-select all selected
|
||||
<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" %>>
|
||||
<%= icon_div @socket, "bi-eraser", [class: "icon baseline"] %>
|
||||
De-select all selected
|
||||
</button>
|
||||
|
||||
<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}>
|
||||
<i class="bi bi-save me-1"></i> Assign shifts to selected days
|
||||
<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" %>>
|
||||
<%= icon_div @socket, "bi-save", [class: "icon baseline"] %>
|
||||
Assign shifts to selected days
|
||||
</button>
|
||||
|
||||
</div>
|
|
@ -2,8 +2,10 @@
|
|||
<div class="col-12 col-md-10 col-xl-8">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-box-arrow-in-left me-1"></i> Import Shifts
|
||||
<%= icon_div @socket, "bi-box-arrow-in-left", [class: "icon baseline"] %>
|
||||
Import Shifts
|
||||
</h2>
|
||||
|
||||
<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">
|
||||
|
@ -15,7 +17,7 @@
|
|||
<% valid_class = @url_validated && "is-valid" || "" %>
|
||||
<%= label iimf, :ics_url, "iCal/ics URL", class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-link icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-link", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= url_input iimf, :ics_url,
|
||||
class: show_url_error && "form-control is-invalid" || "form-control #{valid_class}",
|
||||
phx_debounce: 500,
|
||||
|
@ -31,9 +33,9 @@
|
|||
|
||||
<%= label iimf, :time_zone, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= text_input iimf, :time_zone,
|
||||
value: Shift73k.get_app_time_zone(),
|
||||
value: Shift73k.app_time_zone(),
|
||||
class: @tz_valid && "form-control" || "form-control is-invalid",
|
||||
phx_debounce: 250,
|
||||
aria_describedby: "ics-import-tz-error",
|
||||
|
@ -41,7 +43,7 @@
|
|||
%>
|
||||
<datalist id="tz_list">
|
||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||
<option value={tz_name}></option>
|
||||
<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>
|
|
@ -1,46 +0,0 @@
|
|||
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
|
|
@ -1,22 +0,0 @@
|
|||
<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,7 +22,6 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
|||
socket
|
||||
|> init_today(Date.utc_today())
|
||||
|> update_agenda()
|
||||
|> assign_modal_close_handlers()
|
||||
|> assign(:delete_shift, nil)
|
||||
|> apply_action(socket.assigns.live_action, params)
|
||||
|> live_noreply()
|
||||
|
@ -34,11 +33,6 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
|||
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
|
||||
socket
|
||||
|> assign(:page_title, "My Shifts")
|
||||
|
@ -82,14 +76,6 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
|||
|> assign_known_shifts()
|
||||
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
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
shift = Shifts.get_shift!(id)
|
||||
|
@ -108,28 +94,6 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
|||
|> live_noreply()
|
||||
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(nav, cursor_date) do
|
||||
|
|
|
@ -1,48 +1,40 @@
|
|||
<%= 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="col-md-10 col-xl-10">
|
||||
|
||||
<h2 class="mb-3 mb-sm-0">
|
||||
<i class="bi bi-card-list me-1"></i> My Shifts
|
||||
<%= icon_div @socket, "bi-card-list", [class: "icon baseline"] %>
|
||||
My Shifts
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="row justify-content-start justify-content-sm-center">
|
||||
<div class="col-md-10 col-xl-10">
|
||||
|
||||
|
||||
<%# month navigation %>
|
||||
<div class="d-flex justify-content-between align-items-end my-4">
|
||||
<h3 class="text-muted mb-0">
|
||||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||
</h3>
|
||||
<div>
|
||||
<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}>
|
||||
<i class="bi bi-asterisk me-sm-1"></i>
|
||||
<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" %>>
|
||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
||||
<span class="d-none d-sm-inline">Today</span>
|
||||
</button>
|
||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
||||
<span class="d-none d-sm-inline">Prev</span>
|
||||
</button>
|
||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||
<span class="d-none d-sm-inline">Next</span>
|
||||
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<%= for day <- Enum.to_list(@date_range) do %>
|
||||
<%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
|
||||
<div class="border-top mt-4 mb-4"></div>
|
||||
|
@ -53,21 +45,24 @@
|
|||
<% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
|
||||
<%= if !Enum.empty?(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">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-tag text-muted me-1"></i>
|
||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline text-muted me-1"] %>
|
||||
<%= shift.subject %>
|
||||
</h5>
|
||||
|
||||
|
||||
<table class="table table-borderless table-nonfluid table-sm">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-hourglass text-muted"></i>
|
||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Hours:</span>
|
||||
</th>
|
||||
<td>
|
||||
|
@ -84,7 +79,7 @@
|
|||
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-geo text-muted"></i>
|
||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Location:</span>
|
||||
</th>
|
||||
<td>
|
||||
|
@ -97,7 +92,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-justify-left text-muted"></i>
|
||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Description:</span>
|
||||
</th>
|
||||
<td class="shift-description">
|
||||
|
@ -111,22 +106,51 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={shift.id}>
|
||||
<i class="bi bi-trash me-1"></i> Delete
|
||||
</button>
|
||||
<%#= 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 %>
|
||||
<%#= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
||||
<%# 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>
|
||||
|
||||
|
||||
|
||||
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<% else %>
|
||||
<p class="text-muted"><em>Nothing scheduled</em></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,19 @@
|
|||
<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,6 +1,5 @@
|
|||
defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
|
||||
use Shift73kWeb, :live_component
|
||||
import Shift73k, only: [get_app_time_zone: 0]
|
||||
|
||||
alias Shift73k.Shifts.Templates
|
||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,116 @@
|
|||
<%= 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,29 +1,17 @@
|
|||
<%= if @live_action in [:new, :edit, :clone] do %>
|
||||
<.live_component
|
||||
module={Shift73kWeb.ModalComponent}
|
||||
id="modal"
|
||||
component={Shift73kWeb.ShiftTemplateLive.FormComponent}
|
||||
opts={[
|
||||
id: @shift_template.id || :new,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
shift_template: @shift_template,
|
||||
current_user: @current_user
|
||||
]}
|
||||
/>
|
||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.FormComponent,
|
||||
id: @shift_template.id || :new,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
shift_template: @shift_template,
|
||||
current_user: @current_user %>
|
||||
<% end %>
|
||||
|
||||
<%= if @delete_shift_template do %>
|
||||
<.live_component
|
||||
module={Shift73kWeb.ModalComponent}
|
||||
id="modal"
|
||||
component={Shift73kWeb.ShiftTemplateLive.DeleteComponent}
|
||||
opts={[
|
||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent,
|
||||
id: @delete_shift_template.id,
|
||||
title: "Delete Shift Template",
|
||||
delete_shift_template: @delete_shift_template
|
||||
]}
|
||||
/>
|
||||
delete_shift_template: @delete_shift_template %>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
@ -32,10 +20,12 @@
|
|||
|
||||
<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">
|
||||
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||
<%= icon_div @socket, "bi-clock-history", [class: "icon baseline"] %>
|
||||
My Shift Templates
|
||||
</h2>
|
||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %>
|
||||
<i class="bi bi-plus-circle-dotted me-1"></i> New Shift Template
|
||||
<%= icon_div @socket, "bi-plus-circle-dotted", [class: "icon baseline"] %>
|
||||
New Shift Template
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
@ -49,13 +39,13 @@
|
|||
<div class="card mt-4">
|
||||
<h5 class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="visually-hidden">Subject:</span>
|
||||
<i class="bi bi-tag me-1"></i>
|
||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline me-1"] %>
|
||||
<div class="w-100"><%= template.subject %></div>
|
||||
<% fav_icon_data = if template.id == @current_user.fave_shift_template_id, do:
|
||||
{"bi-star-fill", "Unset as favorite", "unset-user-fave-shift-template"}, else:
|
||||
{"bi-star", "Set as favorite", "set-user-fave-shift-template"}
|
||||
%>
|
||||
<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>
|
||||
<%= 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"] %>
|
||||
<% else %>
|
||||
<%= 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 %>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
|
||||
|
@ -63,7 +53,7 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-hourglass text-muted"></i>
|
||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Hours:</span>
|
||||
</th>
|
||||
<td>
|
||||
|
@ -80,7 +70,7 @@
|
|||
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-geo text-muted"></i>
|
||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Location:</span>
|
||||
</th>
|
||||
<td>
|
||||
|
@ -93,7 +83,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">
|
||||
<i class="bi bi-justify-left text-muted"></i>
|
||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
||||
<span class="visually-hidden">Description:</span>
|
||||
</th>
|
||||
<td>
|
||||
|
@ -109,19 +99,24 @@
|
|||
|
||||
<%= 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 %>
|
||||
<i class="bi bi-pencil me-1"></i> Edit
|
||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
||||
Edit
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= 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 %>
|
||||
<i class="bi bi-clipboard-plus me-1"></i> Clone
|
||||
<%= icon_div @socket, "bi-clipboard-plus", [class: "icon baseline"] %>
|
||||
Clone
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%#= button "" %>
|
||||
|
||||
<%= 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}>
|
||||
<i class="bi bi-trash me-1"></i> Delete
|
||||
<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"] %>
|
||||
Delete
|
||||
</button>
|
||||
<% end %>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Shift73kWeb.UserLive.Registration do
|
||||
use Shift73kWeb, :live_view
|
||||
alias Shift73k.Repo
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
|
@ -20,7 +20,9 @@ defmodule Shift73kWeb.UserLive.Registration do
|
|||
user_id: nil,
|
||||
user_return_to: Map.get(session, "user_return_to", "/"),
|
||||
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
|
||||
|
@ -33,33 +35,19 @@ defmodule Shift73kWeb.UserLive.Registration do
|
|||
|
||||
@impl true
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
is_first_user = !Repo.exists?(User)
|
||||
user_params
|
||||
|> Map.put("role", Accounts.registration_role())
|
||||
|> Accounts.register_user()
|
||||
|> case do
|
||||
{:ok, user} ->
|
||||
# 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(
|
||||
user,
|
||||
&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)
|
||||
{:ok, %Bamboo.Email{}} =
|
||||
Accounts.deliver_user_confirmation_instructions(
|
||||
user,
|
||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||
)
|
||||
|
||||
socket
|
||||
|> assign(login_params: login_params)
|
||||
|> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
|
||||
|> assign(trigger_submit: true)
|
||||
|> live_noreply()
|
||||
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-journal-plus me-1"></i> Register
|
||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %>
|
||||
Register
|
||||
</h2>
|
||||
<p class="lead">Create an account to manage your work shifts with us.</p>
|
||||
|
||||
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="reg_form">
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %>
|
||||
|
||||
<%= 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>
|
||||
<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"),
|
||||
|
@ -24,8 +25,8 @@
|
|||
</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>
|
||||
<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"),
|
||||
|
@ -45,7 +46,7 @@
|
|||
%>
|
||||
</div>
|
||||
|
||||
</.form>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
|
@ -1,12 +1,9 @@
|
|||
defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
import Shift73k, only: [get_app_allow_reg: 0]
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
user = Accounts.get_user!(session["user_id"])
|
||||
|
@ -40,6 +37,4 @@ defmodule Shift73kWeb.UserLive.ResetPassword do
|
|||
|> assign(changeset: changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
def allow_registration, do: get_app_allow_reg()
|
||||
end
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-shield-lock me-1"></i> Reset password
|
||||
<%= icon_div @socket, "bi-shield-lock", [class: "icon baseline"] %>
|
||||
Reset password
|
||||
</h2>
|
||||
<p class="lead">Hi <%= @user.email %> — tell us your new password, please.</p>
|
||||
|
||||
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="pw_reset_form">
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "pw_reset_form"], fn f -> %>
|
||||
|
||||
<%= label f, :password, "New 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>
|
||||
<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"),
|
||||
|
@ -22,8 +23,8 @@
|
|||
</div>
|
||||
|
||||
<%= 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)}>
|
||||
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||
<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"] %>
|
||||
<%= password_input f, :password_confirmation,
|
||||
value: input_value(f, :password_confirmation),
|
||||
class: input_class(f, :password_confirmation, "form-control"),
|
||||
|
@ -42,12 +43,10 @@
|
|||
%>
|
||||
</div>
|
||||
|
||||
</.form>
|
||||
<% end %>
|
||||
|
||||
<p class="mt-3 is-pulled-right">
|
||||
<%= if allow_registration() do %>
|
||||
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
||||
<% end %>
|
||||
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
||||
</p>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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>
|
17
lib/shift73k_web/live/user/settings.html.leex
Normal file
17
lib/shift73k_web/live/user/settings.html.leex
Normal file
|
@ -0,0 +1,17 @@
|
|||
<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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col mb-3">
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||
|
||||
<%= 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>
|
||||
<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"),
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
<%= label f, :current_password, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= password_input f, :current_password,
|
||||
value: input_value(f, :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>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||
|
||||
<%= label f, :password, "New 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>
|
||||
<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"),
|
||||
|
@ -18,8 +18,8 @@
|
|||
</div>
|
||||
|
||||
<%= 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)}>
|
||||
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||
<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"] %>
|
||||
<%= password_input f, :password_confirmation,
|
||||
value: input_value(f, :password_confirmation),
|
||||
class: input_class(f, :password_confirmation, "form-control"),
|
||||
|
@ -31,7 +31,7 @@
|
|||
|
||||
<%= label f, :current_password, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= password_input f, :current_password,
|
||||
value: input_value(f, :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>
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
|||
|
||||
<%= label cvf, :week_start_at, "Week starts at", class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-calendar2-range icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-calendar2-range", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= select cvf, :week_start_at, week_start_options(),
|
||||
value: @current_user.week_start_at,
|
||||
class: "form-select"
|
|
@ -9,13 +9,9 @@ defmodule Shift73kWeb.UserManagement.DeleteComponent do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("confirm", %{"id" => id, "email" => email} = params, socket) do
|
||||
IO.inspect(params)
|
||||
|
||||
user = Accounts.get_user(id)
|
||||
IO.inspect(user)
|
||||
|
||||
user
|
||||
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
|
||||
id
|
||||
|> Accounts.get_user()
|
||||
|> Accounts.delete_user()
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,16 @@
|
|||
<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
|
||||
case Accounts.register_user(user_params) do
|
||||
{:ok, user} ->
|
||||
{:ok, _, %Swoosh.Email{} = _captured_email} =
|
||||
{:ok, %Bamboo.Email{}} =
|
||||
Accounts.deliver_user_confirmation_instructions(
|
||||
user,
|
||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,62 @@
|
|||
<%= 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
|
||||
ndt
|
||||
|> DateTime.from_naive!(Shift73k.get_app_time_zone())
|
||||
|> DateTime.from_naive!(Shift73k.app_time_zone())
|
||||
|> Calendar.strftime("%Y %b %-d, %-I:%M %p")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,34 +1,24 @@
|
|||
<%= if @live_action in [:new, :edit] do %>
|
||||
<.live_component
|
||||
module={Shift73kWeb.ModalComponent}
|
||||
id="modal"
|
||||
component={Shift73kWeb.UserManagement.FormComponent}
|
||||
opts={[
|
||||
<%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent,
|
||||
id: @user.id || :new,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
user: @user,
|
||||
current_user: @current_user
|
||||
]}
|
||||
/>
|
||||
current_user: @current_user %>
|
||||
<% end %>
|
||||
|
||||
<%= if @delete_user do %>
|
||||
<.live_component
|
||||
module={Shift73kWeb.ModalComponent}
|
||||
id="modal"
|
||||
component={Shift73kWeb.UserManagement.DeleteComponent}
|
||||
opts={[
|
||||
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
|
||||
id: @delete_user.id,
|
||||
title: "Delete User",
|
||||
delete_user: @delete_user
|
||||
]}
|
||||
/>
|
||||
%>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h2 class="mb-3">
|
||||
<i class="bi bi-people me-1"></i> Listing Users
|
||||
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
|
||||
Listing Users
|
||||
</h2>
|
||||
|
||||
<%# filtering and new item creation %>
|
||||
|
@ -37,7 +27,8 @@
|
|||
<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, [])),
|
||||
class: "btn btn-primary mb-3 mb-md-0" do %>
|
||||
<i class="bi bi-person-plus me-1"></i> New User
|
||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
|
||||
New User
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
@ -45,10 +36,10 @@
|
|||
<%= form_for :sort, "#", [phx_change: "sort-by-change"], fn srt -> %>
|
||||
<%= label srt, :sort_by, class: "visually-hidden" %>
|
||||
<div class="input-group inner-addon left-addon mb-3 mb-md-0">
|
||||
<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 rounded-start" %>
|
||||
<%= icon_div @socket, "bi-arrow-down-up", [class: "icon is-left text-muted fs-5", style: "z-index:1001;"], [style: "padding: 1px;"] %>
|
||||
<%= Phoenix.HTML.Form.select srt, :sort_by, ["Email": "email", "Role": "role", "Created at": "inserted_at"], value: @query.sort_by, class: "form-select" %>
|
||||
<button class="btn btn-primary" type="button" aria-label="Change sort order" phx-click="sort-order-change">
|
||||
<i class={if @query.sort_order == "desc", do: "bi bi-sort-up-alt", else: "bi bi-sort-down-alt"}></i>
|
||||
<%= icon_div @socket, (@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"), [class: "icon baseline"] %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -58,9 +49,9 @@
|
|||
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
||||
<%= label flt, :filter, class: "visually-hidden" %>
|
||||
<div class="inner-addon left-addon right-addon">
|
||||
<i class="bi bi-funnel icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @socket, "bi-funnel", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= if @query.filter != "" do %>
|
||||
<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>
|
||||
<%= 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"] %>
|
||||
<% end %>
|
||||
<%= text_input flt, :filter,
|
||||
name: "filter",
|
||||
|
@ -101,19 +92,21 @@
|
|||
</dt>
|
||||
<dd class="d-inline d-sm-block col-auto">
|
||||
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
|
||||
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<%= 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 %>
|
||||
<i class="bi bi-pencil me-1"></i> Edit
|
||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
||||
Edit
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= 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}>
|
||||
<i class="bi bi-trash me-1"></i> Delete
|
||||
<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"] %>
|
||||
Delete
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
|
@ -133,25 +126,28 @@
|
|||
<thead>
|
||||
<tr>
|
||||
|
||||
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||
Email
|
||||
<%= if @query.sort_by == "email" do %>
|
||||
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||
<% end %>
|
||||
<%= if @query.sort_by == "email", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||
Role
|
||||
<%= if @query.sort_by == "role" do %>
|
||||
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||
<% end %>
|
||||
<%= if @query.sort_by == "role", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||
Created at
|
||||
<%= if @query.sort_by == "inserted_at" do %>
|
||||
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||
<% end %>
|
||||
<%= if @query.sort_by == "inserted_at", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col">Confirmed?</th>
|
||||
|
@ -172,25 +168,27 @@
|
|||
</tr>
|
||||
<% else %>
|
||||
<%= 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.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">
|
||||
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
|
||||
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
||||
</td>
|
||||
<td class="align-middle text-end text-nowrap">
|
||||
|
||||
<%= 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 %>
|
||||
<i class="bi bi-pencil me-1"></i> Edit
|
||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
||||
Edit
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= 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}>
|
||||
<i class="bi bi-trash me-1"></i> Delete
|
||||
<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"] %>
|
||||
Delete
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
|
@ -229,47 +227,47 @@
|
|||
<ul class="pagination mb-0">
|
||||
|
||||
<%# previous page button %>
|
||||
<% icon = icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
||||
<%= if @page.page_number == 1 do %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</li>
|
||||
<% else %>
|
||||
<li class="page-item">
|
||||
<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>
|
||||
</li>
|
||||
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number="<%= @page.page_number - 1 %>" phx-click="page-change"><%= icon %></a>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<%# page buttons %>
|
||||
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
||||
<%= cond do %>
|
||||
<% page_num < 1 -> %>
|
||||
<%= page_num < 1 -> %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">…</span>
|
||||
<span class="visually-hidden" role="img" aria-label="ellipses">…</span>
|
||||
</li>
|
||||
<% page_num == @page.page_number -> %>
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link"><%= page_num %></span>
|
||||
<span class="page-link"><%= page_num %></a>
|
||||
</li>
|
||||
<% true -> %>
|
||||
<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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%# next page button %>
|
||||
<% icon = icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %>
|
||||
<%= if @page.page_number == @page.total_pages do %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</li>
|
||||
<% else %>
|
||||
<li class="page-item">
|
||||
<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>
|
||||
</li>
|
||||
<a class="page-link" href="#" aria-label="Next" phx-value-page_number="<%= @page.page_number + 1 %>" phx-click="page-change"><%= icon %></a>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,34 +0,0 @@
|
|||
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,7 +27,8 @@ defmodule Shift73kWeb.EnsureRolePlug do
|
|||
def call(conn, roles) do
|
||||
user_token = get_session(conn, :user_token)
|
||||
|
||||
(user_token && Accounts.get_user_by_session_token(user_token))
|
||||
(user_token &&
|
||||
Accounts.get_user_by_session_token(user_token))
|
||||
|> has_role?(roles)
|
||||
|> maybe_halt(conn)
|
||||
end
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
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,100 +2,98 @@ defmodule Shift73kWeb.Router do
|
|||
use Shift73kWeb, :router
|
||||
import Shift73kWeb.UserAuth
|
||||
alias Shift73kWeb.EnsureRolePlug
|
||||
alias Shift73kWeb.EnsureUserExistPlug
|
||||
alias Shift73kWeb.EnsureAllowRegistrationPlug
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, {Shift73kWeb.LayoutView, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_current_user
|
||||
plug(:accepts, ["html"])
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_live_flash)
|
||||
plug(:put_root_layout, {Shift73kWeb.LayoutView, :root})
|
||||
plug(:protect_from_forgery)
|
||||
plug(:put_secure_browser_headers)
|
||||
plug(:fetch_current_user)
|
||||
end
|
||||
|
||||
pipeline :ensure_role_user do
|
||||
plug EnsureRolePlug, [:admin, :manager, :user]
|
||||
pipeline :api do
|
||||
plug(:accepts, ["json"])
|
||||
end
|
||||
|
||||
pipeline :ensure_user_exist do
|
||||
plug EnsureUserExistPlug
|
||||
pipeline :user do
|
||||
plug(EnsureRolePlug, [:admin, :manager, :user])
|
||||
end
|
||||
|
||||
pipeline :ensure_allow_registration do
|
||||
plug EnsureAllowRegistrationPlug
|
||||
pipeline :manager do
|
||||
plug(EnsureRolePlug, [:admin, :manager])
|
||||
end
|
||||
|
||||
pipeline :ensure_role_manager do
|
||||
plug EnsureRolePlug, [:admin, :manager]
|
||||
pipeline :admin do
|
||||
plug(EnsureRolePlug, :admin)
|
||||
end
|
||||
|
||||
pipeline :ensure_role_admin do
|
||||
plug EnsureRolePlug, :admin
|
||||
scope "/", Shift73kWeb do
|
||||
pipe_through([:browser])
|
||||
|
||||
get("/", Redirector, to: "/assign")
|
||||
end
|
||||
|
||||
# Enables the Swoosh mailbox preview in development.
|
||||
# Other scopes may use custom stacks.
|
||||
# scope "/api", Shift73kWeb do
|
||||
# pipe_through :api
|
||||
# end
|
||||
|
||||
# Enables LiveDashboard only for development
|
||||
#
|
||||
# Note that preview only shows emails that were sent by the same
|
||||
# node running the Phoenix server.
|
||||
if Mix.env() == :dev do
|
||||
scope "/dev" do
|
||||
pipe_through :browser
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
# you can use Plug.BasicAuth to set up some basic authentication
|
||||
# as long as you are also using SSL (which you should anyway).
|
||||
if Mix.env() in [:dev, :test] do
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
scope "/" do
|
||||
pipe_through(:browser)
|
||||
live_dashboard("/dashboard", metrics: Shift73kWeb.Telemetry)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", Shift73kWeb do
|
||||
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]
|
||||
pipe_through([:browser, :redirect_if_user_is_authenticated])
|
||||
|
||||
# session routes, irrelevant if user is authenticated
|
||||
get "/users/log_in", UserSessionController, :new
|
||||
post "/users/log_in", UserSessionController, :create
|
||||
get "/users/reset_password", UserResetPasswordController, :new
|
||||
post "/users/reset_password", UserResetPasswordController, :create
|
||||
get "/users/reset_password/:token", UserResetPasswordController, :edit
|
||||
get("/users/register", UserRegistrationController, :new)
|
||||
get("/users/log_in", UserSessionController, :new)
|
||||
post("/users/log_in", UserSessionController, :create)
|
||||
get("/users/reset_password", UserResetPasswordController, :new)
|
||||
post("/users/reset_password", UserResetPasswordController, :create)
|
||||
get("/users/reset_password/:token", UserResetPasswordController, :edit)
|
||||
end
|
||||
|
||||
scope "/", Shift73kWeb do
|
||||
pipe_through [:browser, :require_authenticated_user]
|
||||
pipe_through([:browser, :require_authenticated_user])
|
||||
|
||||
# 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
|
||||
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||
get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
|
||||
end
|
||||
|
||||
scope "/", Shift73kWeb do
|
||||
pipe_through [:browser, :ensure_user_exist]
|
||||
pipe_through([:browser])
|
||||
|
||||
# session paths
|
||||
delete "/users/log_out", UserSessionController, :delete
|
||||
get "/users/force_logout", UserSessionController, :force_logout
|
||||
get "/users/confirm", UserConfirmationController, :new
|
||||
post "/users/confirm", UserConfirmationController, :create
|
||||
get "/users/confirm/:token", UserConfirmationController, :confirm
|
||||
delete("/users/log_out", UserSessionController, :delete)
|
||||
get("/users/force_logout", UserSessionController, :force_logout)
|
||||
get("/users/confirm", UserConfirmationController, :new)
|
||||
post("/users/confirm", UserConfirmationController, :create)
|
||||
get("/users/confirm/:token", UserConfirmationController, :confirm)
|
||||
|
||||
# ics/ical route for user's shifts
|
||||
get "/ics/:slug", UserShiftsIcsController, :index
|
||||
get("/ics/:slug", UserShiftsIcsController, :index)
|
||||
end
|
||||
|
||||
scope "/", Shift73kWeb do
|
||||
pipe_through [:browser, :require_authenticated_user, :ensure_role_user]
|
||||
pipe_through([:browser, :require_authenticated_user, :user])
|
||||
|
||||
live "/templates", ShiftTemplateLive.Index, :index
|
||||
live "/templates/new", ShiftTemplateLive.Index, :new
|
||||
|
@ -113,16 +111,16 @@ defmodule Shift73kWeb.Router do
|
|||
end
|
||||
|
||||
# scope "/", Shift73kWeb do
|
||||
# pipe_through([:browser, :require_authenticated_user, :ensure_role_admin])
|
||||
# pipe_through([:browser, :require_authenticated_user, :admin])
|
||||
# end
|
||||
|
||||
# Users Management
|
||||
scope "/users", Shift73kWeb do
|
||||
pipe_through [:browser, :require_authenticated_user, :ensure_role_manager, :require_email_confirmed]
|
||||
pipe_through([:browser, :require_authenticated_user, :manager, :require_email_confirmed])
|
||||
|
||||
live "/", UserManagementLive.Index, :index
|
||||
live "/new", UserManagementLive.Index, :new
|
||||
live "/edit/:id", UserManagementLive.Index, :edit
|
||||
live("/", UserManagementLive.Index, :index)
|
||||
live("/new", UserManagementLive.Index, :new)
|
||||
live("/edit/:id", UserManagementLive.Index, :edit)
|
||||
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
||||
end
|
||||
end
|
||||
|
|
88
lib/shift73k_web/templates/layout/_navbar.html.eex
Normal file
88
lib/shift73k_web/templates/layout/_navbar.html.eex
Normal file
|
@ -0,0 +1,88 @@
|
|||
<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>
|
|
@ -1,70 +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 %>
|
||||
<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>
|
|
@ -1,10 +0,0 @@
|
|||
<%= 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 ">
|
||||
<%= for {kind, class} <- alert_kinds() 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 %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
|
@ -5,7 +5,7 @@
|
|||
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
||||
<%= for {kind, class} <- alert_kinds() 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 %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
<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,24 +1,28 @@
|
|||
<li class="nav-item dropdown">
|
||||
|
||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownShiftsMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-calendar2 me-1"></i> Shifts
|
||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline me-1"] %>
|
||||
Shifts
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownShiftsMenu">
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
||||
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||
<%= icon_div @conn, "bi-calendar2-plus", [class: "icon baseline me-1"] %>
|
||||
Schedule Shifts
|
||||
<% 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
|
||||
<%= icon_div @conn, "bi-card-list", [class: "icon baseline me-1"] %>
|
||||
My Scheduled Shifts
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= 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"] %>
|
||||
My Shift Templates
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
|
@ -26,12 +30,14 @@
|
|||
<%# user_shifts_csv_path %>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %>
|
||||
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline me-1"] %>
|
||||
CSV Export
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.shift_import_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||
<i class="bi bi-box-arrow-in-left me-1"></i> iCal Import
|
||||
<%= icon_div @conn, "bi-box-arrow-in-left", [class: "icon baseline me-1"] %>
|
||||
iCal Import
|
||||
<% end %>
|
||||
</li>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<li class="nav-item dropdown">
|
||||
|
||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle me-1"></i> Hello!
|
||||
<%= icon_div @conn, "bi-person-circle", [class: "icon baseline me-1"] %>
|
||||
<%= @current_user && "Hello!" || "Hello?" %>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
||||
|
@ -11,19 +12,22 @@
|
|||
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||
<i class="bi bi-people me-1"></i> Users
|
||||
<%= icon_div @conn, "bi-people", [class: "icon baseline me-1"] %>
|
||||
Users
|
||||
<% end %>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
||||
<i class="bi bi-sliders me-1"></i> Settings
|
||||
<%= icon_div @conn, "bi-sliders", [class: "icon baseline me-1"] %>
|
||||
Settings
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
|
||||
<i class="bi bi-door-closed me-1"></i> Log out
|
||||
<%= icon_div @conn, "bi-door-closed", [class: "icon baseline me-1"] %>
|
||||
Log out
|
||||
<% end %>
|
||||
</li>
|
||||
|
|
@ -1,30 +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() %>
|
||||
<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>
|
32
lib/shift73k_web/templates/layout/root.html.leex
Normal file
32
lib/shift73k_web/templates/layout/root.html.leex
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!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,7 +2,8 @@
|
|||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-arrow-repeat me-1"></i> Resend confirmation instructions
|
||||
<%= icon_div @conn, "bi-arrow-repeat", [class: "icon baseline"] %>
|
||||
Resend confirmation instructions
|
||||
</h2>
|
||||
<p class="lead">We'll send you another email with instructions to confirm your email address.</p>
|
||||
|
||||
|
@ -10,7 +11,7 @@
|
|||
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= email_input f, :email,
|
||||
value: @current_user && @current_user.email || "",
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
|
@ -29,9 +30,6 @@
|
|||
<% end %>
|
||||
|
||||
<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 "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||
</p>
|
|
@ -2,7 +2,8 @@
|
|||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-patch-question me-1"></i> Forgot your password?
|
||||
<%= icon_div @conn, "mdi-head-question-outline", [class: "icon baseline"] %>
|
||||
Forgot your password?
|
||||
</h2>
|
||||
<p class="lead">We'll send you an email with instructions to reset your password.</p>
|
||||
|
||||
|
@ -10,7 +11,7 @@
|
|||
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= email_input f, :email,
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
class: "form-control",
|
||||
|
@ -27,9 +28,7 @@
|
|||
<% end %>
|
||||
|
||||
<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 "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||
</p>
|
||||
|
|
@ -2,13 +2,14 @@
|
|||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-door-open me-1"></i> Log in
|
||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
||||
Log in
|
||||
</h2>
|
||||
<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 -> %>
|
||||
<%= if @error_message do %>
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-4" role="alert">
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<%= @error_message %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
@ -16,12 +17,11 @@
|
|||
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= email_input f, :email,
|
||||
class: "form-control",
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
maxlength: User.max_email,
|
||||
autofocus: true,
|
||||
required: true
|
||||
%>
|
||||
<span class="invalid-feedback">must be a valid email address</span>
|
||||
|
@ -29,7 +29,7 @@
|
|||
|
||||
<%= label f, :password, class: "form-label" %>
|
||||
<div class="inner-addon left-addon mb-3">
|
||||
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||
<%= icon_div @conn, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
||||
<%= password_input f, :password,
|
||||
class: "form-control",
|
||||
required: true
|
||||
|
@ -49,9 +49,7 @@
|
|||
<% end %>
|
||||
|
||||
<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 "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
||||
</p>
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
<div class="col-12 col-md-10 col-xl-8">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline"] %>
|
||||
CSV Export
|
||||
</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>
|
||||
|
||||
|
@ -26,7 +27,6 @@
|
|||
value: Date.beginning_of_month(today),
|
||||
min: min_date,
|
||||
max: max_date,
|
||||
required: true,
|
||||
class: "form-control"
|
||||
%>
|
||||
</div>
|
||||
|
@ -37,7 +37,6 @@
|
|||
value: Date.end_of_month(today),
|
||||
min: min_date,
|
||||
max: max_date,
|
||||
required: true,
|
||||
class: "form-control"
|
||||
%>
|
||||
</div>
|
|
@ -2,7 +2,8 @@
|
|||
<div class="col-12 col-md-10 col-xl-8">
|
||||
|
||||
<h2>
|
||||
<i class="bi bi-calendar2 me-1"></i> User Shifts ICS
|
||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline"] %>
|
||||
User Shifts ICS
|
||||
</h2>
|
||||
<p class="lead">Shifts for user: <%= @user.email %></p>
|
||||
<p>Calendar slug: <%= @slug %></p>
|
38
lib/shift73k_web/views/icon_helpers.ex
Normal file
38
lib/shift73k_web/views/icon_helpers.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
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,20 +1,9 @@
|
|||
defmodule Shift73kWeb.LayoutView do
|
||||
use Shift73kWeb, :view
|
||||
import Shift73k, only: [get_app_allow_reg: 0]
|
||||
alias Shift73k.Repo
|
||||
|
||||
alias Shift73k.Accounts.User
|
||||
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
|
||||
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
||||
false -> opts
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
defmodule Shift73kWeb.UserConfirmationView do
|
||||
use Shift73kWeb, :view
|
||||
import Shift73k, only: [get_app_allow_reg: 0]
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
def allow_registration, do: get_app_allow_reg()
|
||||
end
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
defmodule Shift73kWeb.UserResetPasswordView do
|
||||
use Shift73kWeb, :view
|
||||
import Shift73k, only: [get_app_allow_reg: 0]
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
def allow_registration, do: get_app_allow_reg()
|
||||
end
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
defmodule Shift73kWeb.UserSessionView do
|
||||
use Shift73kWeb, :view
|
||||
import Shift73k, only: [get_app_allow_reg: 0]
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
def allow_registration, do: get_app_allow_reg()
|
||||
end
|
||||
|
|
42
mix.exs
42
mix.exs
|
@ -4,10 +4,10 @@ defmodule Shift73k.MixProject do
|
|||
def project do
|
||||
[
|
||||
app: :shift73k,
|
||||
version: "0.2.1",
|
||||
elixir: "~> 1.12",
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.7",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: Mix.compilers(),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
|
@ -33,30 +33,33 @@ defmodule Shift73k.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:bcrypt_elixir, "~> 3.0"},
|
||||
{:phoenix, "~> 1.6.11"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:bcrypt_elixir, "~> 2.0"},
|
||||
{:phoenix, "~> 1.5.8"},
|
||||
{:phoenix_ecto, "~> 4.1"},
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_live_view, "~> 0.15.0"},
|
||||
{:floki, ">= 0.27.0", only: :test},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.18"},
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:swoosh, "~> 1.7"},
|
||||
{:gen_smtp, "~> 1.2"},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:phoenix_live_dashboard, "~> 0.4"},
|
||||
{:telemetry_metrics, "~> 0.4"},
|
||||
{:telemetry_poller, "~> 0.4"},
|
||||
{:gettext, "~> 0.11"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:bamboo, "~> 2.0"},
|
||||
{:bamboo_smtp, "~> 4.0"},
|
||||
{:scrivener_ecto, "~> 2.0"},
|
||||
{:tzdata, "~> 1.1"},
|
||||
{:nimble_csv, "~> 1.0"},
|
||||
{:icalendar, "~> 1.1"},
|
||||
{:httpoison, "~> 2.0"},
|
||||
{:httpoison, "~> 1.7"},
|
||||
|
||||
# Additional packages
|
||||
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false}
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:sobelow, "~> 0.8", only: :dev}
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -71,8 +74,7 @@ defmodule Shift73k.MixProject do
|
|||
setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
|
||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||
"assets.deploy": ["cmd npm --prefix assets run build", "phx.digest"]
|
||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
88
mix.lock
88
mix.lock
|
@ -1,56 +1,60 @@
|
|||
%{
|
||||
"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"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"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.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.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"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.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"},
|
||||
"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_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"},
|
||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
||||
"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"},
|
||||
"db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"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_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"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
|
||||
"ecto": {:hex, :ecto, "3.5.8", "8ebf12be6016cb99313348ba7bb4612f4114b9a506d6da79a2adc7ef449340bc", [: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", "ea0be182ea8922eb7742e3ae8e71b67ee00ae177de1bf76210299a5f16ba4c77"},
|
||||
"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.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.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", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
|
||||
"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.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
|
||||
"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"},
|
||||
"floki": {:hex, :floki, "0.30.1", "75d35526d3a1459920b6e87fdbc2e0b8a3670f965dd0903708d2b267e0904c55", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e9c03524447d1c4cbfccd672d739b8c18453eee377846b119d4fd71b1a176bb8"},
|
||||
"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"},
|
||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
||||
"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"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"httpoison": {:hex, :httpoison, "2.0.0", "d38b091f5e481e45cc700aba8121ce49b66d348122a097c9fbc2dc6876d88090", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "f1253bf455be73a4c3f6ae3407e7e3cf6fc91934093e056d737a0566126e2930"},
|
||||
"icalendar": {:hex, :icalendar, "1.1.2", "5d0afff5d0143c5bd43f18ae32a777bf0fb9a724543ab05229a460d368f0a5e7", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2060f8e353fdf3047e95a3f012583dc3c0bbd7ca1010e32ed9e9fc5760ad4292"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
|
||||
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
||||
"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"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
|
||||
"nimble_csv": {:hex, :nimble_csv, "1.1.0", "b1dba4a86be9e03065c9de829050468e591f569100332db949e7ce71be0afc25", [:mix], [], "hexpm", "e986755bc302832cac429be6deda0fc9d82d3c82b47abefb68b3c17c9d949a3f"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"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.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, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"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_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_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"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"},
|
||||
"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.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.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.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"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.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||
"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_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_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
||||
"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.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.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.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||
"phx_gen_auth": {:hex, :phx_gen_auth, "0.6.0", "4ffbfa5b34ad8178c3dfcb996fed776df425903595cbc8d56a9ae5bc53136810", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9a801c0f0bc251d8d91d62cecba0ebb6a90b8580fa8843029d931d15164e6ad9"},
|
||||
"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_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_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"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"},
|
||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
||||
"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"},
|
||||
"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, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"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, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"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.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"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"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||
"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"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
|
||||
"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", "")
|
||||
|
||||
create table(:users, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :email, :citext, null: false
|
||||
add :hashed_password, :string, null: false
|
||||
add :role, :string, null: false
|
||||
add :confirmed_at, :naive_datetime
|
||||
add :week_start_at, :string, null: false
|
||||
add :calendar_slug, :string, null: false
|
||||
add(:id, :binary_id, primary_key: true)
|
||||
add(:email, :citext, null: false)
|
||||
add(:hashed_password, :string, null: false)
|
||||
add(:role, :string, null: false)
|
||||
add(:confirmed_at, :naive_datetime)
|
||||
add(:week_start_at, :string, null: false)
|
||||
add(:calendar_slug, :string, null: false)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:users, [:email, :calendar_slug])
|
||||
create(unique_index(:users, [:email, :calendar_slug]))
|
||||
|
||||
create table(:users_tokens, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
|
||||
add :token, :binary, null: false
|
||||
add :context, :string, null: false
|
||||
add :sent_to, :string
|
||||
add(:id, :binary_id, primary_key: true)
|
||||
add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false)
|
||||
add(:token, :binary, null: false)
|
||||
add(:context, :string, null: false)
|
||||
add(:sent_to, :string)
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
||||
create index(:users_tokens, [:user_id])
|
||||
create unique_index(:users_tokens, [:context, :token])
|
||||
create(index(:users_tokens, [:user_id]))
|
||||
create(unique_index(:users_tokens, [:context, :token]))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Shift73k.Repo.Migrations.AddUserDefaultShiftColumn do
|
|||
|
||||
def change 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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue