├── .formatter.exs ├── .gitignore ├── .tool-versions ├── Makefile ├── README.md ├── README.png ├── README ├── deploy-via-gigalixir │ ├── README.md │ └── index.md ├── install-via-asdf │ ├── README.md │ └── index.md ├── install-via-brew │ ├── README.md │ └── index.md ├── install-via-kerl │ ├── README.md │ └── index.md └── install-via-source │ └── index.md ├── assets ├── css │ └── app.css ├── js │ └── app.js ├── tailwind.config.js └── vendor │ └── topbar.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── cspell.json ├── lib ├── demo_elixir_phoenix.ex ├── demo_elixir_phoenix │ ├── account.ex │ ├── account │ │ └── user.ex │ ├── application.ex │ ├── mailer.ex │ └── repo.ex ├── demo_elixir_phoenix_web.ex └── demo_elixir_phoenix_web │ ├── components │ ├── core_components.ex │ ├── layouts.ex │ └── layouts │ │ ├── app.html.heex │ │ └── root.html.heex │ ├── controllers │ ├── error_html.ex │ ├── error_json.ex │ ├── page_controller.ex │ ├── page_html.ex │ ├── page_html │ │ └── home.html.heex │ ├── user_controller.ex │ ├── user_html.ex │ └── user_html │ │ ├── edit.html.heex │ │ ├── index.html.heex │ │ ├── new.html.heex │ │ ├── show.html.heex │ │ └── user_form.html.heex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ └── 20240625211751_create_users.exs │ └── seeds.exs └── static │ ├── favicon.ico │ ├── images │ └── logo.svg │ └── robots.txt └── test ├── demo_elixir_phoenix └── account_test.exs ├── demo_elixir_phoenix_web └── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ ├── page_controller_test.exs │ └── user_controller_test.exs ├── support ├── conn_case.ex ├── data_case.ex └── fixtures │ └── account_fixtures.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :ecto_sql, :phoenix], 3 | subdirectories: ["priv/*/migrations"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] 6 | ] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | demo_elixir_phoenix-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 27.1 2 | elixir 1.17.3-otp-27 3 | postgres 16.4 4 | nodejs 22.9.0 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: asdf 2 | asdf: 3 | # Erlang 4 | asdf plugin add erlang 5 | asdf install erlang latest 6 | asdf local erlang latest 7 | 8 | # Elixir 9 | asdf plugin add elixir 10 | asdf install elixir latest 11 | asdf local elixir latest 12 | 13 | # Postgres 14 | asdf plugin add postgres 15 | asdf install postgres latest 16 | asdf local postgres latest 17 | 18 | # Node 19 | asdf plugin add nodejs 20 | asdf install nodejs latest 21 | asdf local nodejs latest 22 | 23 | .PHONY: brew 24 | brew: 25 | # For building 26 | brew install autoconf 27 | brew upgrade autoconf 28 | 29 | # For building with OpenSSL 3.0 30 | brew install openssl 31 | brew upgrade openssl 32 | 33 | # For building with wxWidgets (start observer or debugger). Note that you may need to select the right wx-config before installing Erlang. 34 | brew install wxwidgets 35 | brew upgrade wxwidgets 36 | 37 | # For building PostgreSQL 38 | brew install gcc readline zlib curl ossp-uuid icu4c pkg-config 39 | brew update gcc readline zlib curl ossp-uuid icu4c pkg-config 40 | 41 | # For building documentation and elixir reference builds 42 | brew install libxslt fop 43 | brew upgrade libxslt fop 44 | 45 | # Erlang 46 | brew install erlang 47 | brew upgrade erlang 48 | 49 | # Elixir 50 | brew install elixir 51 | brew upgrade elixir 52 | 53 | # For PostgreSQL database 54 | brew install postgresql 55 | brew upgrade postgresql 56 | 57 | # For front end JavaScript development 58 | brew install node 59 | brew upgrade node 60 | 61 | # For using `asdf` to install more software 62 | brew install asdf 63 | brew upgrade asdf 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo Elixir Phoenix Gigalixir 2 | 3 | Elixir + Phoenix 4 | 5 | Demonstration of Elixir Phoenix, how to get it up and running, and [how to deploy via Gigalixir](README/deploy-via-gigalixir/). 6 | 7 | For this demo, we will use this application: [Demo Elixir Phoenix](https://github.com/joelparkerhenderson/demo-elixir-phoenix) 8 | 9 | If you prefer, you can create your own typical Elixir Phoenix application with your own typical Postgres database. 10 | 11 | There are various ways to install: 12 | 13 | ## Deploy via Gigalixir 14 | 15 | Gigalixir is a hosting service that specializes in hosting Elixir Phoenix applications. 16 | 17 | For docs: https://gigalixir.com/docs/ 18 | 19 | Install for macOS: 20 | 21 | ```sh 22 | python -m pip install gigalixir --user --ignore-installed six 23 | … 24 | ``` 25 | 26 | Make sure the executable is in your path, if it isn’t already. 27 | 28 | ```sh 29 | echo "export PATH=\$PATH:$(python3 -m site --user-base)/bin" >> ~/.profile 30 | source ~/.profile 31 | ``` 32 | 33 | Verify: 34 | 35 | ```sh 36 | gigalixir version 37 | 1.10.0 38 | ``` 39 | 40 | For help: 41 | 42 | ```sh 43 | gigalixir --help 44 | ``` 45 | 46 | 47 | ### Sign up and sign in 48 | 49 | If you're new to Gigalixir, then create your account: 50 | 51 | ```sh 52 | gigalixir signup 53 | GIGALIXIR Terms of Service: https://www.gigalixir.com/terms 54 | GIGALIXIR Privacy Policy: https://www.gigalixir.com/privacy 55 | Do you accept the Terms of Service and Privacy Policy? [y/N]: y 56 | Email: … 57 | ``` 58 | 59 | If you already use Gigalixir, then sign in: 60 | 61 | ```sh 62 | gigalixir login 63 | Email: alice@example.com 64 | Password: 65 | Would you like us to save your api key to your ~/.netrc file? [Y/n]: Y 66 | Logged in as alice@example.com. 67 | ``` 68 | 69 | Verify: 70 | 71 | ```sh 72 | gigalixir account 73 | ``` 74 | 75 | 76 | ### Configure prod.exs 77 | 78 | Gigalixir has options for how to set up an app. For this demo, we will choose the simplest additional option, which is to do deployments via Mix (rather than Distillery which is more sophisticated) and by using the Gigalixir database free tier. 79 | 80 | Append `config/prod.exs` with: 81 | 82 | ```elixir 83 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 84 | http: [port: {:system, "PORT"}], # Possibly not needed, but doesn't hurt 85 | url: [host: System.get_env("APP_NAME") <> ".gigalixirapp.com", port: 80], 86 | secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE"), 87 | server: true 88 | 89 | config :demo_elixir_phoenix, DemoElixirPhoenix.Repo, 90 | adapter: Ecto.Adapters.Postgres, 91 | url: System.get_env("DATABASE_URL"), 92 | ssl: true, 93 | pool_size: 2 # Free tier db only allows 4 connections. Rolling deploys need pool_size*(n+1) connections where n is the number of app replicas. 94 | ``` 95 | 96 | 97 | ### Create buildpack 98 | 99 | Create buildpack files at the repo root: 100 | 101 | ```elixir 102 | echo "elixir_version=1.10.4" > elixir_buildpack.config 103 | echo "erlang_version=23.0.2" >> elixir_buildpack.config 104 | echo "node_version=14.5.0" > phoenix_static_buildpack.config 105 | ``` 106 | 107 | Optionally verify that your versions are in the list of version support by the buildpack here: 108 | https://github.com/HashNuke/heroku-buildpack-elixir#version-support 109 | 110 | 111 | Create file `.buildpacks` with the buildpacks you want, such as: 112 | 113 | ```txt 114 | https://github.com/HashNuke/heroku-buildpack-elixir 115 | https://github.com/gjaldon/heroku-buildpack-phoenix-static 116 | https://github.com/gigalixir/gigalixir-buildpack-mix.git 117 | ``` 118 | 119 | 120 | ### Create the app 121 | 122 | Create the app with any name you want. There are some caveats: the name must be unique at Gigalixir, and can only contain letters, numbers, and dashes, and must start with a letter. 123 | 124 | ```sh 125 | gigalixir create -n demo-elixir-phoenix 126 | Created app: demo-elixir-phoenix. 127 | Set git remote: gigalixir. 128 | demo-elixir-phoenix 129 | ``` 130 | 131 | If you get either of these errors: 132 | 133 | ```json 134 | {"errors":{"unique_name":["has already been taken"]}} 135 | ``` 136 | 137 | ```json 138 | {"errors":{"unique_name":["can only contain letters, numbers, and dashes and must start with a letter."]}} 139 | ``` 140 | 141 | Verify the app exists: 142 | 143 | ```sh 144 | gigalixir apps 145 | ``` 146 | 147 | Output: 148 | 149 | ```json 150 | [ 151 | { 152 | "cloud": "gcp", 153 | "region": "v2018-us-central1", 154 | "replicas": 0, 155 | "size": 0.3, 156 | "stack": "gigalixir-18", 157 | "unique_name": "demo-elixir-phoenix" 158 | } 159 | ] 160 | ``` 161 | 162 | 163 | Verify the git remote exists: 164 | 165 | ```sh 166 | git remote -v | grep gigalixir 167 | ``` 168 | 169 | Output: 170 | 171 | ```sh 172 | gigalixir https://git.gigalixir.com/demo-elixir-phoenix.git/ (fetch) 173 | gigalixir https://git.gigalixir.com/demo-elixir-phoenix.git/ (push) 174 | ``` 175 | 176 | Try running the app with a server: 177 | 178 | ```sh 179 | mix phx.server 180 | ``` 181 | 182 | Try running the app with Interactive Elixir: 183 | 184 | ```sh 185 | iex -S mix phx.server 186 | ``` 187 | 188 | 189 | ### Install assets 190 | 191 | Optionally use PNPM or NPM for assets: 192 | 193 | ```sh 194 | pnpm install --prefix assets 195 | pnpm update --prefix assets 196 | ``` 197 | 198 | 199 | ### Create a database 200 | 201 | See https://gigalixir.readthedocs.io/en/latest/database.html#database-management 202 | 203 | To create a free database: 204 | 205 | ```sh 206 | gigalixir pg:create --free 207 | A word of caution: Free tier databases are not suitable for production 208 | and migrating from a free db to a standard db is not trivial. 209 | Do you wish to continue? [y/N]: y 210 | { 211 | "app_name": "demo-elixir-phoenix", 212 | "database": "f2a1aba9-72fb-42be-abb5-85ebdf2e887c", 213 | "host": "postgres-free-tier-1.gigalixir.com", 214 | "id": "14015dc3-1112-48ed-8059-0b1fcec7de7d", 215 | "password": "pw-16eeecc3-9d81-494e-a701-0a109a8e93e3", 216 | "port": 5432, 217 | "state": "AVAILABLE", 218 | "tier": "FREE", 219 | "url": "postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c", 220 | "username": "f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user" 221 | } 222 | ``` 223 | 224 | Save the database URL as an environment variables, such as by creating a file `.env.prod`: 225 | 226 | ```ini 227 | APP_NAME="demo-elixir-phoenix" 228 | DATABASE_URL="postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c" 229 | ``` 230 | 231 | Load the environment: 232 | 233 | ```sh 234 | source .env.prod 235 | ``` 236 | 237 | Verify: 238 | 239 | ```sh 240 | echo $DATABASE_URL 241 | postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c 242 | ``` 243 | 244 | To list the databases: 245 | 246 | ```sh 247 | gigalixir pg 248 | ``` 249 | 250 | To connect via psql console: 251 | 252 | ```sh 253 | psql $DATABASE_URL 254 | ``` 255 | 256 | 257 | ### Ignore files 258 | 259 | We choose to omit typical dot files and typical environment files. 260 | 261 | Add these lines to the file `.gitignore`: 262 | 263 | ```.gitignore 264 | # Ignore dot files by default, then accept specific files and patterns. 265 | .* 266 | !.gitignore 267 | !.*.gitignore 268 | !.*[-_.]example 269 | !.*[-_.]example[-_.]* 270 | !.*.gpg 271 | 272 | # Ignore env files by default, then accept specific files and patterns. 273 | env 274 | env[-_.]* 275 | !env[-_.]example 276 | !env[-_.]example[-_.]* 277 | !env[-_.]gpg 278 | !env[-_.]*.gpg 279 | ``` 280 | 281 | 282 | ### Verify production runs locally 283 | 284 | Verify an alternative production environment is able to run locally: 285 | 286 | ```sh 287 | APP_NAME=demo-phoenix-elixir \ 288 | SECRET_KEY_BASE="$(mix phx.gen.secret)" \ 289 | MIX_ENV=prod \ 290 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/demo_elixir_phoenix_prod" \ 291 | PORT=4000 \ 292 | mix phx.server 293 | ``` 294 | 295 | If you get this error: 296 | 297 | ```sh 298 | ** (Mix) Could not compile dependency :telemetry 299 | ``` 300 | 301 | Then refresh the dependency, then retry. 302 | 303 | ```sh 304 | mix deps.clean telemetry 305 | mix deps.get 306 | ``` 307 | 308 | 309 | ### Deploy 310 | 311 | 312 | Build and deploy: 313 | 314 | ```sh 315 | git push gigalixir master 316 | ``` 317 | 318 | 319 | ### Troubleshooting 320 | 321 | If error: 322 | 323 | ```sh 324 | Unable to select a buildpack 325 | ``` 326 | 327 | Then ensure you created a file `.buildpacks` in the project root directory. 328 | 329 | If error: 330 | 331 | ```sh 332 | remote: warning: You appear to have cloned an empty repository. 333 | ``` 334 | 335 | Then double-check that you have committed all your files, and pushed them. 336 | 337 | If error: 338 | 339 | ```sh 340 | fatal: the remote end hung up unexpectedly 341 | ``` 342 | 343 | Then try using a larger buffer, such as: 344 | 345 | ```sh 346 | git config --global http.postBuffer 100000000 347 | ``` 348 | 349 | If error: 350 | 351 | ``` 352 | npm ERR! Make sure you have the latest version of node.js and npm installed. 353 | ``` 354 | 355 | Then update node, such as: 356 | 357 | ```sh 358 | npm install --prefix assets npm 359 | npm update --prefix assets 360 | ``` 361 | 362 | If error: 363 | 364 | ```sh 365 | npm ERR! Failed at the @ deploy script 'webpack --mode production'. 366 | ``` 367 | 368 | Then try using webpack locally: 369 | 370 | ```sh 371 | npm install --prefix assets --save-dev webpack 372 | npm install --prefix assets --save-dev webpack-dev-server 373 | cd assets 374 | $(npm bin)/webpack --mode production 375 | ``` 376 | 377 | Output: 378 | 379 | ```sh 380 | Hash: 180d7e02464b28be7970 381 | Version: webpack 4.43.0 382 | Time: 1424ms 383 | Built at: 07/22/2020 9:31:27 AM 384 | Asset Size Chunks Chunk Names 385 | ../css/app.css 9.55 KiB 0 [emitted] app 386 | ../favicon.ico 1.23 KiB [emitted] 387 | ../images/phoenix.png 13.6 KiB [emitted] 388 | ../robots.txt 202 bytes [emitted] 389 | app.js 2.25 KiB 0 [emitted] app 390 | Entrypoint app = ../css/app.css app.js 391 | [0] multi ./js/app.js 28 bytes {0} [built] 392 | [1] ./js/app.js 490 bytes {0} [built] 393 | [2] ./css/app.scss 39 bytes {0} [built] 394 | [3] ../deps/phoenix_html/priv/static/phoenix_html.js 2.21 KiB {0} [built] 395 | + 2 hidden modules 396 | Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!css/app.scss: 397 | Entrypoint mini-css-extract-plugin = * 398 | [1] ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./css/app.scss 745 bytes {0} [built] 399 | [2] ./node_modules/css-loader/dist/cjs.js!./css/phoenix.css 10.4 KiB {0} [built] 400 | + 1 hidden module 401 | ``` 402 | 403 | If error: 404 | 405 | ```sh 406 | express-graphql@0.11.0 requires a peer of graphql@^14.7.0 || ^15.3.0 but none is installed. 407 | You must install peer dependencies yourself. 408 | ``` 409 | 410 | Then install: 411 | 412 | ```sh 413 | npm install --prefix assets graphql 414 | ``` 415 | 416 | If error: 417 | 418 | ```sh 419 | remote: cp: cannot overwrite directory '/tmp/cache/node_modules/phoenix' with non-directory 420 | remote: cp: cannot overwrite directory '/tmp/cache/node_modules/phoenix_html' with non-directory 421 | ``` 422 | 423 | Then you're likely trying to upgrade from an older version of your app, or Node, to a newer version, and the remote setup has changed. To solve this, tell the deployment to clean the node cache. Edit the file `phoenix_static_buildpack.config`, add this one line below, do one successful deploy, then remove the line (or set to false): 424 | 425 | ```ini 426 | clean_cache=true 427 | ``` 428 | 429 | 430 | 431 | ## Update the app as needed 432 | 433 | Update Elixir dependencies: 434 | 435 | ```sh 436 | mix deps.update --all 437 | ``` 438 | 439 | Update NPM: 440 | ```sh 441 | npm update --prefix assets 442 | ``` 443 | 444 | Verify buildpack: 445 | 446 | ```sh 447 | cat elixir_buildpack.config 448 | ``` 449 | 450 | Output such as: 451 | 452 | ```init 453 | elixir_version=1.10.4 454 | erlang_version=23.0.2 455 | ``` 456 | 457 | Verify buildpack: 458 | 459 | ``` 460 | cat phoenix_static_buildpack.config 461 | ``` 462 | 463 | Output such as: 464 | 465 | ```ini 466 | node_version=14.5.0 467 | ``` 468 | -------------------------------------------------------------------------------- /README.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelparkerhenderson/demo-elixir-phoenix/1cc89ade9e2c3d9370faf62d89e3859653c06eb7/README.png -------------------------------------------------------------------------------- /README/deploy-via-gigalixir/README.md: -------------------------------------------------------------------------------- 1 | index.md -------------------------------------------------------------------------------- /README/deploy-via-gigalixir/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Deploy via Gigalixir 4 | 5 | Gigalixir is a hosting service that specializes in hosting Elixir Phoenix applications. 6 | 7 | Install for macOS 8 | 9 | ```sh 10 | pip3 install gigalixir --user --ignore-installed six 11 | … 12 | ``` 13 | 14 | Make sure the executable is in your path, if it isn’t already. 15 | 16 | ```sh 17 | echo "export PATH=\$PATH:$(python3 -m site --user-base)/bin" >> ~/.profile 18 | source ~/.profile 19 | ``` 20 | 21 | Verify: 22 | 23 | ```sh 24 | gigalixir version 25 | 1.10.0 26 | ``` 27 | 28 | For help: 29 | 30 | ```sh 31 | gigalixir --help 32 | ``` 33 | 34 | For docs: https://gigalixir.com/docs/ 35 | 36 | 37 | ### Sign up and sign in 38 | 39 | If you're new to Gigalixir, then create your account: 40 | 41 | ```sh 42 | gigalixir signup 43 | GIGALIXIR Terms of Service: https://www.gigalixir.com/terms 44 | GIGALIXIR Privacy Policy: https://www.gigalixir.com/privacy 45 | Do you accept the Terms of Service and Privacy Policy? [y/N]: y 46 | Email: … 47 | ``` 48 | 49 | If you already use Gigalixir, then sign in: 50 | 51 | ```sh 52 | gigalixir login 53 | Email: alice@example.com 54 | Password: 55 | Would you like us to save your api key to your ~/.netrc file? [Y/n]: Y 56 | Logged in as alice@example.com. 57 | ``` 58 | 59 | Verify: 60 | 61 | ```sh 62 | gigalixir account 63 | ``` 64 | 65 | 66 | ### Configure prod.exs 67 | 68 | Gigalixir has options for how to set up an app. For this demo, we will choose the simplest additional option, which is to do deployments via Mix (rather than Distillery which is more sophisticated) and by using the Gigalixir database free tier. 69 | 70 | Append `config/prod.exs` with: 71 | 72 | ```elixir 73 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 74 | http: [port: {:system, "PORT"}], # Possibly not needed, but doesn't hurt 75 | url: [host: System.get_env("APP_NAME") <> ".gigalixirapp.com", port: 80], 76 | secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE"), 77 | server: true 78 | 79 | config :demo_elixir_phoenix, DemoElixirPhoenix.Repo, 80 | adapter: Ecto.Adapters.Postgres, 81 | url: System.get_env("DATABASE_URL"), 82 | ssl: true, 83 | pool_size: 2 # Free tier db only allows 4 connections. Rolling deploys need pool_size*(n+1) connections where n is the number of app replicas. 84 | ``` 85 | 86 | 87 | ### Create buildpack 88 | 89 | Create buildpack files at the repo root: 90 | 91 | ```elixir 92 | echo "elixir_version=1.10.4" > elixir_buildpack.config 93 | echo "erlang_version=23.0.2" >> elixir_buildpack.config 94 | echo "node_version=14.5.0" > phoenix_static_buildpack.config 95 | ``` 96 | 97 | Optionally verify that your versions are in the list of version support by the buildpack here: 98 | https://github.com/HashNuke/heroku-buildpack-elixir#version-support 99 | 100 | 101 | Create file `.buildpacks` with the buildpacks you want, such as: 102 | 103 | ```txt 104 | https://github.com/HashNuke/heroku-buildpack-elixir 105 | https://github.com/gjaldon/heroku-buildpack-phoenix-static 106 | https://github.com/gigalixir/gigalixir-buildpack-mix.git 107 | ``` 108 | 109 | 110 | ### Create the app 111 | 112 | Create the app with any name you want. There are some caveats: the name must be unique at Gigalixir, and can only contain letters, numbers, and dashes, and must start with a letter. 113 | 114 | ```sh 115 | gigalixir create -n demo-elixir-phoenix 116 | Created app: demo-elixir-phoenix. 117 | Set git remote: gigalixir. 118 | demo-elixir-phoenix 119 | ``` 120 | 121 | If you get either of these errors: 122 | 123 | ```json 124 | {"errors":{"unique_name":["has already been taken"]}} 125 | ``` 126 | 127 | ```json 128 | {"errors":{"unique_name":["can only contain letters, numbers, and dashes and must start with a letter."]}} 129 | ``` 130 | 131 | Verify the app exists: 132 | 133 | ```sh 134 | gigalixir apps 135 | ``` 136 | 137 | Output: 138 | 139 | ```json 140 | [ 141 | { 142 | "cloud": "gcp", 143 | "region": "v2018-us-central1", 144 | "replicas": 0, 145 | "size": 0.3, 146 | "stack": "gigalixir-18", 147 | "unique_name": "demo-elixir-phoenix" 148 | } 149 | ] 150 | ``` 151 | 152 | 153 | Verify the git remote exists: 154 | 155 | ```sh 156 | git remote -v | grep gigalixir 157 | ``` 158 | 159 | Output: 160 | 161 | ```sh 162 | gigalixir https://git.gigalixir.com/demo-elixir-phoenix.git/ (fetch) 163 | gigalixir https://git.gigalixir.com/demo-elixir-phoenix.git/ (push) 164 | ``` 165 | 166 | 167 | ### Create a database 168 | 169 | See https://gigalixir.readthedocs.io/en/latest/database.html#database-management 170 | 171 | To create a free database: 172 | 173 | ```sh 174 | gigalixir pg:create --free 175 | A word of caution: Free tier databases are not suitable for production 176 | and migrating from a free db to a standard db is not trivial. 177 | Do you wish to continue? [y/N]: y 178 | { 179 | "app_name": "demo-elixir-phoenix", 180 | "database": "f2a1aba9-72fb-42be-abb5-85ebdf2e887c", 181 | "host": "postgres-free-tier-1.gigalixir.com", 182 | "id": "14015dc3-1112-48ed-8059-0b1fcec7de7d", 183 | "password": "pw-16eeecc3-9d81-494e-a701-0a109a8e93e3", 184 | "port": 5432, 185 | "state": "AVAILABLE", 186 | "tier": "FREE", 187 | "url": "postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c", 188 | "username": "f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user" 189 | } 190 | ``` 191 | 192 | Save the datbase URL as an environment variables, such as by creating a file `.env.prod`: 193 | 194 | ```ini 195 | APP_NAME="demo-elixir-phoenix" 196 | DATABASE_URL="postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c" 197 | ``` 198 | 199 | Load the environment: 200 | 201 | ```sh 202 | source .env.prod 203 | ``` 204 | 205 | Verify: 206 | 207 | ```sh 208 | echo $DATABASE_URL 209 | postgresql://f2a1aba9-72fb-42be-abb5-85ebdf2e887c-user:pw-16eeecc3-9d81-494e-a701-0a109a8e93e3@postgres-free-tier-1.gigalixir.com:5432/f2a1aba9-72fb-42be-abb5-85ebdf2e887c 210 | ``` 211 | 212 | To list the databases: 213 | 214 | ```sh 215 | gigalixir pg 216 | ``` 217 | 218 | To connect via psql console: 219 | 220 | ```sh 221 | psql $DATABASE_URL 222 | ``` 223 | 224 | 225 | ### Ignore files 226 | 227 | We choose to omit typical dot files and typical environment files. 228 | 229 | Add these lines to the file `.gitignore`: 230 | 231 | ```.gitignore 232 | # Ignore dot files by default, then accept specific files and patterns. 233 | .* 234 | !.gitignore 235 | !.*.gitignore 236 | !.*[-_.]example 237 | !.*[-_.]example[-_.]* 238 | !.*.gpg 239 | 240 | # Ignore env files by default, then accept specific files and patterns. 241 | env 242 | env[-_.]* 243 | !env[-_.]example 244 | !env[-_.]example[-_.]* 245 | !env[-_.]gpg 246 | !env[-_.]*.gpg 247 | ``` 248 | 249 | 250 | ### Verify production runs locally 251 | 252 | Verify an alternative production environment is able to run locally: 253 | 254 | ```sh 255 | APP_NAME=demo-phoenix-elixir \ 256 | SECRET_KEY_BASE="$(mix phx.gen.secret)" \ 257 | MIX_ENV=prod \ 258 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/demo_elixir_phoenix_prod" \ 259 | PORT=4000 \ 260 | mix phx.server 261 | ``` 262 | 263 | If you get this error: 264 | 265 | ```sh 266 | ** (Mix) Could not compile dependency :telemetry 267 | ``` 268 | 269 | Then refresh the dependency, then retry. 270 | 271 | ```sh 272 | mix deps.clean telemetry 273 | mix deps.get 274 | ``` 275 | 276 | 277 | ### Deploy 278 | 279 | 280 | Build and deploy: 281 | 282 | ```sh 283 | git push gigalixir master 284 | ``` 285 | 286 | 287 | ### Troubleshooting 288 | 289 | If error: 290 | 291 | ```sh 292 | Unable to select a buildpack 293 | ``` 294 | 295 | Then ensure you created a file `.buildpacks` in the project root directory. 296 | 297 | If error: 298 | 299 | ```sh 300 | remote: warning: You appear to have cloned an empty repository. 301 | ``` 302 | 303 | Then double-check that you have committed all your files, and pushed them. 304 | 305 | If error: 306 | 307 | ```sh 308 | fatal: the remote end hung up unexpectedly 309 | ``` 310 | 311 | Then try using a larger buffer, such as: 312 | 313 | ```sh 314 | git config --global http.postBuffer 100000000 315 | ``` 316 | 317 | If error: 318 | 319 | ``` 320 | npm ERR! Make sure you have the latest version of node.js and npm installed. 321 | ``` 322 | 323 | Then update node, such as: 324 | 325 | ```sh 326 | npm install --prefix assets npm 327 | npm update --prefix assets 328 | ``` 329 | 330 | If error: 331 | 332 | ```sh 333 | npm ERR! Failed at the @ deploy script 'webpack --mode production'. 334 | ``` 335 | 336 | Then try using webpack locally: 337 | 338 | ```sh 339 | npm install --prefix assets --save-dev webpack 340 | npm install --prefix assets --save-dev webpack-dev-server 341 | cd assets 342 | $(npm bin)/webpack --mode production 343 | ``` 344 | 345 | Output: 346 | 347 | ```sh 348 | Hash: 180d7e02464b28be7970 349 | Version: webpack 4.43.0 350 | Time: 1424ms 351 | Built at: 07/22/2020 9:31:27 AM 352 | Asset Size Chunks Chunk Names 353 | ../css/app.css 9.55 KiB 0 [emitted] app 354 | ../favicon.ico 1.23 KiB [emitted] 355 | ../images/phoenix.png 13.6 KiB [emitted] 356 | ../robots.txt 202 bytes [emitted] 357 | app.js 2.25 KiB 0 [emitted] app 358 | Entrypoint app = ../css/app.css app.js 359 | [0] multi ./js/app.js 28 bytes {0} [built] 360 | [1] ./js/app.js 490 bytes {0} [built] 361 | [2] ./css/app.scss 39 bytes {0} [built] 362 | [3] ../deps/phoenix_html/priv/static/phoenix_html.js 2.21 KiB {0} [built] 363 | + 2 hidden modules 364 | Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!css/app.scss: 365 | Entrypoint mini-css-extract-plugin = * 366 | [1] ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./css/app.scss 745 bytes {0} [built] 367 | [2] ./node_modules/css-loader/dist/cjs.js!./css/phoenix.css 10.4 KiB {0} [built] 368 | + 1 hidden module 369 | ``` 370 | 371 | If error: 372 | 373 | ```sh 374 | express-graphql@0.11.0 requires a peer of graphql@^14.7.0 || ^15.3.0 but none is installed. 375 | You must install peer dependencies yourself. 376 | ``` 377 | 378 | Then install: 379 | 380 | ```sh 381 | npm install --prefix assets graphql 382 | ``` 383 | 384 | If error: 385 | 386 | ```sh 387 | remote: cp: cannot overwrite directory '/tmp/cache/node_modules/phoenix' with non-directory 388 | remote: cp: cannot overwrite directory '/tmp/cache/node_modules/phoenix_html' with non-directory 389 | ``` 390 | 391 | Then you're likely trying to upgrade from an older version of your app, or Node, to a newer version, and the remote setup has changed. To solve this, tell the deployment to clean the node cache. Edit the file `phoenix_static_buildpack.config`, add this one line below, do one successful deploy, then remove the line (or set to false): 392 | 393 | ```ini 394 | clean_cache=true 395 | ``` 396 | -------------------------------------------------------------------------------- /README/install-via-asdf/README.md: -------------------------------------------------------------------------------- 1 | index.md -------------------------------------------------------------------------------- /README/install-via-asdf/index.md: -------------------------------------------------------------------------------- 1 | # Install via adsf 2 | 3 | Shortcut: 4 | 5 | ```sh 6 | make asdf 7 | ``` 8 | 9 | 10 | ## Install Erlang via asdf 11 | 12 | Run: 13 | 14 | ```sh 15 | asdf plugin add erlang 16 | asdf install erlang latest 17 | asdf local erlang latest 18 | ``` 19 | 20 | Success looks like: 21 | 22 | ```sh 23 | Erlang/OTP 27.1 (asdf_27.1) has been successfully built 24 | ``` 25 | 26 | Verify: 27 | 28 | ```sh 29 | erl --version 30 | Erlang/OTP 27 [erts-15.0] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] 31 | ``` 32 | 33 | 34 | ## Install Elixir via asdf 35 | 36 | Run: 37 | 38 | ```sh 39 | asdf plugin add elixir 40 | asdf install elixir latest 41 | asdf local elixir latest 42 | ``` 43 | 44 | Verify: 45 | 46 | ```sh 47 | elixir --version 48 | Erlang/OTP 27 [erts-15.1] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] 49 | Elixir 1.17.3 (compiled with Erlang/OTP 27) 50 | ``` 51 | 52 | 53 | ## Install Node JavaScript via asdf 54 | 55 | Run: 56 | 57 | ```sh 58 | asdf plugin add nodejs 59 | asdf install nodejs latest 60 | asdf local nodejs latest 61 | ``` 62 | 63 | Verify: 64 | 65 | ```sh 66 | node --version 67 | v22.9.0 68 | ``` 69 | 70 | 71 | ## Install PostgreSQL database via asdf 72 | 73 | Run: 74 | 75 | ```sh 76 | asdf plugin add postgres 77 | asdf install postgres latest 78 | asdf local postgres latest 79 | ``` 80 | 81 | Troubleshooting: if you get any errors about "crypto" or "SSL", and also you installed some prerequisites via macOS brew, then you might need to set paths then retry: 82 | 83 | ```sh 84 | export HOMEBREW_PREFIX=$(brew --prefix) 85 | export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/bin/pkg-config:$(brew --prefix openssl)/lib/pkgconfig/:$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix curl)/lib/pkgconfig:$(brew --prefix zlib)/lib/pkgconfig" 86 | ``` 87 | 88 | Success looks like: 89 | 90 | ```sh 91 | Success. You can now start the database server using: 92 | $HOME/.asdf/installs/postgres/16.4/bin/pg_ctl -D $HOME/.asdf/installs/postgres/16.4/data -l logfile start 93 | ``` 94 | 95 | Verify: 96 | 97 | ```sh 98 | psql --version 99 | psql (PostgreSQL) 16.4 100 | ``` 101 | 102 | Start: 103 | 104 | ```sh 105 | $HOME/.asdf/installs/postgres/16.4/bin/pg_ctl -D $HOME/.asdf/installs/postgres/16.4/data -l logfile start 106 | ``` 107 | 108 | Success looks like: 109 | 110 | ```sh 111 | server started 112 | ``` 113 | 114 | The Postgres installation automatically creates a superuser role named with your macOS username. 115 | 116 | Verify you can connect to the Postgres server by using the macOS user name and default database name: 117 | 118 | ```sh 119 | psql postgres 120 | ``` 121 | 122 | Verify you can connect to the Postgres server by using defaults: 123 | 124 | ```sh 125 | psql --username postgres postgres 126 | ``` 127 | -------------------------------------------------------------------------------- /README/install-via-brew/README.md: -------------------------------------------------------------------------------- 1 | index.md -------------------------------------------------------------------------------- /README/install-via-brew/index.md: -------------------------------------------------------------------------------- 1 | # Install via brew 2 | 3 | Shortcut: 4 | 5 | ```sh 6 | make brew 7 | ``` 8 | 9 | 10 | ## Preflight 11 | 12 | 13 | ### For building 14 | 15 | Install: 16 | 17 | ```sh 18 | brew install autoconf 19 | ``` 20 | 21 | 22 | ### For building with OpenSSL 23 | 24 | Install: 25 | 26 | ```sh 27 | brew install openssl 28 | ``` 29 | 30 | Find the path: 31 | 32 | ```sh 33 | brew --prefix openssl 34 | ``` 35 | 36 | Append the path to the `PKG_CONFIG_PATH` environment variable: 37 | 38 | ```sh 39 | export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/opt/openssl@3/lib/pkgconfig/" 40 | ``` 41 | 42 | If you ever get this error message: 43 | 44 | ```sh 45 | configure: error: library 'crypto' is required for OpenSSL 46 | ``` 47 | 48 | Or this error message: 49 | 50 | ```sh 51 | checking for CRYPTO_new_ex_data in -lcrypto... no 52 | ``` 53 | 54 | Then figure out what's wrong with your OpenSSL installation. 55 | 56 | 57 | ### For documentation 58 | 59 | For building documentation and elixir reference builds: 60 | 61 | ```sh 62 | brew install libxslt fop 63 | ``` 64 | 65 | For using `asdf` to install more software: 66 | 67 | ```sh 68 | brew install asdf 69 | ``` 70 | 71 | 72 | ### For PostgreSQL 73 | 74 | Install: 75 | 76 | ```sh 77 | brew install gcc readline zlib curl ossp-uuid icu4c pkg-config 78 | ``` 79 | 80 | You might need: 81 | 82 | ```sh 83 | export HOMEBREW_PREFIX=$(brew --prefix) 84 | ``` 85 | 86 | You might need to append package configuration paths: 87 | 88 | ```sh 89 | export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/bin/pkg-config:$(brew --prefix openssl)/lib/pkgconfig/:$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix curl)/lib/pkgconfig:$(brew --prefix zlib)/lib/pkgconfig" 90 | ``` 91 | 92 | 93 | ### For wxWidgets 94 | 95 | For building with wxWidgets (start observer or debugger). Note that you may need to select the right wx-config before installing Erlang. 96 | 97 | ```sh 98 | brew install wxwidgets 99 | ``` 100 | 101 | 102 | ### Upgrade 103 | 104 | To upgrade any time: 105 | 106 | ```sh 107 | brew upgrade 108 | ``` 109 | 110 | 111 | ## Install Erlang via brew 112 | 113 | Run: 114 | 115 | ```sh 116 | brew install erlang 117 | brew upgrade erlang 118 | ``` 119 | 120 | Verify Erlang: 121 | 122 | ```sh 123 | erl --version 124 | Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] 125 | ``` 126 | 127 | ## Install Elixir via brew 128 | 129 | Run: 130 | 131 | ```sh 132 | brew install elixir 133 | brew upgrade elixir 134 | ``` 135 | 136 | Set path: 137 | 138 | ```sh 139 | path=$(brew --cellar elixir) 140 | version=$(brew list --versions elixir | head -1 | awk '{print $2}') 141 | export PATH="$PATH:$path/$version/bin" 142 | ``` 143 | 144 | Verify: 145 | 146 | ```sh 147 | elixir --version 148 | ``` 149 | 150 | Output: 151 | 152 | ```txt 153 | Erlang/OTP 27 [erts-15.1.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] 154 | 155 | Elixir 1.17.3 (compiled with Erlang/OTP 26) 156 | ``` 157 | 158 | 159 | ## Install Node JavaScript via brew 160 | 161 | Install: 162 | 163 | ```sh 164 | brew install node 165 | brew upgrade node 166 | ``` 167 | 168 | Verify: 169 | 170 | ```sh 171 | node --version 172 | v22.9.0 173 | ``` 174 | 175 | 176 | ## Install PostgreSQL database via brew 177 | 178 | Run: 179 | 180 | ```sh 181 | brew install postgresql 182 | ``` 183 | 184 | Verify: 185 | 186 | ```sh 187 | psql --version 188 | psql (PostgreSQL) 16.4 189 | ``` 190 | 191 | Start: 192 | 193 | ```sh 194 | brew services start postgresql@16 195 | ==> Successfully started `postgresql@16` (label: homebrew.mxcl.postgresql@16) 196 | ``` 197 | 198 | The Brew installation automatically creates a superuser role named with your macOS username. 199 | 200 | Verify you can connect to the Postgres server by using your macOS user name and default database name: 201 | 202 | ```sh 203 | psql --username "$USER" postgres 204 | ``` 205 | -------------------------------------------------------------------------------- /README/install-via-kerl/README.md: -------------------------------------------------------------------------------- 1 | index.md -------------------------------------------------------------------------------- /README/install-via-kerl/index.md: -------------------------------------------------------------------------------- 1 | ## Install via kerl 2 | 3 | Kerl is an erlang-specific version manager. Kerl keeps tracks of the releases it downloads, builds and installs, allowing easy installations to new destinations (without complete rebuilding) and easy switches between Erlang/OTP installations. 4 | 5 | Install `kerl` on macOS via brew: 6 | 7 | ```sh 8 | brew install kerl 9 | brew upgrade kerl 10 | ``` 11 | 12 | Run: 13 | 14 | ```sh 15 | kerl build 27.0 27.0 16 | ``` 17 | -------------------------------------------------------------------------------- /README/install-via-source/index.md: -------------------------------------------------------------------------------- 1 | # Install via source 2 | 3 | Install Erlang: 4 | 5 | ```sh 6 | wget https://github.com/erlang/otp/releases/download/OTP-27.0/otp_src_27.0.tar.gz 7 | tar xvf otp_src_27.0.tar.gz 8 | cd otp_src_27.0 9 | ./configure && make && sudo make install 10 | ``` 11 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, { 27 | longPollFallbackMs: 2500, 28 | params: {_csrf_token: csrfToken} 29 | }) 30 | 31 | // Show progress bar on live navigation and form submits 32 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 33 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 34 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 35 | 36 | // connect if there are any LiveViews on the page 37 | liveSocket.connect() 38 | 39 | // expose liveSocket on window for web console debug logs and latency simulation: 40 | // >> liveSocket.enableDebug() 41 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 42 | // >> liveSocket.disableLatencySim() 43 | window.liveSocket = liveSocket 44 | 45 | -------------------------------------------------------------------------------- /assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | const fs = require("fs") 6 | const path = require("path") 7 | 8 | module.exports = { 9 | content: [ 10 | "./js/**/*.js", 11 | "../lib/demo_elixir_phoenix_web.ex", 12 | "../lib/demo_elixir_phoenix_web/**/*.*ex" 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | brand: "#FD4F00", 18 | } 19 | }, 20 | }, 21 | plugins: [ 22 | require("@tailwindcss/forms"), 23 | // Allows prefixing tailwind classes with LiveView classes to add rules 24 | // only when LiveView classes are applied, for example: 25 | // 26 | //
27 | // 28 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 29 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 30 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), 31 | 32 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle 33 | // See your `CoreComponents.icon/1` for more information. 34 | // 35 | plugin(function({matchComponents, theme}) { 36 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") 37 | let values = {} 38 | let icons = [ 39 | ["", "/24/outline"], 40 | ["-solid", "/24/solid"], 41 | ["-mini", "/20/solid"], 42 | ["-micro", "/16/solid"] 43 | ] 44 | icons.forEach(([suffix, dir]) => { 45 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { 46 | let name = path.basename(file, ".svg") + suffix 47 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)} 48 | }) 49 | }) 50 | matchComponents({ 51 | "hero": ({name, fullPath}) => { 52 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") 53 | let size = theme("spacing.6") 54 | if (name.endsWith("-mini")) { 55 | size = theme("spacing.5") 56 | } else if (name.endsWith("-micro")) { 57 | size = theme("spacing.4") 58 | } 59 | return { 60 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, 61 | "-webkit-mask": `var(--hero-${name})`, 62 | "mask": `var(--hero-${name})`, 63 | "mask-repeat": "no-repeat", 64 | "background-color": "currentColor", 65 | "vertical-align": "middle", 66 | "display": "inline-block", 67 | "width": size, 68 | "height": size 69 | } 70 | } 71 | }, {values}) 72 | }) 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :demo_elixir_phoenix, 11 | ecto_repos: [DemoElixirPhoenix.Repo], 12 | generators: [timestamp_type: :utc_datetime] 13 | 14 | # Configures the endpoint 15 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 16 | url: [host: "localhost"], 17 | adapter: Bandit.PhoenixAdapter, 18 | render_errors: [ 19 | formats: [html: DemoElixirPhoenixWeb.ErrorHTML, json: DemoElixirPhoenixWeb.ErrorJSON], 20 | layout: false 21 | ], 22 | pubsub_server: DemoElixirPhoenix.PubSub, 23 | live_view: [signing_salt: "gUrVqoHz"] 24 | 25 | # Configures the mailer 26 | # 27 | # By default it uses the "Local" adapter which stores the emails 28 | # locally. You can see the emails in your browser, at "/dev/mailbox". 29 | # 30 | # For production it's recommended to configure a different adapter 31 | # at the `config/runtime.exs`. 32 | config :demo_elixir_phoenix, DemoElixirPhoenix.Mailer, adapter: Swoosh.Adapters.Local 33 | 34 | # Configure esbuild (the version is required) 35 | config :esbuild, 36 | version: "0.17.11", 37 | demo_elixir_phoenix: [ 38 | args: 39 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 40 | cd: Path.expand("../assets", __DIR__), 41 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 42 | ] 43 | 44 | # Configure tailwind (the version is required) 45 | config :tailwind, 46 | version: "3.4.3", 47 | demo_elixir_phoenix: [ 48 | args: ~w( 49 | --config=tailwind.config.js 50 | --input=css/app.css 51 | --output=../priv/static/assets/app.css 52 | ), 53 | cd: Path.expand("../assets", __DIR__) 54 | ] 55 | 56 | # Configures Elixir's Logger 57 | config :logger, :console, 58 | format: "$time $metadata[$level] $message\n", 59 | metadata: [:request_id] 60 | 61 | # Use Jason for JSON parsing in Phoenix 62 | config :phoenix, :json_library, Jason 63 | 64 | # Import environment specific config. This must remain at the bottom 65 | # of this file so it overrides the configuration defined above. 66 | import_config "#{config_env()}.exs" 67 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | config :demo_elixir_phoenix, DemoElixirPhoenix.Repo, 5 | username: "demo_elixir_phoenix", 6 | password: "a9ed78cd8e4aa2bd2a37ad7319899106", 7 | hostname: "localhost", 8 | database: "demo_elixir_phoenix_dev", 9 | stacktrace: true, 10 | show_sensitive_data_on_connection_error: true, 11 | pool_size: 10 12 | 13 | # For development, we disable any cache and enable 14 | # debugging and code reloading. 15 | # 16 | # The watchers configuration can be used to run external 17 | # watchers to your application. For example, we can use it 18 | # to bundle .js and .css sources. 19 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 20 | # Binding to loopback ipv4 address prevents access from other machines. 21 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 22 | http: [ip: {127, 0, 0, 1}, port: 4000], 23 | check_origin: false, 24 | code_reloader: true, 25 | debug_errors: true, 26 | secret_key_base: "K17LoAjf/cctJwk3wv+qWB6I7G4s7BqLDsdD5W2g59KdXXsPbPXRqsIabJMH2oOe", 27 | watchers: [ 28 | esbuild: {Esbuild, :install_and_run, [:demo_elixir_phoenix, ~w(--sourcemap=inline --watch)]}, 29 | tailwind: {Tailwind, :install_and_run, [:demo_elixir_phoenix, ~w(--watch)]} 30 | ] 31 | 32 | # ## SSL Support 33 | # 34 | # In order to use HTTPS in development, a self-signed 35 | # certificate can be generated by running the following 36 | # Mix task: 37 | # 38 | # mix phx.gen.cert 39 | # 40 | # Run `mix help phx.gen.cert` for more information. 41 | # 42 | # The `http:` config above can be replaced with: 43 | # 44 | # https: [ 45 | # port: 4001, 46 | # cipher_suite: :strong, 47 | # keyfile: "priv/cert/selfsigned_key.pem", 48 | # certfile: "priv/cert/selfsigned.pem" 49 | # ], 50 | # 51 | # If desired, both `http:` and `https:` keys can be 52 | # configured to run both http and https servers on 53 | # different ports. 54 | 55 | # Watch static and templates for browser reloading. 56 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 57 | live_reload: [ 58 | patterns: [ 59 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 60 | ~r"priv/gettext/.*(po)$", 61 | ~r"lib/demo_elixir_phoenix_web/(controllers|live|components)/.*(ex|heex)$" 62 | ] 63 | ] 64 | 65 | # Enable dev routes for dashboard and mailbox 66 | config :demo_elixir_phoenix, dev_routes: true 67 | 68 | # Do not include metadata nor timestamps in development logs 69 | config :logger, :console, format: "[$level] $message\n" 70 | 71 | # Set a higher stacktrace during development. Avoid configuring such 72 | # in production as building large stacktraces may be expensive. 73 | config :phoenix, :stacktrace_depth, 20 74 | 75 | # Initialize plugs at runtime for faster development compilation 76 | config :phoenix, :plug_init_mode, :runtime 77 | 78 | config :phoenix_live_view, 79 | # Include HEEx debug annotations as HTML comments in rendered markup 80 | debug_heex_annotations: true, 81 | # Enable helpful, but potentially expensive runtime checks 82 | enable_expensive_runtime_checks: true 83 | 84 | # Disable swoosh api client as it is only required for production adapters. 85 | config :swoosh, :api_client, false 86 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Configures Swoosh API Client 12 | config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: DemoElixirPhoenix.Finch 13 | 14 | # Disable Swoosh Local Memory Storage 15 | config :swoosh, local: false 16 | 17 | # Do not print debug messages in production 18 | config :logger, level: :info 19 | 20 | # Runtime production configuration, including reading 21 | # of environment variables, is done on config/runtime.exs. 22 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/demo_elixir_phoenix start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | database_url = 25 | System.get_env("DATABASE_URL") || 26 | raise """ 27 | environment variable DATABASE_URL is missing. 28 | For example: ecto://USER:PASS@HOST/DATABASE 29 | """ 30 | 31 | maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] 32 | 33 | config :demo_elixir_phoenix, DemoElixirPhoenix.Repo, 34 | # ssl: true, 35 | url: database_url, 36 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 37 | socket_options: maybe_ipv6 38 | 39 | # The secret key base is used to sign/encrypt cookies and other secrets. 40 | # A default value is used in config/dev.exs and config/test.exs but you 41 | # want to use a different value for prod and you most likely don't want 42 | # to check this value into version control, so we use an environment 43 | # variable instead. 44 | secret_key_base = 45 | System.get_env("SECRET_KEY_BASE") || 46 | raise """ 47 | environment variable SECRET_KEY_BASE is missing. 48 | You can generate one by calling: mix phx.gen.secret 49 | """ 50 | 51 | host = System.get_env("PHX_HOST") || "example.com" 52 | port = String.to_integer(System.get_env("PORT") || "4000") 53 | 54 | config :demo_elixir_phoenix, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 55 | 56 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 57 | url: [host: host, port: 443, scheme: "https"], 58 | http: [ 59 | # Enable IPv6 and bind on all interfaces. 60 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 61 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 62 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 63 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 64 | port: port 65 | ], 66 | secret_key_base: secret_key_base 67 | 68 | # ## SSL Support 69 | # 70 | # To get SSL working, you will need to add the `https` key 71 | # to your endpoint configuration: 72 | # 73 | # config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 74 | # https: [ 75 | # ..., 76 | # port: 443, 77 | # cipher_suite: :strong, 78 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 79 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 80 | # ] 81 | # 82 | # The `cipher_suite` is set to `:strong` to support only the 83 | # latest and more secure SSL ciphers. This means old browsers 84 | # and clients may not be supported. You can set it to 85 | # `:compatible` for wider support. 86 | # 87 | # `:keyfile` and `:certfile` expect an absolute path to the key 88 | # and cert in disk or a relative path inside priv, for example 89 | # "priv/ssl/server.key". For all supported SSL configuration 90 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 91 | # 92 | # We also recommend setting `force_ssl` in your config/prod.exs, 93 | # ensuring no data is ever sent via http, always redirecting to https: 94 | # 95 | # config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 96 | # force_ssl: [hsts: true] 97 | # 98 | # Check `Plug.SSL` for all available options in `force_ssl`. 99 | 100 | # ## Configuring the mailer 101 | # 102 | # In production you need to configure the mailer to use a different adapter. 103 | # Also, you may need to configure the Swoosh API client of your choice if you 104 | # are not using SMTP. Here is an example of the configuration: 105 | # 106 | # config :demo_elixir_phoenix, DemoElixirPhoenix.Mailer, 107 | # adapter: Swoosh.Adapters.Mailgun, 108 | # api_key: System.get_env("MAILGUN_API_KEY"), 109 | # domain: System.get_env("MAILGUN_DOMAIN") 110 | # 111 | # For this example you need include a HTTP client required by Swoosh API client. 112 | # Swoosh supports Hackney and Finch out of the box: 113 | # 114 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 115 | # 116 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 117 | end 118 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :demo_elixir_phoenix, DemoElixirPhoenix.Repo, 9 | username: "demo_elixir_phoenix", 10 | password: "a9ed78cd8e4aa2bd2a37ad7319899106", 11 | hostname: "localhost", 12 | database: "demo_elixir_phoenix_test#{System.get_env("MIX_TEST_PARTITION")}", 13 | pool: Ecto.Adapters.SQL.Sandbox, 14 | pool_size: System.schedulers_online() * 2 15 | 16 | # We don't run a server during test. If one is required, 17 | # you can enable the server option below. 18 | config :demo_elixir_phoenix, DemoElixirPhoenixWeb.Endpoint, 19 | http: [ip: {127, 0, 0, 1}, port: 4002], 20 | secret_key_base: "wqy/PJdCB0AfeH14d0FmSpJQEwH5cmhCSd/06EqSXeZLWgbqqGNFniCiXQrgXDSX", 21 | server: false 22 | 23 | # In test we don't send emails 24 | config :demo_elixir_phoenix, DemoElixirPhoenix.Mailer, adapter: Swoosh.Adapters.Test 25 | 26 | # Disable swoosh api client as it is only required for production adapters 27 | config :swoosh, :api_client, false 28 | 29 | # Print only warnings and errors during test 30 | config :logger, level: :warning 31 | 32 | # Initialize plugs at runtime for faster test compilation 33 | config :phoenix, :plug_init_mode, :runtime 34 | 35 | # Enable helpful, but potentially expensive runtime checks 36 | config :phoenix_live_view, 37 | enable_expensive_runtime_checks: true 38 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [], 6 | "words": [ 7 | "buildpack", 8 | "gigalixir" 9 | ], 10 | "ignoreWords": [], 11 | "import": [] 12 | } -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix do 2 | @moduledoc """ 3 | DemoElixirPhoenix keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix/account.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Account do 2 | @moduledoc """ 3 | The Account context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias DemoElixirPhoenix.Repo 8 | 9 | alias DemoElixirPhoenix.Account.User 10 | 11 | @doc """ 12 | Returns the list of users. 13 | 14 | ## Examples 15 | 16 | iex> list_users() 17 | [%User{}, ...] 18 | 19 | """ 20 | def list_users do 21 | Repo.all(User) 22 | end 23 | 24 | @doc """ 25 | Gets a single user. 26 | 27 | Raises `Ecto.NoResultsError` if the User does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_user!(123) 32 | %User{} 33 | 34 | iex> get_user!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_user!(id), do: Repo.get!(User, id) 39 | 40 | @doc """ 41 | Creates a user. 42 | 43 | ## Examples 44 | 45 | iex> create_user(%{field: value}) 46 | {:ok, %User{}} 47 | 48 | iex> create_user(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_user(attrs \\ %{}) do 53 | %User{} 54 | |> User.changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Updates a user. 60 | 61 | ## Examples 62 | 63 | iex> update_user(user, %{field: new_value}) 64 | {:ok, %User{}} 65 | 66 | iex> update_user(user, %{field: bad_value}) 67 | {:error, %Ecto.Changeset{}} 68 | 69 | """ 70 | def update_user(%User{} = user, attrs) do 71 | user 72 | |> User.changeset(attrs) 73 | |> Repo.update() 74 | end 75 | 76 | @doc """ 77 | Deletes a user. 78 | 79 | ## Examples 80 | 81 | iex> delete_user(user) 82 | {:ok, %User{}} 83 | 84 | iex> delete_user(user) 85 | {:error, %Ecto.Changeset{}} 86 | 87 | """ 88 | def delete_user(%User{} = user) do 89 | Repo.delete(user) 90 | end 91 | 92 | @doc """ 93 | Returns an `%Ecto.Changeset{}` for tracking user changes. 94 | 95 | ## Examples 96 | 97 | iex> change_user(user) 98 | %Ecto.Changeset{data: %User{}} 99 | 100 | """ 101 | def change_user(%User{} = user, attrs \\ %{}) do 102 | User.changeset(user, attrs) 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix/account/user.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Account.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "users" do 6 | field :name, :string 7 | field :email, :string 8 | 9 | timestamps(type: :utc_datetime) 10 | end 11 | 12 | @doc false 13 | def changeset(user, attrs) do 14 | user 15 | |> cast(attrs, [:name, :email]) 16 | |> validate_required([:name, :email]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix/application.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | DemoElixirPhoenixWeb.Telemetry, 12 | DemoElixirPhoenix.Repo, 13 | {DNSCluster, query: Application.get_env(:demo_elixir_phoenix, :dns_cluster_query) || :ignore}, 14 | {Phoenix.PubSub, name: DemoElixirPhoenix.PubSub}, 15 | # Start the Finch HTTP client for sending emails 16 | {Finch, name: DemoElixirPhoenix.Finch}, 17 | # Start a worker by calling: DemoElixirPhoenix.Worker.start_link(arg) 18 | # {DemoElixirPhoenix.Worker, arg}, 19 | # Start to serve requests, typically the last entry 20 | DemoElixirPhoenixWeb.Endpoint 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: DemoElixirPhoenix.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | @impl true 32 | def config_change(changed, _new, removed) do 33 | DemoElixirPhoenixWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Mailer do 2 | use Swoosh.Mailer, otp_app: :demo_elixir_phoenix 3 | end 4 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Repo do 2 | use Ecto.Repo, 3 | otp_app: :demo_elixir_phoenix, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use DemoElixirPhoenixWeb, :controller 9 | use DemoElixirPhoenixWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: DemoElixirPhoenixWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import DemoElixirPhoenixWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {DemoElixirPhoenixWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | end 59 | end 60 | 61 | def live_component do 62 | quote do 63 | use Phoenix.LiveComponent 64 | 65 | unquote(html_helpers()) 66 | end 67 | end 68 | 69 | def html do 70 | quote do 71 | use Phoenix.Component 72 | 73 | # Import convenience functions from controllers 74 | import Phoenix.Controller, 75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 76 | 77 | # Include general helpers for rendering HTML 78 | unquote(html_helpers()) 79 | end 80 | end 81 | 82 | defp html_helpers do 83 | quote do 84 | # HTML escaping functionality 85 | import Phoenix.HTML 86 | # Core UI components and translation 87 | import DemoElixirPhoenixWeb.CoreComponents 88 | import DemoElixirPhoenixWeb.Gettext 89 | 90 | # Shortcut for generating JS commands 91 | alias Phoenix.LiveView.JS 92 | 93 | # Routes generation with the ~p sigil 94 | unquote(verified_routes()) 95 | end 96 | end 97 | 98 | def verified_routes do 99 | quote do 100 | use Phoenix.VerifiedRoutes, 101 | endpoint: DemoElixirPhoenixWeb.Endpoint, 102 | router: DemoElixirPhoenixWeb.Router, 103 | statics: DemoElixirPhoenixWeb.static_paths() 104 | end 105 | end 106 | 107 | @doc """ 108 | When used, dispatch to the appropriate controller/live_view/etc. 109 | """ 110 | defmacro __using__(which) when is_atom(which) do 111 | apply(__MODULE__, which, []) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/components/core_components.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.CoreComponents do 2 | @moduledoc """ 3 | Provides core UI components. 4 | 5 | At first glance, this module may seem daunting, but its goal is to provide 6 | core building blocks for your application, such as modals, tables, and 7 | forms. The components consist mostly of markup and are well-documented 8 | with doc strings and declarative assigns. You may customize and style 9 | them in any way you want, based on your application growth and needs. 10 | 11 | The default components use Tailwind CSS, a utility-first CSS framework. 12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn 13 | how to customize them or feel free to swap in another framework altogether. 14 | 15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. 16 | """ 17 | use Phoenix.Component 18 | 19 | alias Phoenix.LiveView.JS 20 | import DemoElixirPhoenixWeb.Gettext 21 | 22 | @doc """ 23 | Renders a modal. 24 | 25 | ## Examples 26 | 27 | <.modal id="confirm-modal"> 28 | This is a modal. 29 | 30 | 31 | JS commands may be passed to the `:on_cancel` to configure 32 | the closing/cancel event, for example: 33 | 34 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> 35 | This is another modal. 36 | 37 | 38 | """ 39 | attr :id, :string, required: true 40 | attr :show, :boolean, default: false 41 | attr :on_cancel, JS, default: %JS{} 42 | slot :inner_block, required: true 43 | 44 | def modal(assigns) do 45 | ~H""" 46 | 328 | """ 329 | end 330 | 331 | def input(%{type: "select"} = assigns) do 332 | ~H""" 333 |
334 | <.label for={@id}><%= @label %> 335 | 345 | <.error :for={msg <- @errors}><%= msg %> 346 |
347 | """ 348 | end 349 | 350 | def input(%{type: "textarea"} = assigns) do 351 | ~H""" 352 |
353 | <.label for={@id}><%= @label %> 354 | 364 | <.error :for={msg <- @errors}><%= msg %> 365 |
366 | """ 367 | end 368 | 369 | # All other inputs text, datetime-local, url, password, etc. are handled here... 370 | def input(assigns) do 371 | ~H""" 372 |
373 | <.label for={@id}><%= @label %> 374 | 386 | <.error :for={msg <- @errors}><%= msg %> 387 |
388 | """ 389 | end 390 | 391 | @doc """ 392 | Renders a label. 393 | """ 394 | attr :for, :string, default: nil 395 | slot :inner_block, required: true 396 | 397 | def label(assigns) do 398 | ~H""" 399 | 402 | """ 403 | end 404 | 405 | @doc """ 406 | Generates a generic error message. 407 | """ 408 | slot :inner_block, required: true 409 | 410 | def error(assigns) do 411 | ~H""" 412 |

