├── .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 |
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 |
53 |
54 |
62 |
63 |
64 | <.focus_wrap
65 | id={"#{@id}-container"}
66 | phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
67 | phx-key="escape"
68 | phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
69 | class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
70 | >
71 |
72 |
80 |
81 |
82 | <%= render_slot(@inner_block) %>
83 |
84 |
85 |
86 |
87 |
88 |
89 | """
90 | end
91 |
92 | @doc """
93 | Renders flash notices.
94 |
95 | ## Examples
96 |
97 | <.flash kind={:info} flash={@flash} />
98 | <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
99 | """
100 | attr :id, :string, doc: "the optional id of flash container"
101 | attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
102 | attr :title, :string, default: nil
103 | attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
104 | attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
105 |
106 | slot :inner_block, doc: "the optional inner block that renders the flash message"
107 |
108 | def flash(assigns) do
109 | assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
110 |
111 | ~H"""
112 |
hide("##{@id}")}
116 | role="alert"
117 | class={[
118 | "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
119 | @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
120 | @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
121 | ]}
122 | {@rest}
123 | >
124 |
125 | <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
126 | <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
127 | <%= @title %>
128 |
129 |
<%= msg %>
130 |
133 |
134 | """
135 | end
136 |
137 | @doc """
138 | Shows the flash group with standard titles and content.
139 |
140 | ## Examples
141 |
142 | <.flash_group flash={@flash} />
143 | """
144 | attr :flash, :map, required: true, doc: "the map of flash messages"
145 | attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
146 |
147 | def flash_group(assigns) do
148 | ~H"""
149 |
150 | <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
151 | <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
152 | <.flash
153 | id="client-error"
154 | kind={:error}
155 | title={gettext("We can't find the internet")}
156 | phx-disconnected={show(".phx-client-error #client-error")}
157 | phx-connected={hide("#client-error")}
158 | hidden
159 | >
160 | <%= gettext("Attempting to reconnect") %>
161 | <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
162 |
163 |
164 | <.flash
165 | id="server-error"
166 | kind={:error}
167 | title={gettext("Something went wrong!")}
168 | phx-disconnected={show(".phx-server-error #server-error")}
169 | phx-connected={hide("#server-error")}
170 | hidden
171 | >
172 | <%= gettext("Hang in there while we get back on track") %>
173 | <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
174 |
175 |
176 | """
177 | end
178 |
179 | @doc """
180 | Renders a simple form.
181 |
182 | ## Examples
183 |
184 | <.simple_form for={@form} phx-change="validate" phx-submit="save">
185 | <.input field={@form[:email]} label="Email"/>
186 | <.input field={@form[:username]} label="Username" />
187 | <:actions>
188 | <.button>Save
189 |
190 |
191 | """
192 | attr :for, :any, required: true, doc: "the data structure for the form"
193 | attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
194 |
195 | attr :rest, :global,
196 | include: ~w(autocomplete name rel action enctype method novalidate target multipart),
197 | doc: "the arbitrary HTML attributes to apply to the form tag"
198 |
199 | slot :inner_block, required: true
200 | slot :actions, doc: "the slot for form actions, such as a submit button"
201 |
202 | def simple_form(assigns) do
203 | ~H"""
204 | <.form :let={f} for={@for} as={@as} {@rest}>
205 |
206 | <%= render_slot(@inner_block, f) %>
207 |
208 | <%= render_slot(action, f) %>
209 |
210 |
211 |
212 | """
213 | end
214 |
215 | @doc """
216 | Renders a button.
217 |
218 | ## Examples
219 |
220 | <.button>Send!
221 | <.button phx-click="go" class="ml-2">Send!
222 | """
223 | attr :type, :string, default: nil
224 | attr :class, :string, default: nil
225 | attr :rest, :global, include: ~w(disabled form name value)
226 |
227 | slot :inner_block, required: true
228 |
229 | def button(assigns) do
230 | ~H"""
231 |
242 | """
243 | end
244 |
245 | @doc """
246 | Renders an input with label and error messages.
247 |
248 | A `Phoenix.HTML.FormField` may be passed as argument,
249 | which is used to retrieve the input name, id, and values.
250 | Otherwise all attributes may be passed explicitly.
251 |
252 | ## Types
253 |
254 | This function accepts all HTML input types, considering that:
255 |
256 | * You may also set `type="select"` to render a `