Compare commits

...
Sign in to create a new pull request.

51 commits

Author SHA1 Message Date
25a2e08834
chore: formatting
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-04 00:58:18 +02:00
59f18e1fd2
WIP feat:translation
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-04 00:48:15 +02:00
c251e1dba3
fix: adapt layout to phoenix 1.8
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-03 21:29:01 +02:00
fb9a3cd063
chore: update phoenix to version 1.8.0-rc.3 2025-07-03 21:28:33 +02:00
5b6e5713ec
chore: add build-tailwind commant to just 2025-07-03 19:45:04 +02:00
9f1b7eefe9
feat: add backpex layout 2025-07-03 18:18:10 +02:00
fe1c419fa7
feat: add backpex router 2025-07-03 18:18:09 +02:00
beb95dd37e
feat: add member live resource 2025-07-03 18:18:09 +02:00
6dd997e3e1
feat: add member resource 2025-07-03 18:04:43 +02:00
a958b49a7c
feat: add dasyui for backpex 2025-07-03 16:37:54 +02:00
5d0e8b5f78
feat: add backpex (config, js hooks, deps, css, formatter) 2025-07-03 13:56:50 +02:00
b05cd7db77
rename rauthy-test to rauthy-dev 2025-07-03 13:32:14 +02:00
c43ed10fc4
review(env): shift secret to env file 2025-07-03 13:30:30 +02:00
bbb5fa0c69
feat(oicd_provider): added oicd provider rauthy 2025-07-03 13:25:52 +02:00
380ee9c5d7
Revert "fix(ci): Dont install dependencies again in test step"
This reverts commit d54b226be5.
2025-07-03 13:11:13 +02:00
Renovate Bot
ef141665a1
chore(deps): update renovate/renovate docker tag to v40.62 2025-07-03 13:11:03 +02:00
Renovate Bot
b84ab8ad2a
chore(deps): update renovate/renovate docker tag to v40.60 2025-07-03 13:07:11 +02:00
efecee6176
fix(ci): Dont install dependencies again in test step 2025-07-03 13:06:55 +02:00
b19a6c1d91
Add CI cache 2025-07-03 13:06:45 +02:00
6151b433c4
Fix postgres port in CI 2025-07-03 13:06:38 +02:00
1bdac57d67
tidewave 2025-07-03 13:05:41 +02:00
46c79ac512
fix(tests) Make tests work with docker-based postgres 2025-07-03 12:58:18 +02:00
Renovate Bot
66b094e3d5
chore(deps): update postgres to v17.5 2025-07-03 12:58:01 +02:00
1a36aa32fe
fix(ci): ignore elixir updates for .drone.yml as well 2025-07-03 12:57:50 +02:00
Renovate Bot
617a6fcac0
chore(deps): update renovate/renovate docker tag to v40.51 2025-07-03 12:57:35 +02:00
Renovate Bot
1c9ab4f64e
chore(deps): update renovate/renovate docker tag to v40.49 2025-07-03 12:57:23 +02:00
Renovate Bot
786f9d34dd
chore(deps): update dependency erlang to v27.3.4 2025-07-03 12:57:14 +02:00
ff5dcf1925
fix docker version warning 2025-07-03 12:57:00 +02:00
cf5ccf238d
chore(Justfile): allow regenerating migrations by commit hash 2025-07-03 12:53:04 +02:00
48353f8559
put regen-migrations into Justfile 2025-07-03 12:52:29 +02:00
2814fd11ea
chore: add regen_migrations script and seed-database to Justfile 2025-07-03 12:48:48 +02:00
b9984a0249
fix(ci): Explicitly pass github token to renovate job 2025-07-03 12:48:40 +02:00
33ae458121
Replace accidental podman command with docker 2025-07-03 12:48:11 +02:00
Renovate Bot
8b3576157f
chore(deps): update renovate/renovate docker tag to v40 2025-07-03 12:47:40 +02:00
b7e3037ea9
fix(renovate): Exclude elixir dependencies from postgres update 2025-07-03 12:47:30 +02:00
2f901a23c3
fix: Elixir version matching in renovate config 2025-07-03 12:47:24 +02:00
b6346e3c32
chore: Remove variables from dockerfile base images to enable renovate updates 2025-07-03 12:47:18 +02:00
55e89c880a
chore(renovate): Use glob patterns to match postgres packages 2025-07-03 12:45:27 +02:00
58f11277f8
chore: Also run renovate on push to main branch 2025-07-03 12:45:18 +02:00
281e6fd19b
chore: Group renovate postgres updates 2025-07-03 12:45:12 +02:00
4f3fb1bb4b
chore: Remove renovate comment about postgresql in asdf
Since postgres is now handled via docker-compose instead of asdf, the
restriction mentioned in the comment does not apply anymore.
2025-07-03 12:44:57 +02:00
d620767de2
chore: Ignore elixir dependency in renovate
It's a bit complicated to support the `-otp` postfix right now, so to
fix this right now, we'll just disable automatic updates for elixir.
2025-07-03 12:44:47 +02:00
7ddc7aa7b6
Add Release scripts & Dockerfile 2025-07-03 12:44:34 +02:00
f24ac1e41a
chore: add docker-compose for local postgres container 2025-07-03 12:43:42 +02:00
30c070a15a
chore(drone): don't trigger renovate on each push 2025-07-03 12:43:10 +02:00
Renovate Bot
623ed38481
Update renovate/renovate Docker tag to v39.264 2025-07-03 12:42:46 +02:00
Renovate Bot
9243901896
configure renovate 2025-07-03 12:41:13 +02:00
39eac1aa54
Disable ModuleDoc check
Adding module documentation for all our Ash resources seems desirable,
but ultimately unrealistic, and I think it's more likely that this check
will result in one-liner documentation that doesn't contain any real
information.
2025-07-03 12:40:48 +02:00
4fd2dd4c48
Add default generated credo config 2025-07-03 12:37:49 +02:00
61db0e3bd4
Add just format task 2025-07-03 12:37:32 +02:00
7b5f37960f
Add basic CI setup (#30)
Co-authored-by: Rafael Epplée <hello@rafa.ee>
Reviewed-on: #30
Co-authored-by: Moritz <moritz.m@local-it.org>
Co-committed-by: Moritz <moritz.m@local-it.org>
2025-07-03 12:36:15 +02:00
57 changed files with 3947 additions and 685 deletions

218
.credo.exs Normal file
View file

@ -0,0 +1,218 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"lib/",
"src/",
"test/",
"web/",
"apps/*/lib/",
"apps/*/src/",
"apps/*/test/",
"apps/*/web/"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: false,
#
# To modify the timeout for parsing files, change this value:
#
parse_timeout: 5000,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.UnsafeExec, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.WrongTestFileExtension, []}
],
disabled: [
# Checks disabled by the Mitgliederverwaltung Team
{Credo.Check.Readability.ModuleDoc, []},
#
# Checks scheduled for next check update (opt-in for now)
{Credo.Check.Refactor.UtcNowTruncate, []},
#
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
# and be sure to use `mix credo --strict` to see low priority checks)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.UnusedVariableNames, []},
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
{Credo.Check.Readability.OneArityFunctionInPipe, []},
{Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []},
{Credo.Check.Refactor.PipeChainStart, []},
{Credo.Check.Refactor.RejectFilter, []},
{Credo.Check.Refactor.VariableRebinding, []},
{Credo.Check.Warning.LazyLogging, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
}
]
}

45
.dockerignore Normal file
View file

@ -0,0 +1,45 @@
# This file excludes paths from the Docker build context.
#
# By default, Docker's build context includes all files (and folders) in the
# current directory. Even if a file isn't copied into the container it is still sent to
# the Docker daemon.
#
# There are multiple reasons to exclude files from the build context:
#
# 1. Prevent nested folders from being copied into the container (ex: exclude
# /assets/node_modules when copying /assets)
# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
# 3. Avoid sending files containing sensitive information
#
# More information on using .dockerignore is available here:
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
.dockerignore
# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
#
# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
.git
!.git/HEAD
!.git/refs
# Common development/test artifacts
/cover/
/doc/
/test/
/tmp/
.elixir_ls
# Mix artifacts
/_build/
/deps/
*.ez
# Generated on crash by the VM
erl_crash.dump
# Static artifacts - These should be fetched and built inside the Docker image
/assets/node_modules/
/priv/static/assets/
/priv/static/cache_manifest.json

View file