413 | <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> 414 | <%= render_slot(@inner_block) %> 415 |

416 | """ 417 | end 418 | 419 | @doc """ 420 | Renders a header with title. 421 | """ 422 | attr :class, :string, default: nil 423 | 424 | slot :inner_block, required: true 425 | slot :subtitle 426 | slot :actions 427 | 428 | def header(assigns) do 429 | ~H""" 430 |
431 |
432 |

433 | <%= render_slot(@inner_block) %> 434 |

435 |

436 | <%= render_slot(@subtitle) %> 437 |

438 |
439 |
<%= render_slot(@actions) %>
440 |
441 | """ 442 | end 443 | 444 | @doc ~S""" 445 | Renders a table with generic styling. 446 | 447 | ## Examples 448 | 449 | <.table id="users" rows={@users}> 450 | <:col :let={user} label="id"><%= user.id %> 451 | <:col :let={user} label="username"><%= user.username %> 452 | 453 | """ 454 | attr :id, :string, required: true 455 | attr :rows, :list, required: true 456 | attr :row_id, :any, default: nil, doc: "the function for generating the row id" 457 | attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" 458 | 459 | attr :row_item, :any, 460 | default: &Function.identity/1, 461 | doc: "the function for mapping each row before calling the :col and :action slots" 462 | 463 | slot :col, required: true do 464 | attr :label, :string 465 | end 466 | 467 | slot :action, doc: "the slot for showing user actions in the last table column" 468 | 469 | def table(assigns) do 470 | assigns = 471 | with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do 472 | assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) 473 | end 474 | 475 | ~H""" 476 |
477 | 478 | 479 | 480 | 481 | 484 | 485 | 486 | 491 | 492 | 504 | 515 | 516 | 517 |
<%= col[:label] %> 482 | <%= gettext("Actions") %> 483 |
497 |
498 | 499 | 500 | <%= render_slot(col, @row_item.(row)) %> 501 | 502 |
503 |
505 |
506 | 507 | 511 | <%= render_slot(action, @row_item.(row)) %> 512 | 513 |
514 |
518 |
519 | """ 520 | end 521 | 522 | @doc """ 523 | Renders a data list. 524 | 525 | ## Examples 526 | 527 | <.list> 528 | <:item title="Title"><%= @post.title %> 529 | <:item title="Views"><%= @post.views %> 530 | 531 | """ 532 | slot :item, required: true do 533 | attr :title, :string, required: true 534 | end 535 | 536 | def list(assigns) do 537 | ~H""" 538 |
539 |
540 |
541 |
<%= item.title %>
542 |
<%= render_slot(item) %>
543 |
544 |
545 |
546 | """ 547 | end 548 | 549 | @doc """ 550 | Renders a back navigation link. 551 | 552 | ## Examples 553 | 554 | <.back navigate={~p"/posts"}>Back to posts 555 | """ 556 | attr :navigate, :any, required: true 557 | slot :inner_block, required: true 558 | 559 | def back(assigns) do 560 | ~H""" 561 |
562 | <.link 563 | navigate={@navigate} 564 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" 565 | > 566 | <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> 567 | <%= render_slot(@inner_block) %> 568 | 569 |
570 | """ 571 | end 572 | 573 | @doc """ 574 | Renders a [Heroicon](https://heroicons.com). 575 | 576 | Heroicons come in three styles – outline, solid, and mini. 577 | By default, the outline style is used, but solid and mini may 578 | be applied by using the `-solid` and `-mini` suffix. 579 | 580 | You can customize the size and colors of the icons by setting 581 | width, height, and background color classes. 582 | 583 | Icons are extracted from the `deps/heroicons` directory and bundled within 584 | your compiled app.css by the plugin in your `assets/tailwind.config.js`. 585 | 586 | ## Examples 587 | 588 | <.icon name="hero-x-mark-solid" /> 589 | <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> 590 | """ 591 | attr :name, :string, required: true 592 | attr :class, :string, default: nil 593 | 594 | def icon(%{name: "hero-" <> _} = assigns) do 595 | ~H""" 596 | 597 | """ 598 | end 599 | 600 | ## JS Commands 601 | 602 | def show(js \\ %JS{}, selector) do 603 | JS.show(js, 604 | to: selector, 605 | time: 300, 606 | transition: 607 | {"transition-all transform ease-out duration-300", 608 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", 609 | "opacity-100 translate-y-0 sm:scale-100"} 610 | ) 611 | end 612 | 613 | def hide(js \\ %JS{}, selector) do 614 | JS.hide(js, 615 | to: selector, 616 | time: 200, 617 | transition: 618 | {"transition-all transform ease-in duration-200", 619 | "opacity-100 translate-y-0 sm:scale-100", 620 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} 621 | ) 622 | end 623 | 624 | def show_modal(js \\ %JS{}, id) when is_binary(id) do 625 | js 626 | |> JS.show(to: "##{id}") 627 | |> JS.show( 628 | to: "##{id}-bg", 629 | time: 300, 630 | transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} 631 | ) 632 | |> show("##{id}-container") 633 | |> JS.add_class("overflow-hidden", to: "body") 634 | |> JS.focus_first(to: "##{id}-content") 635 | end 636 | 637 | def hide_modal(js \\ %JS{}, id) do 638 | js 639 | |> JS.hide( 640 | to: "##{id}-bg", 641 | transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} 642 | ) 643 | |> hide("##{id}-container") 644 | |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) 645 | |> JS.remove_class("overflow-hidden", to: "body") 646 | |> JS.pop_focus() 647 | end 648 | 649 | @doc """ 650 | Translates an error message using gettext. 651 | """ 652 | def translate_error({msg, opts}) do 653 | # When using gettext, we typically pass the strings we want 654 | # to translate as a static argument: 655 | # 656 | # # Translate the number of files with plural rules 657 | # dngettext("errors", "1 file", "%{count} files", count) 658 | # 659 | # However the error messages in our forms and APIs are generated 660 | # dynamically, so we need to translate them by calling Gettext 661 | # with our gettext backend as first argument. Translations are 662 | # available in the errors.po file (as we use the "errors" domain). 663 | if count = opts[:count] do 664 | Gettext.dngettext(DemoElixirPhoenixWeb.Gettext, "errors", msg, msg, count, opts) 665 | else 666 | Gettext.dgettext(DemoElixirPhoenixWeb.Gettext, "errors", msg, opts) 667 | end 668 | end 669 | 670 | @doc """ 671 | Translates the errors for a field from a keyword list of errors. 672 | """ 673 | def translate_errors(errors, field) when is_list(errors) do 674 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) 675 | end 676 | end 677 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use DemoElixirPhoenixWeb, :controller` and 9 | `use DemoElixirPhoenixWeb, :live_view`. 10 | """ 11 | use DemoElixirPhoenixWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v<%= Application.spec(:phoenix, :vsn) %> 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | <%= @inner_content %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "DemoElixirPhoenix" %> 9 | 10 | 11 | 13 | 14 | 15 | <%= @inner_content %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use DemoElixirPhoenixWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/demo_elixir_phoenix_web/controllers/error_html/404.html.heex 14 | # * lib/demo_elixir_phoenix_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.PageController do 2 | use DemoElixirPhoenixWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use DemoElixirPhoenixWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/page_html/home.html.heex: -------------------------------------------------------------------------------- 1 | <.flash_group flash={@flash} /> 2 | 41 |
42 |
43 | 49 |

50 | Phoenix Framework 51 | 52 | v<%= Application.spec(:phoenix, :vsn) %> 53 | 54 |

55 |

56 | Peace of mind from prototype to production. 57 |

58 |

59 | 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. 60 |

61 | 221 |
222 |
223 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.UserController do 2 | use DemoElixirPhoenixWeb, :controller 3 | 4 | alias DemoElixirPhoenix.Account 5 | alias DemoElixirPhoenix.Account.User 6 | 7 | def index(conn, _params) do 8 | users = Account.list_users() 9 | render(conn, :index, users: users) 10 | end 11 | 12 | def new(conn, _params) do 13 | changeset = Account.change_user(%User{}) 14 | render(conn, :new, changeset: changeset) 15 | end 16 | 17 | def create(conn, %{"user" => user_params}) do 18 | case Account.create_user(user_params) do 19 | {:ok, user} -> 20 | conn 21 | |> put_flash(:info, "User created successfully.") 22 | |> redirect(to: ~p"/users/#{user}") 23 | 24 | {:error, %Ecto.Changeset{} = changeset} -> 25 | render(conn, :new, changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | user = Account.get_user!(id) 31 | render(conn, :show, user: user) 32 | end 33 | 34 | def edit(conn, %{"id" => id}) do 35 | user = Account.get_user!(id) 36 | changeset = Account.change_user(user) 37 | render(conn, :edit, user: user, changeset: changeset) 38 | end 39 | 40 | def update(conn, %{"id" => id, "user" => user_params}) do 41 | user = Account.get_user!(id) 42 | 43 | case Account.update_user(user, user_params) do 44 | {:ok, user} -> 45 | conn 46 | |> put_flash(:info, "User updated successfully.") 47 | |> redirect(to: ~p"/users/#{user}") 48 | 49 | {:error, %Ecto.Changeset{} = changeset} -> 50 | render(conn, :edit, user: user, changeset: changeset) 51 | end 52 | end 53 | 54 | def delete(conn, %{"id" => id}) do 55 | user = Account.get_user!(id) 56 | {:ok, _user} = Account.delete_user(user) 57 | 58 | conn 59 | |> put_flash(:info, "User deleted successfully.") 60 | |> redirect(to: ~p"/users") 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.UserHTML do 2 | use DemoElixirPhoenixWeb, :html 3 | 4 | embed_templates "user_html/*" 5 | 6 | @doc """ 7 | Renders a user form. 8 | """ 9 | attr :changeset, Ecto.Changeset, required: true 10 | attr :action, :string, required: true 11 | 12 | def user_form(assigns) 13 | end 14 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html/edit.html.heex: -------------------------------------------------------------------------------- 1 | <.header> 2 | Edit User <%= @user.id %> 3 | <:subtitle>Use this form to manage user records in your database. 4 | 5 | 6 | <.user_form changeset={@changeset} action={~p"/users/#{@user}"} /> 7 | 8 | <.back navigate={~p"/users"}>Back to users 9 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html/index.html.heex: -------------------------------------------------------------------------------- 1 | <.header> 2 | Listing Users 3 | <:actions> 4 | <.link href={~p"/users/new"}> 5 | <.button>New User 6 | 7 | 8 | 9 | 10 | <.table id="users" rows={@users} row_click={&JS.navigate(~p"/users/#{&1}")}> 11 | <:col :let={user} label="Name"><%= user.name %> 12 | <:col :let={user} label="Email"><%= user.email %> 13 | <:action :let={user}> 14 |
15 | <.link navigate={~p"/users/#{user}"}>Show 16 |
17 | <.link navigate={~p"/users/#{user}/edit"}>Edit 18 | 19 | <:action :let={user}> 20 | <.link href={~p"/users/#{user}"} method="delete" data-confirm="Are you sure?"> 21 | Delete 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html/new.html.heex: -------------------------------------------------------------------------------- 1 | <.header> 2 | New User 3 | <:subtitle>Use this form to manage user records in your database. 4 | 5 | 6 | <.user_form changeset={@changeset} action={~p"/users"} /> 7 | 8 | <.back navigate={~p"/users"}>Back to users 9 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html/show.html.heex: -------------------------------------------------------------------------------- 1 | <.header> 2 | User <%= @user.id %> 3 | <:subtitle>This is a user record from your database. 4 | <:actions> 5 | <.link href={~p"/users/#{@user}/edit"}> 6 | <.button>Edit user 7 | 8 | 9 | 10 | 11 | <.list> 12 | <:item title="Name"><%= @user.name %> 13 | <:item title="Email"><%= @user.email %> 14 | 15 | 16 | <.back navigate={~p"/users"}>Back to users 17 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/controllers/user_html/user_form.html.heex: -------------------------------------------------------------------------------- 1 | <.simple_form :let={f} for={@changeset} action={@action}> 2 | <.error :if={@changeset.action}> 3 | Oops, something went wrong! Please check the errors below. 4 | 5 | <.input field={f[:name]} type="text" label="Name" /> 6 | <.input field={f[:email]} type="text" label="Email" /> 7 | <:actions> 8 | <.button>Save User 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :demo_elixir_phoenix 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_demo_elixir_phoenix_key", 10 | signing_salt: "bPK7rXXl", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :demo_elixir_phoenix, 25 | gzip: false, 26 | only: DemoElixirPhoenixWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :demo_elixir_phoenix 35 | end 36 | 37 | plug Phoenix.LiveDashboard.RequestLogger, 38 | param_key: "request_logger", 39 | cookie_key: "request_logger" 40 | 41 | plug Plug.RequestId 42 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 43 | 44 | plug Plug.Parsers, 45 | parsers: [:urlencoded, :multipart, :json], 46 | pass: ["*/*"], 47 | json_decoder: Phoenix.json_library() 48 | 49 | plug Plug.MethodOverride 50 | plug Plug.Head 51 | plug Plug.Session, @session_options 52 | plug DemoElixirPhoenixWeb.Router 53 | end 54 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import DemoElixirPhoenixWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :demo_elixir_phoenix 24 | end 25 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.Router do 2 | use DemoElixirPhoenixWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {DemoElixirPhoenixWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", DemoElixirPhoenixWeb do 18 | pipe_through :browser 19 | 20 | resources "/users", UserController 21 | 22 | get "/", PageController, :home 23 | end 24 | 25 | # Other scopes may use custom stacks. 26 | # scope "/api", DemoElixirPhoenixWeb do 27 | # pipe_through :api 28 | # end 29 | 30 | # Enable LiveDashboard and Swoosh mailbox preview in development 31 | if Application.compile_env(:demo_elixir_phoenix, :dev_routes) do 32 | # If you want to use the LiveDashboard in production, you should put 33 | # it behind authentication and allow only admins to access it. 34 | # If your application does not have an admins-only section yet, 35 | # you can use Plug.BasicAuth to set up some basic authentication 36 | # as long as you are also using SSL (which you should anyway). 37 | import Phoenix.LiveDashboard.Router 38 | 39 | scope "/dev" do 40 | pipe_through :browser 41 | 42 | live_dashboard "/dashboard", metrics: DemoElixirPhoenixWeb.Telemetry 43 | forward "/mailbox", Plug.Swoosh.MailboxPreview 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/demo_elixir_phoenix_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | summary("phoenix.channel_joined.duration", 47 | unit: {:native, :millisecond} 48 | ), 49 | summary("phoenix.channel_handled_in.duration", 50 | tags: [:event], 51 | unit: {:native, :millisecond} 52 | ), 53 | 54 | # Database Metrics 55 | summary("demo_elixir_phoenix.repo.query.total_time", 56 | unit: {:native, :millisecond}, 57 | description: "The sum of the other measurements" 58 | ), 59 | summary("demo_elixir_phoenix.repo.query.decode_time", 60 | unit: {:native, :millisecond}, 61 | description: "The time spent decoding the data received from the database" 62 | ), 63 | summary("demo_elixir_phoenix.repo.query.query_time", 64 | unit: {:native, :millisecond}, 65 | description: "The time spent executing the query" 66 | ), 67 | summary("demo_elixir_phoenix.repo.query.queue_time", 68 | unit: {:native, :millisecond}, 69 | description: "The time spent waiting for a database connection" 70 | ), 71 | summary("demo_elixir_phoenix.repo.query.idle_time", 72 | unit: {:native, :millisecond}, 73 | description: 74 | "The time the connection spent waiting before being checked out for the query" 75 | ), 76 | 77 | # VM Metrics 78 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 79 | summary("vm.total_run_queue_lengths.total"), 80 | summary("vm.total_run_queue_lengths.cpu"), 81 | summary("vm.total_run_queue_lengths.io") 82 | ] 83 | end 84 | 85 | defp periodic_measurements do 86 | [ 87 | # A module, function and arguments to be invoked periodically. 88 | # This function must call :telemetry.execute/3 and a metric must be added above. 89 | # {DemoElixirPhoenixWeb, :count_users, []} 90 | ] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :demo_elixir_phoenix, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {DemoElixirPhoenix.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.14"}, 36 | {:phoenix_ecto, "~> 4.5"}, 37 | {:ecto_sql, "~> 3.10"}, 38 | {:postgrex, ">= 0.0.0"}, 39 | {:phoenix_html, "~> 4.1"}, 40 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 41 | # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, 42 | {:phoenix_live_view, "~> 1.0.0-rc.1", override: true}, 43 | {:floki, ">= 0.30.0", only: :test}, 44 | {:phoenix_live_dashboard, "~> 0.8.3"}, 45 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 46 | {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, 47 | {:heroicons, 48 | github: "tailwindlabs/heroicons", 49 | tag: "v2.1.1", 50 | sparse: "optimized", 51 | app: false, 52 | compile: false, 53 | depth: 1}, 54 | {:swoosh, "~> 1.5"}, 55 | {:finch, "~> 0.13"}, 56 | {:telemetry_metrics, "~> 1.0"}, 57 | {:telemetry_poller, "~> 1.0"}, 58 | {:gettext, "~> 0.20"}, 59 | {:jason, "~> 1.2"}, 60 | {:dns_cluster, "~> 0.1.1"}, 61 | {:bandit, "~> 1.5"} 62 | ] 63 | end 64 | 65 | # Aliases are shortcuts or tasks specific to the current project. 66 | # For example, to install project dependencies and perform other setup tasks, run: 67 | # 68 | # $ mix setup 69 | # 70 | # See the documentation for `Mix` for more info on aliases. 71 | defp aliases do 72 | [ 73 | setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], 74 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 75 | "ecto.reset": ["ecto.drop", "ecto.setup"], 76 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 77 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], 78 | "assets.build": ["tailwind demo_elixir_phoenix", "esbuild demo_elixir_phoenix"], 79 | "assets.deploy": [ 80 | "tailwind demo_elixir_phoenix --minify", 81 | "esbuild demo_elixir_phoenix --minify", 82 | "phx.digest" 83 | ] 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.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", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, 3 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 4 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 5 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 6 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, 7 | "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [: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", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [: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", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, 9 | "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, 10 | "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, 11 | "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, 12 | "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"}, 13 | "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, 14 | "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, 15 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, 16 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 17 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 18 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 19 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [: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", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 20 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 21 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 22 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [: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", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, 23 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [: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", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, 24 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 25 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [: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", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, 26 | "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"}, 27 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [: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", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"}, 28 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 29 | "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"}, 30 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 31 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 32 | "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [: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", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, 33 | "swoosh": {:hex, :swoosh, "1.17.1", "01295a82bddd2c6cac1e65856e29444d7c23c4501e0ebc69cea8a82018227e25", [: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", "3b20d25e580cb79af631335a1bdcfbffd835c08ebcdc16e98577223a241a18a1"}, 34 | "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, 35 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 36 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, 37 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, 38 | "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, 39 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 40 | "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [: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", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, 41 | } 42 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should have %{count} item(s)" 54 | msgid_plural "should have %{count} item(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should be %{count} character(s)" 59 | msgid_plural "should be %{count} character(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be %{count} byte(s)" 64 | msgid_plural "should be %{count} byte(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at least %{count} character(s)" 74 | msgid_plural "should be at least %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should be at least %{count} byte(s)" 79 | msgid_plural "should be at least %{count} byte(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | msgid "should have at most %{count} item(s)" 84 | msgid_plural "should have at most %{count} item(s)" 85 | msgstr[0] "" 86 | msgstr[1] "" 87 | 88 | msgid "should be at most %{count} character(s)" 89 | msgid_plural "should be at most %{count} character(s)" 90 | msgstr[0] "" 91 | msgstr[1] "" 92 | 93 | msgid "should be at most %{count} byte(s)" 94 | msgid_plural "should be at most %{count} byte(s)" 95 | msgstr[0] "" 96 | msgstr[1] "" 97 | 98 | ## From Ecto.Changeset.validate_number/3 99 | msgid "must be less than %{number}" 100 | msgstr "" 101 | 102 | msgid "must be greater than %{number}" 103 | msgstr "" 104 | 105 | msgid "must be less than or equal to %{number}" 106 | msgstr "" 107 | 108 | msgid "must be greater than or equal to %{number}" 109 | msgstr "" 110 | 111 | msgid "must be equal to %{number}" 112 | msgstr "" 113 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | ## From Ecto.Changeset.cast/4 11 | msgid "can't be blank" 12 | msgstr "" 13 | 14 | ## From Ecto.Changeset.unique_constraint/3 15 | msgid "has already been taken" 16 | msgstr "" 17 | 18 | ## From Ecto.Changeset.put_change/3 19 | msgid "is invalid" 20 | msgstr "" 21 | 22 | ## From Ecto.Changeset.validate_acceptance/3 23 | msgid "must be accepted" 24 | msgstr "" 25 | 26 | ## From Ecto.Changeset.validate_format/3 27 | msgid "has invalid format" 28 | msgstr "" 29 | 30 | ## From Ecto.Changeset.validate_subset/3 31 | msgid "has an invalid entry" 32 | msgstr "" 33 | 34 | ## From Ecto.Changeset.validate_exclusion/3 35 | msgid "is reserved" 36 | msgstr "" 37 | 38 | ## From Ecto.Changeset.validate_confirmation/3 39 | msgid "does not match confirmation" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.no_assoc_constraint/3 43 | msgid "is still associated with this entry" 44 | msgstr "" 45 | 46 | msgid "are still associated with this entry" 47 | msgstr "" 48 | 49 | ## From Ecto.Changeset.validate_length/3 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be %{count} character(s)" 56 | msgid_plural "should be %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should be %{count} byte(s)" 61 | msgid_plural "should be %{count} byte(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should have at least %{count} item(s)" 66 | msgid_plural "should have at least %{count} item(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should be at least %{count} character(s)" 71 | msgid_plural "should be at least %{count} character(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | msgid "should be at least %{count} byte(s)" 76 | msgid_plural "should be at least %{count} byte(s)" 77 | msgstr[0] "" 78 | msgstr[1] "" 79 | 80 | msgid "should have at most %{count} item(s)" 81 | msgid_plural "should have at most %{count} item(s)" 82 | msgstr[0] "" 83 | msgstr[1] "" 84 | 85 | msgid "should be at most %{count} character(s)" 86 | msgid_plural "should be at most %{count} character(s)" 87 | msgstr[0] "" 88 | msgstr[1] "" 89 | 90 | msgid "should be at most %{count} byte(s)" 91 | msgid_plural "should be at most %{count} byte(s)" 92 | msgstr[0] "" 93 | msgstr[1] "" 94 | 95 | ## From Ecto.Changeset.validate_number/3 96 | msgid "must be less than %{number}" 97 | msgstr "" 98 | 99 | msgid "must be greater than %{number}" 100 | msgstr "" 101 | 102 | msgid "must be less than or equal to %{number}" 103 | msgstr "" 104 | 105 | msgid "must be greater than or equal to %{number}" 106 | msgstr "" 107 | 108 | msgid "must be equal to %{number}" 109 | msgstr "" 110 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240625211751_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.Repo.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | timestamps(type: :utc_datetime) 7 | add :name, :string 8 | add :email, :string 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # DemoElixirPhoenix.Repo.insert!(%DemoElixirPhoenix.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelparkerhenderson/demo-elixir-phoenix/1cc89ade9e2c3d9370faf62d89e3859653c06eb7/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/demo_elixir_phoenix/account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.AccountTest do 2 | use DemoElixirPhoenix.DataCase 3 | 4 | alias DemoElixirPhoenix.Account 5 | 6 | describe "users" do 7 | alias DemoElixirPhoenix.Account.User 8 | 9 | import DemoElixirPhoenix.AccountFixtures 10 | 11 | @invalid_attrs %{name: nil, email: nil} 12 | 13 | test "list_users/0 returns all users" do 14 | user = user_fixture() 15 | assert Account.list_users() == [user] 16 | end 17 | 18 | test "get_user!/1 returns the user with given id" do 19 | user = user_fixture() 20 | assert Account.get_user!(user.id) == user 21 | end 22 | 23 | test "create_user/1 with valid data creates a user" do 24 | valid_attrs = %{name: "some name", email: "some email"} 25 | 26 | assert {:ok, %User{} = user} = Account.create_user(valid_attrs) 27 | assert user.name == "some name" 28 | assert user.email == "some email" 29 | end 30 | 31 | test "create_user/1 with invalid data returns error changeset" do 32 | assert {:error, %Ecto.Changeset{}} = Account.create_user(@invalid_attrs) 33 | end 34 | 35 | test "update_user/2 with valid data updates the user" do 36 | user = user_fixture() 37 | update_attrs = %{name: "some updated name", email: "some updated email"} 38 | 39 | assert {:ok, %User{} = user} = Account.update_user(user, update_attrs) 40 | assert user.name == "some updated name" 41 | assert user.email == "some updated email" 42 | end 43 | 44 | test "update_user/2 with invalid data returns error changeset" do 45 | user = user_fixture() 46 | assert {:error, %Ecto.Changeset{}} = Account.update_user(user, @invalid_attrs) 47 | assert user == Account.get_user!(user.id) 48 | end 49 | 50 | test "delete_user/1 deletes the user" do 51 | user = user_fixture() 52 | assert {:ok, %User{}} = Account.delete_user(user) 53 | assert_raise Ecto.NoResultsError, fn -> Account.get_user!(user.id) end 54 | end 55 | 56 | test "change_user/1 returns a user changeset" do 57 | user = user_fixture() 58 | assert %Ecto.Changeset{} = Account.change_user(user) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/demo_elixir_phoenix_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.ErrorHTMLTest do 2 | use DemoElixirPhoenixWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(DemoElixirPhoenixWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(DemoElixirPhoenixWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/demo_elixir_phoenix_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.ErrorJSONTest do 2 | use DemoElixirPhoenixWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert DemoElixirPhoenixWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert DemoElixirPhoenixWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/demo_elixir_phoenix_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.PageControllerTest do 2 | use DemoElixirPhoenixWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/demo_elixir_phoenix_web/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.UserControllerTest do 2 | use DemoElixirPhoenixWeb.ConnCase 3 | 4 | import DemoElixirPhoenix.AccountFixtures 5 | 6 | @create_attrs %{name: "some name", email: "some email"} 7 | @update_attrs %{name: "some updated name", email: "some updated email"} 8 | @invalid_attrs %{name: nil, email: nil} 9 | 10 | describe "index" do 11 | test "lists all users", %{conn: conn} do 12 | conn = get(conn, ~p"/users") 13 | assert html_response(conn, 200) =~ "Listing Users" 14 | end 15 | end 16 | 17 | describe "new user" do 18 | test "renders form", %{conn: conn} do 19 | conn = get(conn, ~p"/users/new") 20 | assert html_response(conn, 200) =~ "New User" 21 | end 22 | end 23 | 24 | describe "create user" do 25 | test "redirects to show when data is valid", %{conn: conn} do 26 | conn = post(conn, ~p"/users", user: @create_attrs) 27 | 28 | assert %{id: id} = redirected_params(conn) 29 | assert redirected_to(conn) == ~p"/users/#{id}" 30 | 31 | conn = get(conn, ~p"/users/#{id}") 32 | assert html_response(conn, 200) =~ "User #{id}" 33 | end 34 | 35 | test "renders errors when data is invalid", %{conn: conn} do 36 | conn = post(conn, ~p"/users", user: @invalid_attrs) 37 | assert html_response(conn, 200) =~ "New User" 38 | end 39 | end 40 | 41 | describe "edit user" do 42 | setup [:create_user] 43 | 44 | test "renders form for editing chosen user", %{conn: conn, user: user} do 45 | conn = get(conn, ~p"/users/#{user}/edit") 46 | assert html_response(conn, 200) =~ "Edit User" 47 | end 48 | end 49 | 50 | describe "update user" do 51 | setup [:create_user] 52 | 53 | test "redirects when data is valid", %{conn: conn, user: user} do 54 | conn = put(conn, ~p"/users/#{user}", user: @update_attrs) 55 | assert redirected_to(conn) == ~p"/users/#{user}" 56 | 57 | conn = get(conn, ~p"/users/#{user}") 58 | assert html_response(conn, 200) =~ "some updated name" 59 | end 60 | 61 | test "renders errors when data is invalid", %{conn: conn, user: user} do 62 | conn = put(conn, ~p"/users/#{user}", user: @invalid_attrs) 63 | assert html_response(conn, 200) =~ "Edit User" 64 | end 65 | end 66 | 67 | describe "delete user" do 68 | setup [:create_user] 69 | 70 | test "deletes chosen user", %{conn: conn, user: user} do 71 | conn = delete(conn, ~p"/users/#{user}") 72 | assert redirected_to(conn) == ~p"/users" 73 | 74 | assert_error_sent 404, fn -> 75 | get(conn, ~p"/users/#{user}") 76 | end 77 | end 78 | end 79 | 80 | defp create_user(_) do 81 | user = user_fixture() 82 | %{user: user} 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenixWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use DemoElixirPhoenixWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint DemoElixirPhoenixWeb.Endpoint 24 | 25 | use DemoElixirPhoenixWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import DemoElixirPhoenixWeb.ConnCase 31 | end 32 | end 33 | 34 | setup tags do 35 | DemoElixirPhoenix.DataCase.setup_sandbox(tags) 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use DemoElixirPhoenix.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias DemoElixirPhoenix.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import DemoElixirPhoenix.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | DemoElixirPhoenix.DataCase.setup_sandbox(tags) 32 | :ok 33 | end 34 | 35 | @doc """ 36 | Sets up the sandbox based on the test tags. 37 | """ 38 | def setup_sandbox(tags) do 39 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(DemoElixirPhoenix.Repo, shared: not tags[:async]) 40 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 41 | end 42 | 43 | @doc """ 44 | A helper that transforms changeset errors into a map of messages. 45 | 46 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 47 | assert "password is too short" in errors_on(changeset).password 48 | assert %{password: ["password is too short"]} = errors_on(changeset) 49 | 50 | """ 51 | def errors_on(changeset) do 52 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 53 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 54 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 55 | end) 56 | end) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/support/fixtures/account_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoElixirPhoenix.AccountFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `DemoElixirPhoenix.Account` context. 5 | """ 6 | 7 | @doc """ 8 | Generate a user. 9 | """ 10 | def user_fixture(attrs \\ %{}) do 11 | {:ok, user} = 12 | attrs 13 | |> Enum.into(%{ 14 | email: "some email", 15 | name: "some name" 16 | }) 17 | |> DemoElixirPhoenix.Account.create_user() 18 | 19 | user 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(DemoElixirPhoenix.Repo, :manual) 3 | --------------------------------------------------------------------------------