@ -1,10 +1,132 @@
kind: pipeline
type: docker
name: default
name: check
services:
- name: postgres
image: docker.io/library/postgres:17.5
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
trigger:
event:
- push
steps:
- name: greeting
image: alpine
commands:
- echo hello
- echo world
- name: compute cache key
image: docker.io/library/elixir:1.18.3-otp-27
commands:
- mix_lock_hash=$(sha256sum mix.lock | cut -d ' ' -f 1)
- echo "$DRONE_REPO_OWNER/$DRONE_REPO_NAME/$mix_lock_hash" >> .cache_key
# Print cache key for debugging
- cat .cache_key
- name: restore-cache
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- ./deps
- ./_build
ttl: 30
volumes:
- name: cache
path: /cache
- name: lint
image: docker.io/library/elixir:1.18.3-otp-27
commands:
# Install hex package manager
- mix local.hex --force
# Fetch dependencies
- mix deps.get
# Check for compilation errors & warnings
- mix compile --warnings-as-errors
# Check formatting
- mix format --check-formatted
# Security checks
- mix sobelow --config
# Check dependencies for known vulnerabilities
- mix deps.audit
# Check for dependencies that are not maintained anymore
- mix hex.audit
# Provide hints for improving code quality
- mix credo
- name: wait_for_postgres
image: docker.io/library/postgres:17.5
commands:
# Wait for postgres to become available
- |
for i in {1..20}; do
if pg_isready -h postgres -U postgres; then
exit 0
else
true
fi
sleep 2
done
echo "Postgres did not become available, aborting."
exit 1
- name: test
image: docker.io/library/elixir:1.18.3-otp-27
environment:
MIX_ENV: test
TEST_POSTGRES_HOST: postgres
TEST_POSTGRES_PORT: 5432
commands:
# Install hex package manager
- mix local.hex --force
# Fetch dependencies
- mix deps.get
# Run tests
- mix test
- name: rebuild-cache
image: drillster/drone-volume-cache
settings:
rebuild: true
mount:
- ./deps
- ./_build
volumes:
- name: cache
path: /cache
volumes:
- name: cache
host:
path: /tmp/drone_cache
---
kind: pipeline
type: docker
name: renovate
trigger:
event:
- cron
- custom
- push
branch:
- main
environment:
LOG_LEVEL: debug
steps:
- name: renovate
image: renovate/renovate:40.62
environment:
RENOVATE_CONFIG_FILE: "renovate_backend_config.js"
RENOVATE_TOKEN:
from_secret: RENOVATE_TOKEN
GITHUB_COM_TOKEN:
from_secret: GITHUB_COM_TOKEN
commands:
# https://github.com/renovatebot/renovate/discussions/15049
- unset GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
- renovate-config-validator
- renovate

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
[*.yml]
indent_size = 2
[*.json]
indent_size = 2

View file

@ -1,5 +1,5 @@
[
import_deps: [:ecto, :ecto_sql, :phoenix],
import_deps: [:ecto, :ecto_sql, :phoenix, :backpex],
subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]

4
.gitignore vendored
View file

@ -35,3 +35,7 @@ mv-*.tar
npm-debug.log
/assets/node_modules/
.cursor
# Ignore the .env file with env variables
.env

13
.sobelow-conf Normal file
View file

@ -0,0 +1,13 @@
[
verbose: false,
private: false,
skip: false,
router: nil,
exit: false,
format: "txt",
out: nil,
threshold: :low,
ignore: ["Config.CSP"],
ignore_files: [],
version: false,
]

View file

@ -1,4 +1,3 @@
elixir 1.18.3-otp-27
postgres 17.2
erlang 27.3
erlang 27.3.4
just 1.40.0

93
Dockerfile Normal file
View file

@ -0,0 +1,93 @@
# 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-20250317-slim - for the release image
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim
#
ARG BUILDER_IMAGE="hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim"
ARG RUNNER_IMAGE="debian:bullseye-20250317-slim"
FROM ${BUILDER_IMAGE} as builder
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# 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
# 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 ca-certificates \
&& 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/mv ./
USER nobody
# If using an environment that doesn't automatically reap zombie processes, it is
# advised to add an init process such as tini via `apt-get install`
# above and adding an entrypoint. See https://github.com/krallin/tini for details
# ENTRYPOINT ["/tini", "--"]
CMD ["/app/bin/server"]

View file

@ -1,8 +1,82 @@
run: install-dependencies migrate-database
set dotenv-load := true
run: install-dependencies start-database migrate-database seed-database build-tailwind
mix phx.server
install-dependencies:
mix deps.get
migrate-database:
mix ecto.create
mix ecto.create
reset-database:
mix ecto.reset
seed-database:
mix run priv/repo/seeds.exs
start-database:
docker compose up -d
build-tailwind:
mix tailwind mv
gettext:
mix gettext.extract
mix gettext.merge priv/gettext
ci-dev: lint audit test
lint:
mix format --check-formatted
mix compile --warnings-as-errors
mix credo
audit:
mix sobelow --config
mix deps.audit
mix hex.audit
test: install-dependencies start-database
mix test
format:
mix format
build-docker-container:
docker build --tag mitgliederverwaltung .
# This is meant for debugging the container build process only.
run-docker-container: build-docker-container
docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung
# Usage:
# just regen-migrations migration_name [commit_hash]
# If commit_hash is given, rollback & delete the migrations from that commit.
# Otherwise, rollback & delete all untracked migrations.
regen-migrations migration_name commit_hash='':
#!/usr/bin/env bash
set -euo pipefail
# Pick migrations either from the given commit or untracked files
if [ -n "{{commit_hash}}" ]; then
echo "→ Rolling back migrations from commit {{commit_hash}}"
MIG_FILES=$(git show --name-only --pretty=format: "{{commit_hash}}" \
| grep -E "^priv/repo/migrations/|^priv/resource_snapshots")
else
echo "→ Rolling back all untracked migrations"
MIG_FILES=$(git ls-files --others priv/repo/migrations)
fi
# Roll back in Ash
COUNT=$(echo "$MIG_FILES" | wc -l)
mix ash_postgres.rollback -n "$COUNT"
# Remove the migration files
echo removing $MIG_FILES
echo "$MIG_FILES" | xargs rm -f
# Also clean up any untracked resource snapshots
git ls-files --others priv/resource_snapshots | xargs rm -f
# Generate a fresh migration
mix ash.codegen --name "{{migration_name}}"

View file

@ -1,2 +1,18 @@
# mitgliederverwaltung
# Mv
To start your Phoenix server:
* Run `mix setup` to install and setup dependencies
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## Learn more
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix

View file

@ -1,5 +1,104 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
/* See the Tailwind configuration guide for advanced usage
https://tailwindcss.com/docs/configuration */
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../lib/mv_web";
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
The heroicons installation itself is managed by your mix.exs */
@plugin "../vendor/heroicons";
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
@plugin "../vendor/daisyui" {
themes: false;
}
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
@plugin "../vendor/daisyui-theme" {
name: "dark";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1);
--color-base-300: oklch(20.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(58% 0.233 277.117);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(58% 0.233 277.117);
--color-secondary-content: oklch(96% 0.018 272.314);
--color-accent: oklch(60% 0.25 292.717);
--color-accent-content: oklch(96% 0.016 293.756);
--color-neutral: oklch(37% 0.044 257.287);
--color-neutral-content: oklch(98% 0.003 247.858);
--color-info: oklch(58% 0.158 241.966);
--color-info-content: oklch(97% 0.013 236.62);
--color-success: oklch(60% 0.118 184.704);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
@plugin "../vendor/daisyui-theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(98% 0 0);
--color-base-200: oklch(96% 0.001 286.375);
--color-base-300: oklch(92% 0.004 286.32);
--color-base-content: oklch(21% 0.006 285.885);
--color-primary: oklch(70% 0.213 47.604);
--color-primary-content: oklch(98% 0.016 73.684);
--color-secondary: oklch(55% 0.027 264.364);
--color-secondary-content: oklch(98% 0.002 247.839);
--color-accent: oklch(0% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(44% 0.017 285.786);
--color-neutral-content: oklch(98% 0 0);
--color-info: oklch(62% 0.214 259.815);
--color-info-content: oklch(97% 0.014 254.604);
--color-success: oklch(70% 0.14 182.503);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session] { display: contents }
/* This file is for your main application CSS */
@source "../../deps/backpex/**/*.*ex";
@source '../../deps/backpex/assets/js/**/*.*js'

View file

@ -14,6 +14,8 @@
//
// import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
@ -21,9 +23,10 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import { Hooks as BackpexHooks } from 'backpex'
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
})
@ -42,3 +45,38 @@ liveSocket.connect()
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
// The lines below enable quality of life phoenix_live_reload
// development features:
//
// 1. stream server logs to the browser console
// 2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// Enable server log streaming to client.
// Disable with reloader.disableServerLogs()
reloader.enableServerLogs()
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//
// * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", e => keyDown = null)
window.addEventListener("click", e => {
if(keyDown === "c"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
} else if(keyDown === "d"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
}
}, true)
window.liveReloader = reloader
})
}

View file

@ -1,74 +0,0 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = {
content: [
"./js/**/*.js",
"../lib/mv_web.ex",
"../lib/mv_web/**/*.*ex"
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
},
},
plugins: [
require("@tailwindcss/forms"),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
// <div class="phx-click-loading:animate-ping">
//
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
plugin(function({matchComponents, theme}) {
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
["-micro", "/16/solid"]
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
})
})
matchComponents({
"hero": ({name, fullPath}) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
let size = theme("spacing.6")
if (name.endsWith("-mini")) {
size = theme("spacing.5")
} else if (name.endsWith("-micro")) {
size = theme("spacing.4")
}
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
"mask": `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
"display": "inline-block",
"width": size,
"height": size
}
}
}, {values})
})
]
}

124
assets/vendor/daisyui-theme.js vendored Normal file

File diff suppressed because one or more lines are too long

1021
assets/vendor/daisyui.js vendored Normal file

File diff suppressed because one or more lines are too long

43
assets/vendor/heroicons.js vendored Normal file
View file

@ -0,0 +1,43 @@
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = plugin(function({matchComponents, theme}) {
let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
["-micro", "/16/solid"]
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
})
})
matchComponents({
"hero": ({name, fullPath}) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
content = encodeURIComponent(content)
let size = theme("spacing.6")
if (name.endsWith("-mini")) {
size = theme("spacing.5")
} else if (name.endsWith("-micro")) {
size = theme("spacing.4")
}
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
"mask": `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
"display": "inline-block",
"width": size,
"height": size
}
}
}, {values})
})

View file

@ -1,39 +1,12 @@
/**
* @license MIT
* topbar 2.0.0, 2023-02-04
* https://buunguyen.github.io/topbar
* Copyright (c) 2021 Buu Nguyen
* topbar 3.0.0
* http://buunguyen.github.io/topbar
* Copyright (c) 2024 Buu Nguyen
*/
(function (window, document) {
"use strict";
// https://gist.github.com/paulirish/1579671
(function () {
var lastTime = 0;
var vendors = ["ms", "moz", "webkit", "o"];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame =
window[vendors[x] + "RequestAnimationFrame"];
window.cancelAnimationFrame =
window[vendors[x] + "CancelAnimationFrame"] ||
window[vendors[x] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function (callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function () {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
})();
var canvas,
currentProgress,
showing,
@ -88,7 +61,6 @@
style.zIndex = 100001;
style.display = "none";
if (options.className) canvas.classList.add(options.className);
document.body.appendChild(canvas);
addEvent(window, "resize", repaint);
},
topbar = {
@ -101,10 +73,11 @@
if (delay) {
if (delayTimerId) return;
delayTimerId = setTimeout(() => topbar.show(), delay);
} else {
} else {
showing = true;
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
if (!canvas) createCanvas();
if (!canvas.parentElement) document.body.appendChild(canvas);
canvas.style.opacity = 1;
canvas.style.display = "block";
topbar.progress(0);

View file

@ -11,6 +11,12 @@ config :mv,
ecto_repos: [Mv.Repo],
generators: [timestamp_type: :utc_datetime]
# Backpex configuration
config :backpex,
pubsub_server: Mv.PubSub,
translator_function: {MvWeb.CoreComponents, :translate_backpex},
error_translator_function: {MvWeb.CoreComponents, :translate_error}
# Configures the endpoint
config :mv, MvWeb.Endpoint,
url: [host: "localhost"],
@ -22,6 +28,10 @@ config :mv, MvWeb.Endpoint,
pubsub_server: Mv.PubSub,
live_view: [signing_salt: "76NHxpwt"]
# Configures translation
config :gettext, :locales, ["en", "de"]
config :gettext, :default_locale, "de"
# Configures the mailer
#
# By default it uses the "Local" adapter which stores the emails
@ -36,25 +46,24 @@ config :esbuild,
version: "0.17.11",
mv: [
args:
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
# Configure tailwind (the version is required)
config :tailwind,
version: "3.4.3",
version: "4.0.9",
mv: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
--input=assets/css/app.css
--output=priv/static/assets/css/app.css
),
cd: Path.expand("../assets", __DIR__)
cd: Path.expand("..", __DIR__)
]
# Configures Elixir's Logger
config :logger, :console,
config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

View file

@ -5,6 +5,7 @@ config :mv, Mv.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
port: 5000,
database: "mv_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
@ -16,10 +17,10 @@ config :mv, Mv.Repo,
# The watchers configuration can be used to run external
# watchers to your application. For example, we can use it
# to bundle .js and .css sources.
# Binding to loopback ipv4 address prevents access from other machines.
config :mv, MvWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {127, 0, 0, 1}, port: 4000],
http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")],
check_origin: false,
code_reloader: true,
debug_errors: true,
@ -55,10 +56,11 @@ config :mv, MvWeb.Endpoint,
# Watch static and templates for browser reloading.
config :mv, MvWeb.Endpoint,
live_reload: [
web_console_logger: true,
patterns: [
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/mv_web/(controllers|live|components)/.*(ex|heex)$"
~r"lib/mv_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$"
]
]
@ -66,7 +68,7 @@ config :mv, MvWeb.Endpoint,
config :mv, dev_routes: true
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
config :logger, :default_formatter, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
@ -76,7 +78,8 @@ config :phoenix, :stacktrace_depth, 20
config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
# Include HEEx debug annotations as HTML comments in rendered markup
# Include HEEx debug annotations as HTML comments in rendered markup.
# Changing this configuration will require mix clean and a full recompile.
debug_heex_annotations: true,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true

View file

@ -8,7 +8,7 @@ import Config
config :mv, MvWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
# Configures Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Mv.Finch
config :swoosh, api_client: Swoosh.ApiClient.Req
# Disable Swoosh Local Memory Storage
config :swoosh, local: false

View file

@ -48,7 +48,7 @@ if config_env() == :prod do
You can generate one by calling: mix phx.gen.secret
"""
host = System.get_env("PHX_HOST") || "example.com"
host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable."
port = String.to_integer(System.get_env("PORT") || "4000")
config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
@ -109,7 +109,7 @@ if config_env() == :prod do
# 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:
# Swoosh supports Hackney, Req and Finch out of the box:
#
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
#

View file

@ -8,7 +8,8 @@ import Config
config :mv, Mv.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
hostname: System.get_env("TEST_POSTGRES_HOST", "localhost"),
port: System.get_env("TEST_POSTGRES_PORT", "5000"),
database: "mv_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2

68
docker-compose.yml Normal file
View file

@ -0,0 +1,68 @@
version: "3.5"
networks:
local:
rauthy-dev:
driver: bridge
services:
db:
image: postgres:17.5-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mv_dev
volumes:
- type: volume
source: postgres-data
target: /var/lib/postgresql/data
volume:
nocopy: true
ports:
- "5000:5432"
networks:
- local
mailcrab:
image: marlonb/mailcrab:latest
ports:
- "1080:1080"
networks:
- rauthy-dev
rauthy:
container_name: rauthy-dev
image: ghcr.io/sebadob/rauthy:0.30.2
environment:
- LOCAL_TEST=true
- SMTP_URL=mailcrab
- SMTP_PORT=1025
- SMTP_DANGER_INSECURE=true
- LISTEN_SCHEME=http
- PUB_URL=localhost:8080
- BOOTSTRAP_ADMIN_PASSWORD_PLAIN=RauthyTest12345
#- HIQLITE=false
#- PG_HOST=db
#- PG_PORT=5432
#- PG_USER=postgres
#- PG_PASSWORD=postgres
#- PG_DB_NAME=mv_dev
ports:
- "8080:8080"
depends_on:
- mailcrab
- db
networks:
- rauthy-dev
- local
volumes:
- type: volume
source: rauthy-data
target: /app/data
volumes:
postgres-data:
rauthy-data:

View file

@ -12,8 +12,6 @@ defmodule Mv.Application do
Mv.Repo,
{DNSCluster, query: Application.get_env(:mv, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Mv.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: Mv.Finch},
# Start a worker by calling: Mv.Worker.start_link(arg)
# {Mv.Worker, arg},
# Start to serve requests, typically the last entry

130
lib/mv/membership/member.ex Normal file
View file

@ -0,0 +1,130 @@
defmodule Mv.Membership.Member do
use Ecto.Schema
import Ecto.Changeset
import EctoCommons.EmailValidator
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "members" do
field :first_name, :string
field :last_name, :string
field :email, :string
field :phone_number, :string
field :postal_code, :string
field :birth_date, :date
field :paid, :boolean, default: false
field :join_date, :date
field :exit_date, :date
field :notes, :string
field :city, :string
field :street, :string
field :house_number, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(member, attrs) do
member
|> cast(attrs, [
:first_name,
:last_name,
:email,
:birth_date,
:paid,
:phone_number,
:join_date,
:exit_date,
:notes,
:city,
:street,
:house_number,
:postal_code
])
|> validate_required([:first_name, :last_name, :email])
|> validate_length(:first_name, min: 1)
|> validate_length(:last_name, min: 1)
|> validate_length(:email, min: 5, max: 254)
|> validate_email(:email, checks: [:html_input, :pow])
|> validate_birth_date()
|> validate_join_date()
|> validate_exit_date()
|> validate_phone_number()
|> validate_postal_code()
end
def create_changeset(member, attrs, _metadata) do
changeset(member, attrs)
end
def update_changeset(member, attrs, _metadata) do
changeset(member, attrs)
end
defp validate_birth_date(changeset) do
case get_field(changeset, :birth_date) do
nil ->
changeset
birth_date ->
if Date.compare(birth_date, Date.utc_today()) == :gt do
add_error(changeset, :birth_date, "cannot be in the future")
else
changeset
end
end
end
defp validate_join_date(changeset) do
case get_field(changeset, :join_date) do
nil ->
changeset
join_date ->
if Date.compare(join_date, Date.utc_today()) == :gt do
add_error(changeset, :join_date, "cannot be in the future")
else
changeset
end
end
end
defp validate_exit_date(changeset) do
join_date = get_field(changeset, :join_date)
exit_date = get_field(changeset, :exit_date)
if join_date && exit_date && Date.compare(exit_date, join_date) != :gt do
add_error(changeset, :exit_date, "cannot be before join date")
else
changeset
end
end
defp validate_phone_number(changeset) do
case get_field(changeset, :phone_number) do
nil ->
changeset
phone_number ->
if Regex.match?(~r/^\+?[0-9\- ]{6,20}$/, phone_number) do
changeset
else
add_error(changeset, :phone_number, "is not a valid phone number")
end
end
end
defp validate_postal_code(changeset) do
case get_field(changeset, :postal_code) do
nil ->
changeset
postal_code ->
if Regex.match?(~r/^\d{5}$/, postal_code) do
changeset
else
add_error(changeset, :postal_code, "must consist of 5 digits")
end
end
end
end

28
lib/mv/release.ex Normal file
View file

@ -0,0 +1,28 @@
defmodule Mv.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :mv
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

View file

@ -38,9 +38,7 @@ defmodule MvWeb do
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: MvWeb.Layouts]
use Phoenix.Controller, formats: [:html, :json]
use Gettext, backend: MvWeb.Gettext
@ -52,8 +50,7 @@ defmodule MvWeb do
def live_view do
quote do
use Phoenix.LiveView,
layout: {MvWeb.Layouts, :app}
use Phoenix.LiveView
unquote(html_helpers())
end
@ -90,8 +87,9 @@ defmodule MvWeb do
# Core UI components
import MvWeb.CoreComponents
# Shortcut for generating JS commands
# Common modules used in templates
alias Phoenix.LiveView.JS
alias MvWeb.Layouts
# Routes generation with the ~p sigil
unquote(verified_routes())

View file

@ -3,92 +3,34 @@ defmodule MvWeb.CoreComponents do
Provides core UI components.
At first glance, this module may seem daunting, but its goal is to provide
core building blocks for your application, such as modals, tables, and
forms. The components consist mostly of markup and are well-documented
core building blocks for your application, such as tables, forms, and
inputs. The components consist mostly of markup and are well-documented
with doc strings and declarative assigns. You may customize and style
them in any way you want, based on your application growth and needs.
The default components use Tailwind CSS, a utility-first CSS framework.
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
how to customize them or feel free to swap in another framework altogether.
The foundation for styling is Tailwind CSS, a utility-first CSS framework,
augmented with daisyUI, a Tailwind CSS plugin that provides UI components
and themes. Here are useful references:
* [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
started and see the available components.
* [Tailwind CSS](https://tailwindcss.com) - the foundational framework
we build on. You will use it for layout, sizing, flexbox, grid, and
spacing.
* [Heroicons](https://heroicons.com) - see `icon/1` for usage.
* [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) -
the component system used by Phoenix. Some components, such as `<.link>`
and `<.form>`, are defined there.
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
use Phoenix.Component
use Gettext, backend: MvWeb.Gettext
alias Phoenix.LiveView.JS
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
This is a modal.
</.modal>
JS commands may be passed to the `:on_cancel` to configure
the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
This is another modal.
</.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
slot :inner_block, required: true
def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
>
<div class="absolute top-6 right-5">
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<div id={"#{@id}-content"}>
{render_slot(@inner_block)}
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices.
@ -114,132 +56,59 @@ defmodule MvWeb.CoreComponents do
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
]}
class="toast toast-top toast-end z-50"
{@rest}
>
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
{@title}
</p>
<p class="mt-2 text-sm leading-5">{msg}</p>
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
"""
end
@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
def flash_group(assigns) do
~H"""
<div id={@id}>
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
<.flash
id="client-error"
kind={:error}
title={gettext("We can't find the internet")}
phx-disconnected={show(".phx-client-error #client-error")}
phx-connected={hide("#client-error")}
hidden
>
{gettext("Attempting to reconnect")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
<.flash
id="server-error"
kind={:error}
title={gettext("Something went wrong!")}
phx-disconnected={show(".phx-server-error #server-error")}
phx-connected={hide("#server-error")}
hidden
>
{gettext("Hang in there while we get back on track")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
</div>
"""
end
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the data structure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
{render_slot(@inner_block, f)}
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
{render_slot(action, f)}
<div class={[
"alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
@kind == :info && "alert-info",
@kind == :error && "alert-error"
]}>
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
<div>
<p :if={@title} class="font-semibold">{@title}</p>
<p>{msg}</p>
</div>
<div class="flex-1" />
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
</.form>
</div>
"""
end
@doc """
Renders a button.
Renders a button with navigation support.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
<.button phx-click="go" variant="primary">Send!</.button>
<.button navigate={~p"/"}>Home</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
attr :rest, :global, include: ~w(href navigate patch method)
attr :variant, :string, values: ~w(primary)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
{render_slot(@inner_block)}
</button>
"""
def button(%{rest: rest} = assigns) do
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
if rest[:href] || rest[:navigate] || rest[:patch] do
~H"""
<.link class={["btn", @class]} {@rest}>
{render_slot(@inner_block)}
</.link>
"""
else
~H"""
<button class={["btn", @class]} {@rest}>
{render_slot(@inner_block)}
</button>
"""
end
end
@doc """
@ -276,7 +145,7 @@ defmodule MvWeb.CoreComponents do
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file month number password
range search select tel text textarea time url week)
search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
@ -286,6 +155,8 @@ defmodule MvWeb.CoreComponents do
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :class, :string, default: nil, doc: "the input class to use over defaults"
attr :error_class, :string, default: nil, doc: "the input error class to use over defaults"
attr :rest, :global,
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
@ -309,108 +180,95 @@ defmodule MvWeb.CoreComponents do
end)
~H"""
<div>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<fieldset class="fieldset mb-2">
<label>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
{@label}
<span class="label">
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class={@class || "checkbox checkbox-sm"}
{@rest}
/>{@label}
</span>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</fieldset>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<select
id={@id}
name={@name}
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
<fieldset class="fieldset mb-2">
<label>
<span :if={@label} class="label mb-1">{@label}</span>
<select
id={@id}
name={@name}
class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</fieldset>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<textarea
id={@id}
name={@name}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
<fieldset class="fieldset mb-2">
<label>
<span :if={@label} class="label mb-1">{@label}</span>
<textarea
id={@id}
name={@name}
class={[
@class || "w-full textarea",
@errors != [] && (@error_class || "textarea-error")
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</fieldset>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
<fieldset class="fieldset mb-2">
<label>
<span :if={@label} class="label mb-1">{@label}</span>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
@class || "w-full input",
@errors != [] && (@error_class || "input-error")
]}
{@rest}
/>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</fieldset>
"""
end
@doc """
Renders a label.
"""
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
# Helper used by inputs to generate form errors
defp error(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
{render_slot(@inner_block)}
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
<.icon name="hero-exclamation-circle" class="size-5" />
{render_slot(@inner_block)}
</p>
"""
@ -427,12 +285,12 @@ defmodule MvWeb.CoreComponents do
def header(assigns) do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4", @class]}>
<div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
<h1 class="text-lg font-semibold leading-8">
{render_slot(@inner_block)}
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
<p :if={@subtitle != []} class="text-sm text-base-content/70">
{render_slot(@subtitle)}
</p>
</div>
@ -473,49 +331,34 @@ defmodule MvWeb.CoreComponents do
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
{render_slot(col, @row_item.(row))}
</span>
</div>
</td>
<td :if={@action != []} class="relative w-14 p-0">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
{render_slot(action, @row_item.(row))}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<table class="table table-zebra">
<thead>
<tr>
<th :for={col <- @col}>{col[:label]}</th>
<th :if={@action != []}>
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
<td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)}
class={@row_click && "hover:cursor-pointer"}
>
{render_slot(col, @row_item.(row))}
</td>
<td :if={@action != []} class="w-0 font-semibold">
<div class="flex gap-4">
<%= for action <- @action do %>
{render_slot(action, @row_item.(row))}
<% end %>
</div>
</td>
</tr>
</tbody>
</table>
"""
end
@ -535,38 +378,14 @@ defmodule MvWeb.CoreComponents do
def list(assigns) do
~H"""
<div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
<dd class="text-zinc-700">{render_slot(item)}</dd>
<ul class="list">
<li :for={item <- @item} class="list-row">
<div>
<div class="font-bold">{item.title}</div>
<div>{render_slot(item)}</div>
</div>
</dl>
</div>
"""
end
@doc """
Renders a back navigation link.
## Examples
<.back navigate={~p"/posts"}>Back to posts</.back>
"""
attr :navigate, :any, required: true
slot :inner_block, required: true
def back(assigns) do
~H"""
<div class="mt-16">
<.link
navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
{render_slot(@inner_block)}
</.link>
</div>
</li>
</ul>
"""
end
@ -581,15 +400,15 @@ defmodule MvWeb.CoreComponents do
width, height, and background color classes.
Icons are extracted from the `deps/heroicons` directory and bundled within
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
## Examples
<.icon name="hero-x-mark-solid" />
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
<.icon name="hero-x-mark" />
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
attr :class, :string, default: "size-4"
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
@ -604,7 +423,7 @@ defmodule MvWeb.CoreComponents do
to: selector,
time: 300,
transition:
{"transition-all transform ease-out duration-300",
{"transition-all ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
@ -615,41 +434,16 @@ defmodule MvWeb.CoreComponents do
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
{"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
time: 300,
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
IO.puts("DEBUG: translate_error called with msg: #{inspect(msg)}, opts: #{inspect(opts)}")
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
@ -660,11 +454,15 @@ defmodule MvWeb.CoreComponents do
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(MvWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(MvWeb.Gettext, "errors", msg, opts)
end
result =
if count = opts[:count] do
Gettext.dngettext(MvWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(MvWeb.Gettext, "errors", msg, opts)
end
IO.puts("DEBUG: translate_error result: #{inspect(result)}")
result
end
@doc """
@ -673,4 +471,22 @@ defmodule MvWeb.CoreComponents do
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
@doc """
Translates Backpex strings using gettext.
"""
def translate_backpex({msg, opts}) do
IO.puts("DEBUG: translate_backpex called with msg: #{inspect(msg)}, opts: #{inspect(opts)}")
# Use our custom translation module
result =
if count = opts[:count] do
Gettext.dngettext(MvWeb.Gettext, "backpex", msg, msg, count, opts)
else
Gettext.dgettext(MvWeb.Gettext, "backpex", msg, opts)
end
IO.puts("DEBUG: translate_backpex result: #{inspect(result)}")
result
end
end

View file

@ -4,11 +4,133 @@ defmodule MvWeb.Layouts do
See the `layouts` directory for all templates available.
The "root" layout is a skeleton rendered as part of the
application router. The "app" layout is set as the default
layout on both `use MvWeb, :controller` and
`use MvWeb, :live_view`.
application router. The "app" layout is rendered as component
in regular views and live views.
"""
use MvWeb, :html
embed_templates "layouts/*"
@doc "The main app layout (Backpex shell, topbar, sidebar, flash, content)"
attr :flash, :map, required: true
attr :socket, :any, default: nil
attr :current_url, :string, default: nil
attr :fluid?, :boolean, default: false
attr :inner_content, :any, required: true
def app(assigns) do
~H"""
<Backpex.HTML.Layout.app_shell fluid={@fluid?}>
<:topbar>
<Backpex.HTML.Layout.topbar_branding />
<Backpex.HTML.Layout.theme_selector
socket={@socket}
themes={[
{"Light", "light"},
{"Dark", "dark"}
]}
/>
<Backpex.HTML.Layout.topbar_dropdown class="mr-2 md:mr-0">
<:label>
<label tabindex="0" class="btn btn-square btn-ghost">
<.icon name="hero-user" class="size-6" />
</label>
</:label>
<li>
<.link navigate={~p"/"} class="text-error flex justify-between hover:bg-base-200">
<p>Logout</p>
<.icon name="hero-arrow-right-on-rectangle" class="size-5" />
</.link>
</li>
</Backpex.HTML.Layout.topbar_dropdown>
</:topbar>
<:sidebar>
<Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/members"}>
<.icon name="hero-users" class="size-5" /> Members
</Backpex.HTML.Layout.sidebar_item>
</:sidebar>
<Backpex.HTML.Layout.flash_messages flash={@flash} />
{@inner_content}
</Backpex.HTML.Layout.app_shell>
"""
end
@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
def flash_group(assigns) do
~H"""
<div id={@id} aria-live="polite">
<.flash kind={:info} flash={@flash} />
<.flash kind={:error} flash={@flash} />
<.flash
id="client-error"
kind={:error}
title={gettext("We can't find the internet")}
phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
hidden
>
{gettext("Attempting to reconnect")}
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
</.flash>
<.flash
id="server-error"
kind={:error}
title={gettext("Something went wrong!")}
phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
hidden
>
{gettext("Attempting to reconnect")}
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
</.flash>
</div>
"""
end
@doc """
Provides dark vs light theme toggle based on themes defined in app.css.
See <head> in root.html.heex which applies the theme before page load.
"""
def theme_toggle(assigns) do
~H"""
<div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
<div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "system"})}
class="flex p-2 cursor-pointer w-1/3"
>
<.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
</button>
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "light"})}
class="flex p-2 cursor-pointer w-1/3"
>
<.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
</button>
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "dark"})}
class="flex p-2 cursor-pointer w-1/3"
>
<.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
</button>
</div>
"""
end
end

View file

@ -1,32 +0,0 @@
<header class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v{Application.spec(:phoenix, :vsn)}
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
{@inner_content}
</div>
</main>

View file

@ -1,17 +1,37 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<html lang="en" class="[scrollbar-gutter:stable]" data-theme={assigns[:theme] || "light"}>
<head>
{Application.get_env(:live_debugger, :live_debugger_tags)}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="Mv" suffix=" · Phoenix Framework">
{assigns[:page_title]}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
</script>
<script>
(() => {
const setTheme = (theme) => {
if (theme === "system") {
localStorage.removeItem("phx:theme");
document.documentElement.removeAttribute("data-theme");
} else {
localStorage.setItem("phx:theme", theme);
document.documentElement.setAttribute("data-theme", theme);
}
};
if (!document.documentElement.hasAttribute("data-theme")) {
setTheme(localStorage.getItem("phx:theme") || "system");
}
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
})();
</script>
</head>
<body class="bg-white">
<body>
{@inner_content}
</body>
</html>

View file

@ -2,8 +2,6 @@ defmodule MvWeb.PageController do
use MvWeb, :controller
def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
render(conn, :home, layout: false)
render(conn, :home)
end
end

View file

@ -1,4 +1,4 @@
<.flash_group flash={@flash} />
<Layouts.flash_group flash={@flash} />
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
<svg
viewBox="0 0 1480 957"
@ -46,16 +46,20 @@
fill="#FD4F00"
/>
</svg>
<h1 class="text-brand mt-10 flex items-center text-sm font-semibold leading-6">
Phoenix Framework
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6">
v{Application.spec(:phoenix, :vsn)}
</small>
</h1>
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance">
<div class="mt-10 flex justify-between items-center">
<h1 class="flex items-center text-sm font-semibold leading-6">
Phoenix Framework
<small class="badge badge-warning badge-sm ml-3">
v{Application.spec(:phoenix, :vsn)}
</small>
</h1>
<Layouts.theme_toggle />
</div>
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
Peace of mind from prototype to production.
</p>
<p class="mt-4 text-base leading-7 text-zinc-600">
<p class="mt-4 leading-7 text-base-content/70">
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
</p>
<div class="flex">
@ -63,16 +67,16 @@
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
<path d="m12 4 10-2v18l-10 2V4Z" fill="#18181B" fill-opacity=".15" />
<path d="m12 4 10-2v18l-10 2V4Z" fill="currentColor" fill-opacity=".15" />
<path
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
stroke="#18181B"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
@ -83,17 +87,17 @@
</a>
<a
href="https://github.com/phoenixframework/phoenix"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
fill="#18181B"
/>
</svg>
Source Code
@ -101,15 +105,15 @@
</a>
<a
href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
<path
d="M12 1v6M12 17v6"
stroke="#18181B"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
@ -118,9 +122,9 @@
cx="12"
cy="12"
r="4"
fill="#18181B"
fill="currentColor"
fill-opacity=".15"
stroke="#18181B"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
@ -130,85 +134,61 @@
</span>
</a>
</div>
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-zinc-700 sm:grid-cols-2">
<div>
<a
href="https://twitter.com/elixirphoenix"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
>
<path d="M5.403 14c5.283 0 8.172-4.617 8.172-8.62 0-.131 0-.262-.008-.391A6.033 6.033 0 0 0 15 3.419a5.503 5.503 0 0 1-1.65.477 3.018 3.018 0 0 0 1.263-1.676 5.579 5.579 0 0 1-1.824.736 2.832 2.832 0 0 0-1.63-.916 2.746 2.746 0 0 0-1.821.319A2.973 2.973 0 0 0 8.076 3.78a3.185 3.185 0 0 0-.182 1.938 7.826 7.826 0 0 1-3.279-.918 8.253 8.253 0 0 1-2.64-2.247 3.176 3.176 0 0 0-.315 2.208 3.037 3.037 0 0 0 1.203 1.836A2.739 2.739 0 0 1 1.56 6.22v.038c0 .7.23 1.377.65 1.919.42.54 1.004.912 1.654 1.05-.423.122-.866.14-1.297.052.184.602.541 1.129 1.022 1.506a2.78 2.78 0 0 0 1.662.598 5.656 5.656 0 0 1-2.007 1.074A5.475 5.475 0 0 1 1 12.64a7.827 7.827 0 0 0 4.403 1.358" />
</svg>
Follow on Twitter
</a>
</div>
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-base-content/80 sm:grid-cols-2">
<div>
<a
href="https://elixirforum.com"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
</svg>
Discuss on the Elixir Forum
</a>
</div>
<div>
<a
href="https://web.libera.chat/#elixir"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.356 2.007a.75.75 0 0 1 .637.849l-1.5 10.5a.75.75 0 1 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.637ZM11.356 2.008a.75.75 0 0 1 .637.848l-1.5 10.5a.75.75 0 0 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.636Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 5.25a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75ZM13 10.75a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75Z"
/>
</svg>
Chat on Libera IRC
</a>
</div>
<div>
<a
href="https://discord.gg/elixir"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
</svg>
Join our Discord server
</a>
</div>
<div>
<a
href="https://elixir-slack.community/"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
</svg>
Join us on Slack
</a>
</div>
<div>
<a
href="https://fly.io/docs/elixir/getting-started/"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 20 20"
aria-hidden="true"
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
</svg>

View file

@ -0,0 +1,9 @@
defmodule MvWeb.RedirectController do
use MvWeb, :controller
def redirect_to_members(conn, _params) do
conn
|> Phoenix.Controller.redirect(to: ~p"/members")
|> Plug.Conn.halt()
end
end

View file

@ -17,12 +17,13 @@ defmodule MvWeb.Endpoint do
# 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.
# When code reloading is disabled (e.g., in production),
# the `gzip` option is enabled to serve compressed
# static files generated by running `phx.digest`.
plug Plug.Static,
at: "/",
from: :mv,
gzip: false,
gzip: not code_reloading?,
only: MvWeb.static_paths()
# Code reloading can be explicitly enabled under the
@ -31,7 +32,6 @@ defmodule MvWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :mv
end
plug Phoenix.LiveDashboard.RequestLogger,

View file

@ -0,0 +1,74 @@
defmodule MvWeb.Live.MemberLive do
use Backpex.LiveResource,
adapter_config: [
schema: Mv.Membership.Member,
repo: Mv.Repo,
update_changeset: &Mv.Membership.Member.update_changeset/3,
create_changeset: &Mv.Membership.Member.create_changeset/3
],
layout: {MvWeb.Layouts, :app}
@impl Backpex.LiveResource
def singular_name, do: "Member"
@impl Backpex.LiveResource
def plural_name, do: "Members"
@impl Backpex.LiveResource
def fields do
[
first_name: %{
module: Backpex.Fields.Text,
label: "First Name"
},
last_name: %{
module: Backpex.Fields.Text,
label: "Last Name"
},
email: %{
module: Backpex.Fields.Text,
label: "Email"
},
phone_number: %{
module: Backpex.Fields.Text,
label: "Phone Number"
},
birth_date: %{
module: Backpex.Fields.Date,
label: "Birth Date"
},
join_date: %{
module: Backpex.Fields.Date,
label: "Join Date"
},
exit_date: %{
module: Backpex.Fields.Date,
label: "Exit Date"
},
paid: %{
module: Backpex.Fields.Boolean,
label: "Paid"
},
street: %{
module: Backpex.Fields.Text,
label: "Street"
},
house_number: %{
module: Backpex.Fields.Text,
label: "House Number"
},
postal_code: %{
module: Backpex.Fields.Text,
label: "Postal Code"
},
city: %{
module: Backpex.Fields.Text,
label: "City"
},
notes: %{
module: Backpex.Fields.Textarea,
label: "Notes"
}
]
end
end

View file

@ -0,0 +1,72 @@
defmodule MvWeb.Plugs.SetLocale do
@moduledoc """
Plug to set the locale for the application.
Defaults to German if no locale is specified.
"""
import Plug.Conn
@supported_locales ["de", "en"]
@default_locale "de"
def init(_), do: nil
def call(conn, _) do
case fetch_locale(conn) do
nil ->
# Set default locale if none found
Gettext.put_locale(MvWeb.Gettext, @default_locale)
persist(@default_locale, conn)
locale ->
# Set and persist locale
IO.inspect(locale, label: "locale")
Gettext.put_locale(MvWeb.Gettext, locale)
|> persist(conn)
end
end
defp fetch_locale(conn) do
conn.params["locale"] ||
get_session(conn, :locale) ||
parse_accept_language(conn) ||
@default_locale
end
defp parse_accept_language(conn) do
case get_req_header(conn, "accept-language") do
[accept_language | _] ->
parse_accept_language_header(accept_language)
_ ->
nil
end
end
defp parse_accept_language_header(accept_language) do
accept_language
|> String.split(",")
|> Enum.map(&parse_language_tag/1)
|> Enum.find(&(&1 in @supported_locales))
end
defp parse_language_tag(tag) do
tag
|> String.trim()
|> String.split(";")
|> List.first()
|> String.split("-")
|> List.first()
|> String.downcase()
end
defp persist(locale, conn) do
IO.inspect(locale, label: "persist_locale")
# Ensure locale is a string before setting cookie
locale_string = to_string(locale)
put_session(conn, :locale, locale_string)
|> put_resp_cookie("locale", locale_string)
end
end

View file

@ -1,5 +1,6 @@
defmodule MvWeb.Router do
use MvWeb, :router
import Backpex.Router
pipeline :browser do
plug :accepts, ["html"]
@ -8,6 +9,8 @@ defmodule MvWeb.Router do
plug :put_root_layout, html: {MvWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Backpex.ThemeSelectorPlug
plug MvWeb.Plugs.SetLocale
end
pipeline :api do
@ -17,7 +20,13 @@ defmodule MvWeb.Router do
scope "/", MvWeb do
pipe_through :browser
get "/", PageController, :home
backpex_routes()
get "/", RedirectController, :redirect_to_members
live_session :default, on_mount: Backpex.InitAssigns do
live_resources "/members", Live.MemberLive
end
end
# Other scopes may use custom stacks.

View file

@ -52,29 +52,6 @@ defmodule MvWeb.Telemetry do
unit: {:native, :millisecond}
),
# Database Metrics
summary("mv.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("mv.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("mv.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("mv.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("mv.repo.query.idle_time",
unit: {:native, :millisecond},
description:
"The time the connection spent waiting before being checked out for the query"
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),

23
mix.exs
View file

@ -5,11 +5,12 @@ defmodule Mv.MixProject do
[
app: :mv,
version: "0.1.0",
elixir: "~> 1.14",
elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
deps: deps(),
listeners: [Phoenix.CodeReloader]
]
end
@ -32,17 +33,19 @@ defmodule Mv.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.7.20"},
{:tidewave, "~> 0.1", only: [:dev]},
{:backpex, "~> 0.13.0"},
{:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.10"},
{:postgrex, ">= 0.0.0"},
{:phoenix, "~> 1.8.0-rc.3", override: true},
{:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 1.0.0"},
{:phoenix_live_view, "~> 1.0.9"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
{:esbuild, "~> 0.9", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
{:heroicons,
github: "tailwindlabs/heroicons",
tag: "v2.1.1",
@ -50,12 +53,16 @@ defmodule Mv.MixProject do
app: false,
compile: false,
depth: 1},
{:swoosh, "~> 1.5"},
{:finch, "~> 0.13"},
{:swoosh, "~> 1.16"},
{:req, "~> 0.5"},
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.26"},
{:jason, "~> 1.2"},
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ecto_commons, "~> 0.3"},
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"}
]

View file

@ -1,41 +1,65 @@
%{
"bandit": {:hex, :bandit, "1.6.8", "be6fcbe01a74e6cba42ae35f4085acaeae9b2d8d360c0908d0b9addbc2811e47", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4fc08c8d4733735d175a007ecb25895e84d09292b0180a2e9f16948182c88b6e"},
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
"backpex": {:hex, :backpex, "0.13.0", "7c7e5d2e6bfaf41e663c094b9073564c739382b6085adb80e171e03fd1a612cc", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: true]}, {:ash_postgres, "~> 2.0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:money, "~> 1.13", [hex: :money, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.4", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "a193043ea84b7c5e9dc1d9811b1627095702df0f39d92a10edbd6eb2ea9017e5"},
"bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
"circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 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", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
"ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
"money": {:hex, :money, "1.14.0", "61c1e9d9ae1dd45dae7f72568987b3e7275031c3f5a0bf8a053bd74259555934", [:mix], [{:decimal, "~> 1.2 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "b8691009e0c31715d2e5a3cca68ca2e1a46895d63c11257b317d8801ee2c54e3"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"},
"number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"},
"phoenix": {:hex, :phoenix, "1.8.0-rc.3", "6ae19e57b9c109556f1b8abdb992d96d443b0ae28e03b604f3dc6c75d9f7d35f", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "419422afc33e965c0dbf181cbedc77b4cfd024dac0db7d9d2287656043d48e24"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.5", "f072166f87c44ffaf2b47b65c5ced8c375797830e517bfcf0a006fe7eb113911", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {: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.3 or ~> 4.0", [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]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94abbc84df8a93a64514fc41528695d7326b6f3095e906b32f264ec4280811f3"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [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]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"reactor": {:hex, :reactor, "0.15.2", "8c1b3fe0527b7a92b0b22c3f33f2e66858dd069bf1dd51d1031f63cd8cbd1fd5", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "091435a1fa0cab9bc2ed3934b203a0fd190f62e8b6aca63741f9242b8c7631ac"},
"req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
"sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"},
"sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"},
"spark": {:hex, :spark, "2.2.52", "50094275c9bbafa8e5e9eed0ab61983ee209a500e7044914ccf88e9921ae5082", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "f8de8c298bbbf7abd2a80d0ecabcefef65941f397cdbe94ce6165a121b09084f"},
"spitfire": {:hex, :spitfire, "0.2.0", "0de1f519a23f65bde40d316adad53c07a9563f25cc68915d639d8a509a0aad8a", [:mix], [], "hexpm", "743daaee2d81a0d8095431729f478ce49b47ea8943c7d770de86704975cb7775"},
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.18.2", "41279e8449b65d14b571b66afe9ab352c3b0179291af8e5f4ad9207f489ad11a", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "032fcb2179f6d4e3b90030514ddc8d3946d8b046be939d121db480ca78adbc38"},
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"thousand_island": {:hex, :thousand_island, "1.3.11", "b68f3e91f74d564ae20b70d981bbf7097dde084343c14ae8a33e5b5fbb3d6f37", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "555c18c62027f45d9c80df389c3d01d86ba11014652c00be26e33b1b64e98d29"},
"thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
"tidewave": {:hex, :tidewave, "0.1.7", "a93c500a414cfd211c7058a2b4b22759fb8cde5d72c471a34f7046cd66a5a5e6", [:mix], [{:circular_buffer, "~> 0.4", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.47 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "2cfe9c0c3295132cc682b3cd1c859f801bf2e4d02816618d0659f4d765d26435"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
}

252
priv/gettext/backpex.pot Normal file
View file

@ -0,0 +1,252 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new messages manually only if they're dynamic
## messages that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#
msgid ""
msgstr ""
#, elixir-autogen, elixir-format
msgid "%{count} %{resources} have been deleted successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "%{resource} has been deleted successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "%{resource} has been edited successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "(%{count} total)"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Add entry"
msgstr ""
#, elixir-autogen, elixir-format
msgid "An error occurred while deleting %{count} %{resources}!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "An error occurred while deleting the %{resource}!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Apply"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete %{count} items?"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete the item?"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Attach %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Choose %{resource} ..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "clear"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Clear %{name} filter"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Close modal"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Deselect all"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Detach relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Error in relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Filters"
msgstr ""
#, elixir-autogen, elixir-format
msgid "From"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Items %{from} to %{to}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "New %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "New %{resource} has been created successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Next Page"
msgstr ""
#, elixir-autogen, elixir-format
msgid "No %{resources} found"
msgstr ""
#, elixir-autogen, elixir-format
msgid "No options found"
msgstr ""
#, elixir-autogen, elixir-format
msgid "or drag and drop"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Previous Page"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Save"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Save & Continue editing"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Search"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select all"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select all items"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select item with id: %{id}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select options..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "selected"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Show more"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "The item is used elsewhere."
msgstr ""
#, elixir-autogen, elixir-format
msgid "The items are used elsewhere."
msgstr ""
#, elixir-autogen, elixir-format
msgid "There are errors in the form."
msgstr ""
#, elixir-autogen, elixir-format
msgid "To"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Toggle columns"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Toggle metrics"
msgstr ""
#, elixir-autogen, elixir-format
msgid "too large"
msgstr ""
#, elixir-autogen, elixir-format
msgid "too many files"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Try a different filter setting or clear all filters."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Try a different search term."
msgstr ""
#, elixir-autogen, elixir-format
msgid "unacceptable file type"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Unselect %{label}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Upload a file"
msgstr ""
#, elixir-autogen, elixir-format
msgid "We can't find the internet!"
msgstr ""

View file

@ -0,0 +1,264 @@
# German translations for backpex package.
#
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "%{count} %{resources} have been deleted successfully."
msgstr "%{count} %{resources} wurden erfolgreich gelöscht."
msgid "%{resource} has been deleted successfully."
msgstr "%{resource} wurde erfolgreich gelöscht."
msgid "%{resource} has been edited successfully."
msgstr "%{resource} wurde erfolgreich bearbeitet."
msgid "(%{count} total)"
msgstr "(%{count} gesamt)"
msgid "Add entry"
msgstr "Eintrag hinzufügen"
msgid "An error occurred while deleting %{count} %{resources}!"
msgstr "Beim Löschen von %{count} %{resources} ist ein Fehler aufgetreten!"
msgid "An error occurred while deleting the %{resource}!"
msgstr "Beim Löschen von %{resource} ist ein Fehler aufgetreten!"
msgid "Apply"
msgstr "Anwenden"
msgid "Are you sure you want to delete %{count} items?"
msgstr "Sind Sie sicher, dass Sie %{count} Elemente löschen möchten?"
msgid "Are you sure you want to delete the item?"
msgstr "Sind Sie sicher, dass Sie das Element löschen möchten?"
msgid "Attach %{resource}"
msgstr "%{resource} anhängen"
msgid "Attempting to reconnect..."
msgstr "Versuche erneut zu verbinden..."
msgid "Cancel"
msgstr "Abbrechen"
msgid "Choose %{resource} ..."
msgstr "%{resource} auswählen ..."
msgid "clear"
msgstr "löschen"
msgid "Clear %{name} filter"
msgstr "%{name} Filter löschen"
msgid "Close modal"
msgstr "Modal schließen"
msgid "Delete"
msgstr "Löschen"
msgid "Deselect all"
msgstr "Alle abwählen"
msgid "Detach relation with index %{index}"
msgstr "Beziehung mit Index %{index} trennen"
msgid "Edit"
msgstr "Bearbeiten"
msgid "Edit %{resource}"
msgstr "%{resource} bearbeiten"
msgid "Edit relation with index %{index}"
msgstr "Beziehung mit Index %{index} bearbeiten"
msgid "Error in relation with index %{index}"
msgstr "Fehler in Beziehung mit Index %{index}"
msgid "Filters"
msgstr "Filter"
msgid "From"
msgstr "Von"
msgid "Hang in there while we get back on track..."
msgstr "Bleiben Sie dran, während wir wieder auf Kurs kommen..."
msgid "Items %{from} to %{to}"
msgstr "Elemente %{from} bis %{to}"
msgid "New %{resource}"
msgstr "Neuer %{resource}"
msgid "New %{resource} has been created successfully."
msgstr "Neuer %{resource} wurde erfolgreich erstellt."
msgid "Next Page"
msgstr "Nächste Seite"
msgid "No %{resources} found"
msgstr "Keine %{resources} gefunden"
msgid "No options found"
msgstr "Keine Optionen gefunden"
msgid "or drag and drop"
msgstr "oder ziehen und ablegen"
msgid "Previous Page"
msgstr "Vorherige Seite"
msgid "Save"
msgstr "Speichern"
msgid "Save & Continue editing"
msgstr "Speichern & Weiter bearbeiten"
msgid "Search"
msgstr "Suchen"
msgid "Select all"
msgstr "Alle auswählen"
msgid "Select all items"
msgstr "Alle Elemente auswählen"
msgid "Select item with id: %{id}"
msgstr "Element mit ID auswählen: %{id}"
msgid "Select options..."
msgstr "Optionen auswählen..."
msgid "selected"
msgstr "ausgewählt"
msgid "Show"
msgstr "Anzeigen"
msgid "Show more"
msgstr "Mehr anzeigen"
msgid "Something went wrong!"
msgstr "Etwas ist schiefgelaufen!"
msgid "The item is used elsewhere."
msgstr "Das Element wird an anderer Stelle verwendet."
msgid "The items are used elsewhere."
msgstr "Die Elemente werden an anderer Stelle verwendet."
msgid "There are errors in the form."
msgstr "Es gibt Fehler im Formular."
msgid "To"
msgstr "Bis"
msgid "Toggle columns"
msgstr "Spalten umschalten"
msgid "Toggle metrics"
msgstr "Metriken umschalten"
msgid "too large"
msgstr "zu groß"
msgid "too many files"
msgstr "zu viele Dateien"
msgid "Try a different filter setting or clear all filters."
msgstr "Versuchen Sie eine andere Filtereinstellung oder löschen Sie alle Filter."
msgid "Try a different search term."
msgstr "Versuchen Sie einen anderen Suchbegriff."
msgid "unacceptable file type"
msgstr "nicht akzeptierter Dateityp"
msgid "Unselect %{label}"
msgstr "%{label} abwählen"
msgid "Upload a file"
msgstr "Datei hochladen"
msgid "We can't find the internet!"
msgstr "Wir können das Internet nicht finden!"
msgid "First Name"
msgstr "Vorname"
msgid "Last Name"
msgstr "Nachname"
msgid "Email"
msgstr "E-Mail"
msgid "Phone Number"
msgstr "Telefonnummer"
msgid "Birth Date"
msgstr "Geburtsdatum"
msgid "Join Date"
msgstr "Beitrittsdatum"
msgid "Exit Date"
msgstr "Austrittsdatum"
msgid "Paid"
msgstr "Bezahlt"
msgid "Street"
msgstr "Straße"
msgid "House Number"
msgstr "Hausnummer"
msgid "Postal Code"
msgstr "Postleitzahl"
msgid "City"
msgstr "Stadt"
msgid "Notes"
msgstr "Notizen"
msgid "Member"
msgstr "Mitglied"
msgid "Members"
msgstr "Mitglieder"
msgid "Actions"
msgstr "Aktionen"
msgid "Loading..."
msgstr "Lädt..."
msgid "Error"
msgstr "Fehler"
msgid "Success"
msgstr "Erfolg"
msgid "Warning"
msgstr "Warnung"
msgid "Info"
msgstr "Info"
msgid "Yes"
msgstr "Ja"
msgid "No"
msgstr "Nein"
msgid "Are you sure?"
msgstr "Sind Sie sicher?"
msgid "No results found"
msgstr "Keine Ergebnisse gefunden"
msgid "Clear"
msgstr "Löschen"

View file

@ -0,0 +1,104 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/mv_web/components/core_components.ex:339
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Aktionen"
#: lib/mv_web/components/layouts.ex:84
#: lib/mv_web/components/layouts.ex:96
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Versuche erneut zu verbinden"
#: lib/mv_web/components/layouts.ex:91
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "Etwas ist schiefgelaufen!"
#: lib/mv_web/components/layouts.ex:79
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "Wir können das Internet nicht finden"
#: lib/mv_web/components/core_components.ex:74
#, elixir-autogen, elixir-format
msgid "close"
msgstr "schließen"
#: lib/mv_web/components/layouts.ex:85
#: lib/mv_web/components/layouts.ex:97
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track"
msgstr "Bleiben Sie dran, während wir wieder auf Kurs kommen"
#: lib/mv_web/components/layouts.ex:82
#: lib/mv_web/components/layouts.ex:94
#, elixir-autogen, elixir-format
msgid "Loading"
msgstr "Lädt"
#: lib/mv_web/components/layouts.ex:83
#: lib/mv_web/components/layouts.ex:95
#, elixir-autogen, elixir-format
msgid "Loading..."
msgstr "Lädt..."
#: lib/mv_web/components/layouts.ex:80
#: lib/mv_web/components/layouts.ex:92
#, elixir-autogen, elixir-format
msgid "Something went wrong"
msgstr "Etwas ist schiefgelaufen"
#: lib/mv_web/components/layouts.ex:81
#: lib/mv_web/components/layouts.ex:93
#, elixir-autogen, elixir-format
msgid "We can't find the internet!"
msgstr "Wir können das Internet nicht finden!"
#: lib/mv_web/components/layouts.ex:86
#: lib/mv_web/components/layouts.ex:98
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track..."
msgstr "Bleiben Sie dran, während wir wieder auf Kurs kommen..."
#: lib/mv_web/components/layouts.ex:87
#: lib/mv_web/components/layouts.ex:99
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect..."
msgstr "Versuche erneut zu verbinden..."
#: lib/mv_web/components/layouts.ex:88
#: lib/mv_web/components/layouts.ex:100
#, elixir-autogen, elixir-format
msgid "Error"
msgstr "Fehler"
#: lib/mv_web/components/layouts.ex:89
#: lib/mv_web/components/layouts.ex:101
#, elixir-autogen, elixir-format
msgid "Success"
msgstr "Erfolg"
#: lib/mv_web/components/layouts.ex:90
#: lib/mv_web/components/layouts.ex:102
#, elixir-autogen, elixir-format
msgid "Warning"
msgstr "Warnung"
#: lib/mv_web/components/layouts.ex:91
#: lib/mv_web/components/layouts.ex:103
#, elixir-autogen, elixir-format
msgid "Info"
msgstr "Info"

View file

@ -0,0 +1,102 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "can't be blank"
msgstr "darf nicht leer sein"
msgid "has already been taken"
msgstr "ist bereits vergeben"
msgid "is invalid"
msgstr "ist ungültig"
msgid "must be accepted"
msgstr "muss akzeptiert werden"
msgid "has invalid format"
msgstr "hat ein ungültiges Format"
msgid "has an invalid entry"
msgstr "hat einen ungültigen Eintrag"
msgid "is reserved"
msgstr "ist reserviert"
msgid "does not match confirmation"
msgstr "stimmt nicht mit der Bestätigung überein"
msgid "is still associated with this entry"
msgstr "ist noch mit diesem Eintrag verknüpft"
msgid "are still associated with this entry"
msgstr "sind noch mit diesem Eintrag verknüpft"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "sollte %{count} Element haben"
msgstr[1] "sollte %{count} Elemente haben"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "sollte %{count} Zeichen lang sein"
msgstr[1] "sollte %{count} Zeichen lang sein"
msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
msgstr[0] "sollte %{count} Byte haben"
msgstr[1] "sollte %{count} Bytes haben"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "sollte mindestens %{count} Element haben"
msgstr[1] "sollte mindestens %{count} Elemente haben"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "sollte mindestens %{count} Zeichen lang sein"
msgstr[1] "sollte mindestens %{count} Zeichen lang sein"
msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
msgstr[0] "sollte mindestens %{count} Byte haben"
msgstr[1] "sollte mindestens %{count} Bytes haben"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "sollte höchstens %{count} Element haben"
msgstr[1] "sollte höchstens %{count} Elemente haben"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "sollte höchstens %{count} Zeichen lang sein"
msgstr[1] "sollte höchstens %{count} Zeichen lang sein"
msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
msgstr[0] "sollte höchstens %{count} Byte haben"
msgstr[1] "sollte höchstens %{count} Bytes haben"
msgid "must be less than %{number}"
msgstr "muss kleiner als %{number} sein"
msgid "must be greater than %{number}"
msgstr "muss größer als %{number} sein"
msgid "must be less than or equal to %{number}"
msgstr "muss kleiner oder gleich %{number} sein"
msgid "must be greater than or equal to %{number}"
msgstr "muss größer oder gleich %{number} sein"
msgid "must be equal to %{number}"
msgstr "muss gleich %{number} sein"

38
priv/gettext/default.pot Normal file
View file

@ -0,0 +1,38 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new messages manually only if they're dynamic
## messages that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#
msgid ""
msgstr ""
#: lib/mv_web/components/core_components.ex:339
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/mv_web/components/layouts.ex:84
#: lib/mv_web/components/layouts.ex:96
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/components/layouts.ex:91
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/mv_web/components/layouts.ex:79
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/mv_web/components/core_components.ex:74
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""

View file

@ -0,0 +1,252 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#, elixir-autogen, elixir-format
msgid "%{count} %{resources} have been deleted successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "%{resource} has been deleted successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "%{resource} has been edited successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "(%{count} total)"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Add entry"
msgstr ""
#, elixir-autogen, elixir-format
msgid "An error occurred while deleting %{count} %{resources}!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "An error occurred while deleting the %{resource}!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Apply"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete %{count} items?"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete the item?"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Attach %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Choose %{resource} ..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "clear"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Clear %{name} filter"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Close modal"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Deselect all"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Detach relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Edit relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Error in relation with index %{index}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Filters"
msgstr ""
#, elixir-autogen, elixir-format
msgid "From"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Items %{from} to %{to}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "New %{resource}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "New %{resource} has been created successfully."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Next Page"
msgstr ""
#, elixir-autogen, elixir-format
msgid "No %{resources} found"
msgstr ""
#, elixir-autogen, elixir-format
msgid "No options found"
msgstr ""
#, elixir-autogen, elixir-format
msgid "or drag and drop"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Previous Page"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Save"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Save & Continue editing"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Search"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select all"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select all items"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select item with id: %{id}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Select options..."
msgstr ""
#, elixir-autogen, elixir-format
msgid "selected"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Show more"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#, elixir-autogen, elixir-format
msgid "The item is used elsewhere."
msgstr ""
#, elixir-autogen, elixir-format
msgid "The items are used elsewhere."
msgstr ""
#, elixir-autogen, elixir-format
msgid "There are errors in the form."
msgstr ""
#, elixir-autogen, elixir-format
msgid "To"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Toggle columns"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Toggle metrics"
msgstr ""
#, elixir-autogen, elixir-format
msgid "too large"
msgstr ""
#, elixir-autogen, elixir-format
msgid "too many files"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Try a different filter setting or clear all filters."
msgstr ""
#, elixir-autogen, elixir-format
msgid "Try a different search term."
msgstr ""
#, elixir-autogen, elixir-format
msgid "unacceptable file type"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Unselect %{label}"
msgstr ""
#, elixir-autogen, elixir-format
msgid "Upload a file"
msgstr ""
#, elixir-autogen, elixir-format
msgid "We can't find the internet!"
msgstr ""

View file

@ -0,0 +1,38 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/mv_web/components/core_components.ex:339
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/mv_web/components/layouts.ex:84
#: lib/mv_web/components/layouts.ex:96
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/components/layouts.ex:91
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/mv_web/components/layouts.ex:79
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/mv_web/components/core_components.ex:74
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""

View file

@ -0,0 +1,26 @@
defmodule Mv.Repo.Migrations.CreateMembers do
use Ecto.Migration
def change do
create table(:members, primary_key: false) do
add :id, :uuid, primary_key: true, default: fragment("gen_random_uuid()")
add :first_name, :string, null: false
add :last_name, :string, null: false
add :email, :string, null: false
add :birth_date, :date
add :paid, :boolean, default: false
add :phone_number, :string
add :join_date, :date
add :exit_date, :date
add :notes, :string
add :city, :string
add :street, :string
add :house_number, :string
add :postal_code, :string
timestamps(type: :utc_datetime)
end
create unique_index(:members, [:email])
end
end

5
rel/overlays/bin/migrate Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
exec ./mv eval Mv.Release.migrate

5
rel/overlays/bin/server Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
PHX_SERVER=true exec ./mv start

30
renovate.json Normal file
View file

@ -0,0 +1,30 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"groupName": "Mix dependencies",
"matchCategories": "elixir"
},
{
"groupName": "asdf tool versions",
"matchFileNames": [".tool-versions"]
},
{
"groupName": "postgres",
"description": "Group updates to postgres across drone ci, docker-compose.yml and other files",
"matchPackageNames": ["postgres", "docker.io/library/postgres"],
"matchDatasources": ["docker"]
},
{
"description": "Disable elixir updates, as renovate does not work with their <version>-otp-<version> numbering scheme.",
"matchCurrentValue": "**-otp-**",
"enabled": false
},
{
"description": "Disable erlang updates as they need to be coordinated with elixir updates.",
"matchDepNames": "erlang",
"enabled": false
}
]
}

View file

@ -0,0 +1,19 @@
// This file only contains "backend" administration settings for renovate,
// such as where to access the forgejo api etc.
// To configure how renovate updates dependencies, see `renovate.json`.
module.exports = {
endpoint: "https://git.local-it.org/api/v1/",
gitAuthor: "Renovate Bot <renovate@local-it.org>",
username: "renovate",
platform: "gitea",
onboarding: true,
autodiscover: true,
persistRepoData: true,
optimizeForDisabled: true,
assignees: [],
labels: ["renovate", "dependencies", "automated"],
onboardingConfig: {
extends: ["config:recommended"],
},
repositories: ["local-it/mitgliederverwaltung"],
};

View file

@ -2,7 +2,7 @@ defmodule MvWeb.ErrorHTMLTest do
use MvWeb.ConnCase, async: true
# Bring render_to_string/4 for testing custom views
import Phoenix.Template
import Phoenix.Template, only: [render_to_string: 4]
test "renders 404.html" do
assert render_to_string(MvWeb.ErrorHTML, "404", "html", []) == "Not Found"

View file

@ -31,8 +31,7 @@ defmodule MvWeb.ConnCase do
end
end
setup tags do
Mv.DataCase.setup_sandbox(tags)
setup _tags do
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

View file

@ -1,2 +1 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Mv.Repo, :manual)