├── .formatter.exs ├── .gitignore ├── README.md ├── assets ├── .prettierrc ├── build.js ├── css │ ├── app.css │ └── phoenix.css ├── js │ ├── app.js │ ├── components │ │ ├── e-2-e │ │ │ ├── ChatWindow.svelte │ │ │ ├── ContactChat.svelte │ │ │ └── EmptyContactChat.svelte │ │ ├── no-event │ │ │ ├── ClientSide.svelte │ │ │ ├── ContactCard.svelte │ │ │ └── EmptyContactCard.svelte │ │ ├── patch-event │ │ │ ├── ContactCard.svelte │ │ │ ├── ContactList.svelte │ │ │ └── EmptyContactCard.svelte │ │ ├── push-event │ │ │ ├── Contact.svelte │ │ │ └── ContactForm.svelte │ │ └── simple-svelte-components │ │ │ ├── HelloSvelte.svelte │ │ │ └── Nested.svelte │ └── hooks.js ├── package.json ├── tailwind.config.js └── vendor │ └── topbar.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── lib ├── swiphly.ex ├── swiphly │ ├── application.ex │ ├── mailer.ex │ ├── repo.ex │ ├── schema.ex │ ├── visitors.ex │ └── visitors │ │ ├── chat.ex │ │ └── contact.ex ├── swiphly_web.ex └── swiphly_web │ ├── components │ └── svelte_component.ex │ ├── controllers │ └── page_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── live │ ├── end_to_end.ex │ ├── no_event.ex │ ├── patch_event.ex │ ├── push_event.ex │ └── simple_svelte_component.ex │ ├── router.ex │ ├── telemetry.ex │ ├── templates │ ├── layout │ │ ├── app.html.heex │ │ ├── live.html.heex │ │ └── root.html.heex │ └── page │ │ └── index.html.heex │ └── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20220830185553_create_contacts.exs │ │ └── 20220831021612_create_chats.exs │ └── seeds.exs └── static │ ├── favicon.ico │ ├── images │ ├── phoenix-logo.png │ ├── phoenix-logo.png:Zone.Identifier │ ├── phoenix.png │ ├── svelte-logo.png │ └── svelte-logo.png:Zone.Identifier │ └── robots.txt └── test ├── support ├── conn_case.ex ├── data_case.ex └── fixtures │ └── visitors_fixtures.ex ├── swiphly └── visitors_test.exs ├── swiphly_web ├── controllers │ └── page_controller_test.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /.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 | # Ignore package tarball (built via "mix hex.build"). 23 | swiphly-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | # Database files 36 | *.db 37 | *.db-* 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elixirconf2022 2 | Code examples for ElixirConf 2022 talk on using Svelte with Phoenix LiveView. 3 | 4 | A more descriptive write-up of the talk can be found [here on dev.to](https://dev.to/debussyman/e2e-reactivity-using-svelte-with-phoenix-liveview-38mf). 5 | 6 | [![ElixirConf 2022 - using Svelte with Phoenix LiveView](https://i.ytimg.com/vi/asm2TTm035o/maxresdefault.jpg)](https://www.youtube.com/watch?v=asm2TTm035o) 7 | 8 | This project is built with the following components: 9 | - Phoenix v1.6.11 10 | - Svelte v3.49.0 11 | - npm modules tailwindcss, postcss, autoprefixier, esbuild-style-plugin, daisyui, esbuild-svelte 12 | 13 | # This project 14 | Someone once said there are three hard things in programming - 15 | 1. Naming 16 | 2. Off-by-one errors 17 | 18 | Fortunately, we're more clever than that. 19 | Let's call this Phoenix app **S**velte **wi**th **ph**oenix, serious**ly**. 20 | 21 | ## Initial setup 22 | 23 | We initialized this code repository with the following commands. If you clone this repo, you can skip these steps, but when starting from scratch, create a new Phoenix app in an empty directory (`.` after `phx.new` indicates current directory). 24 | ``` 25 | mix phx.new . --app swiphly --database sqlite3 26 | ``` 27 | > note: if you are starting from scratch and getting an error with sqlite3, you might need to update your phx\_new app: 28 | `mix archive.install hex phx_new` 29 | 30 | ### Configure esbuild 31 | 32 | **This is a key step when setting up from scrach** 33 | 34 | We will be using an esbuild plugin to build Svelte and Tailwindcss components. This requires several changes to the Phoenix app generated by `phx.new`. 35 | Modify the Phoenix app to support esbuild plugins, bypassing the default configuration of esbuild (via the Elixir wrapper). 36 | Follow instructions here - https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins 37 | 38 | ### Node modules 39 | This application needs following node dependences to be installed. They can be installed as dev dependencies since esbuild will be bundling all of the JavaScript and CSS into static assets served by Phoenix. 40 | 41 | It is also necessary to install the Phoenix npm packages, since we are bypassing the Elixir esbuild wrapper, as noted above. 42 | ``` 43 | npm install esbuild svelte tailwindcss postcss autoprefixer esbuild-svelte esbuild-style-plugin daisyui @faker-js/faker --save-dev 44 | npm install ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view --save 45 | ``` 46 | 47 | ### Ecto tables 48 | The following database tables are used by this demo 49 | ``` 50 | mix phx.gen.context Visitors Contact contacts name:string title:string company:string event:string 51 | mix phx.gen.context Visitors Chat chats contact_id:integer message:string 52 | ``` 53 | 54 | ## Setup 55 | On first clone, you'll need to setup Elixir and run migrations to create the database: 56 | ``` 57 | mix setup 58 | mix ecto.migrate 59 | ``` 60 | 61 | ## Running 62 | To run the Phoenix server with an interactive shell, use the following command: 63 | ``` 64 | iex -S mix phx.server 65 | ``` 66 | 67 | ## Key moving parts 68 | Let's highlight key files that play a main role in integrating Svelte into Phoenix LiveView - 69 | ``` 70 | ./ 71 | /assets 72 | build.js # added plugins to esbuild for svelte and postcss (for tailwindcss) 73 | /js 74 | app.js # include custom hook when instantiating livesocket 75 | hooks.js # define custom hook for linking LiveComponent lifecycle with Svelte component 76 | /components # directory containing all Svelte component files 77 | /lib 78 | /swiphly_web 79 | router.ex # maps webpage routes to LiveView pages 80 | /components 81 | svelte_component.ex # Simple reusable LiveView component for rendering HTML div with data attributes 82 | /live 83 | *.ex # LiveView pages corresponding to webpage routes 84 | ``` 85 | 86 | ### Resources 87 | https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins 88 | https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html 89 | https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4 90 | https://github.com/EMH333/esbuild-svelte 91 | https://svelte.dev/tutorial/ 92 | -------------------------------------------------------------------------------- /assets/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 100 7 | } -------------------------------------------------------------------------------- /assets/build.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild') 2 | const sveltePlugin = require('esbuild-svelte') 3 | const postCssPlugin = require('esbuild-style-plugin') 4 | 5 | const args = process.argv.slice(2) 6 | const watch = args.includes('--watch') 7 | const deploy = args.includes('--deploy') 8 | 9 | const loader = { 10 | // Add loaders for images/fonts/etc, e.g. { '.svg': 'file' } 11 | } 12 | 13 | const plugins = [ 14 | // Add and configure plugins here 15 | sveltePlugin(), 16 | postCssPlugin({ 17 | postcss: { 18 | plugins: [require('tailwindcss'), require('autoprefixer')], 19 | }, 20 | }) 21 | ] 22 | 23 | let opts = { 24 | entryPoints: ['js/app.js'], 25 | mainFields: ["svelte", "browser", "module", "main"], 26 | bundle: true, 27 | minify: false, 28 | target: 'es2017', 29 | outdir: '../priv/static/assets', 30 | logLevel: 'info', 31 | loader, 32 | plugins 33 | } 34 | 35 | if (watch) { 36 | opts = { 37 | ...opts, 38 | watch, 39 | sourcemap: 'inline' 40 | } 41 | } 42 | 43 | if (deploy) { 44 | opts = { 45 | ...opts, 46 | minify: true 47 | } 48 | } 49 | 50 | const promise = esbuild.build(opts) 51 | 52 | if (watch) { 53 | promise.then(_result => { 54 | process.stdin.on('close', () => { 55 | process.exit(0) 56 | }) 57 | 58 | process.stdin.resume() 59 | }) 60 | } -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application CSS */ 2 | /* @import "./phoenix.css"; */ 3 | @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; 6 | 7 | /* Alerts and form errors used by phx.new */ 8 | .alert { 9 | padding: 15px; 10 | margin-bottom: 20px; 11 | border: 1px solid transparent; 12 | border-radius: 4px; 13 | } 14 | .alert-info { 15 | color: #31708f; 16 | background-color: #d9edf7; 17 | border-color: #bce8f1; 18 | } 19 | .alert-warning { 20 | color: #8a6d3b; 21 | background-color: #fcf8e3; 22 | border-color: #faebcc; 23 | } 24 | .alert-danger { 25 | color: #a94442; 26 | background-color: #f2dede; 27 | border-color: #ebccd1; 28 | } 29 | .alert p { 30 | margin-bottom: 0; 31 | } 32 | .alert:empty { 33 | display: none; 34 | } 35 | .invalid-feedback { 36 | color: #a94442; 37 | display: block; 38 | margin: -1rem 0 2rem; 39 | } 40 | 41 | /* LiveView specific classes for your customization */ 42 | .phx-no-feedback.invalid-feedback, 43 | .phx-no-feedback .invalid-feedback { 44 | display: none; 45 | } 46 | 47 | .phx-click-loading { 48 | opacity: 0.5; 49 | transition: opacity 1s ease-out; 50 | } 51 | 52 | .phx-loading{ 53 | cursor: wait; 54 | } 55 | 56 | .phx-modal { 57 | opacity: 1!important; 58 | position: fixed; 59 | z-index: 1; 60 | left: 0; 61 | top: 0; 62 | width: 100%; 63 | height: 100%; 64 | overflow: auto; 65 | background-color: rgba(0,0,0,0.4); 66 | } 67 | 68 | .phx-modal-content { 69 | background-color: #fefefe; 70 | margin: 15vh auto; 71 | padding: 20px; 72 | border: 1px solid #888; 73 | width: 80%; 74 | } 75 | 76 | .phx-modal-close { 77 | color: #aaa; 78 | float: right; 79 | font-size: 28px; 80 | font-weight: bold; 81 | } 82 | 83 | .phx-modal-close:hover, 84 | .phx-modal-close:focus { 85 | color: black; 86 | text-decoration: none; 87 | cursor: pointer; 88 | } 89 | 90 | .fade-in-scale { 91 | animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys; 92 | } 93 | 94 | .fade-out-scale { 95 | animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys; 96 | } 97 | 98 | .fade-in { 99 | animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys; 100 | } 101 | .fade-out { 102 | animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys; 103 | } 104 | 105 | @keyframes fade-in-scale-keys{ 106 | 0% { scale: 0.95; opacity: 0; } 107 | 100% { scale: 1.0; opacity: 1; } 108 | } 109 | 110 | @keyframes fade-out-scale-keys{ 111 | 0% { scale: 1.0; opacity: 1; } 112 | 100% { scale: 0.95; opacity: 0; } 113 | } 114 | 115 | @keyframes fade-in-keys{ 116 | 0% { opacity: 0; } 117 | 100% { opacity: 1; } 118 | } 119 | 120 | @keyframes fade-out-keys{ 121 | 0% { opacity: 1; } 122 | 100% { opacity: 0; } 123 | } 124 | -------------------------------------------------------------------------------- /assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.4.1 https://milligram.github.io 6 | * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | pre{padding: 1em;} 19 | 20 | .container{ 21 | margin: 0 auto; 22 | max-width: 80.0rem; 23 | padding: 0 2.0rem; 24 | position: relative; 25 | width: 100% 26 | } 27 | select { 28 | width: auto; 29 | } 30 | 31 | /* Phoenix promo and logo */ 32 | .phx-hero { 33 | text-align: center; 34 | border-bottom: 1px solid #e3e3e3; 35 | background: #eee; 36 | border-radius: 6px; 37 | padding: 3em 3em 1em; 38 | margin-bottom: 3rem; 39 | font-weight: 200; 40 | font-size: 120%; 41 | } 42 | .phx-hero input { 43 | background: #ffffff; 44 | } 45 | .phx-logo { 46 | min-width: 300px; 47 | margin: 1rem; 48 | display: block; 49 | } 50 | .phx-logo img { 51 | width: auto; 52 | display: block; 53 | } 54 | 55 | /* Headers */ 56 | header { 57 | width: 100%; 58 | background: #fdfdfd; 59 | border-bottom: 1px solid #eaeaea; 60 | margin-bottom: 2rem; 61 | } 62 | header section { 63 | align-items: center; 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: space-between; 67 | } 68 | header section :first-child { 69 | order: 2; 70 | } 71 | header section :last-child { 72 | order: 1; 73 | } 74 | header nav ul, 75 | header nav li { 76 | margin: 0; 77 | padding: 0; 78 | display: block; 79 | text-align: right; 80 | white-space: nowrap; 81 | } 82 | header nav ul { 83 | margin: 1rem; 84 | margin-top: 0; 85 | } 86 | header nav a { 87 | display: block; 88 | } 89 | 90 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 91 | header section { 92 | flex-direction: row; 93 | } 94 | header nav ul { 95 | margin: 1rem; 96 | } 97 | .phx-logo { 98 | flex-basis: 527px; 99 | margin: 2rem 1rem; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We import the CSS which is extracted to its own file by esbuild. 2 | // Remove this line if you add a your own CSS build pipeline (e.g postcss). 3 | import "../css/app.css" 4 | 5 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 6 | // to get started and then uncomment the line below. 7 | // import "./user_socket.js" 8 | 9 | // You can include dependencies in two ways. 10 | // 11 | // The simplest option is to put them in assets/vendor and 12 | // import them using relative paths: 13 | // 14 | // import "../vendor/some-package.js" 15 | // 16 | // Alternatively, you can `npm install some-package --prefix assets` and import 17 | // them using a path starting with the package name: 18 | // 19 | // import "some-package" 20 | // 21 | 22 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 23 | import "phoenix_html" 24 | // Establish Phoenix Socket and LiveView configuration. 25 | import {Socket} from "phoenix" 26 | import {LiveSocket} from "phoenix_live_view" 27 | import topbar from "../vendor/topbar" 28 | import Hooks from "./hooks" 29 | 30 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 31 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) 32 | 33 | // Show progress bar on live navigation and form submits 34 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 35 | window.addEventListener("phx:page-loading-start", info => topbar.show()) 36 | window.addEventListener("phx:page-loading-stop", info => topbar.hide()) 37 | 38 | // connect if there are any LiveViews on the page 39 | liveSocket.connect() 40 | 41 | // expose liveSocket on window for web console debug logs and latency simulation: 42 | // >> liveSocket.enableDebug() 43 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 44 | // >> liveSocket.disableLatencySim() 45 | window.liveSocket = liveSocket 46 | 47 | -------------------------------------------------------------------------------- /assets/js/components/e-2-e/ChatWindow.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 |
29 | 34 | 35 |
36 |
37 | 38 |
39 | {#if last_chat} 40 | 41 | {:else} 42 | 43 | {/if} 44 |
45 | -------------------------------------------------------------------------------- /assets/js/components/e-2-e/ContactChat.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
{contact?.name || 'Anon'}
7 | 8 | 13 | 14 | 15 |

{chat.message}

16 |
17 | 18 | 24 | -------------------------------------------------------------------------------- /assets/js/components/e-2-e/EmptyContactChat.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | Empty! 6 | -------------------------------------------------------------------------------- /assets/js/components/no-event/ClientSide.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 | {#if contacts.length > 0} 18 |
19 | 30 | 41 |
42 | {/if} 43 | {#each sortedContacts as contact (contact.id)} 44 | 47 |
48 | {:else} 49 | No contacts! 50 | {/each} 51 |
52 |
53 | {#if selected} 54 | 55 | {:else if contacts.length > 0} 56 | 57 | {/if} 58 |
59 |
60 | -------------------------------------------------------------------------------- /assets/js/components/no-event/ContactCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |

{contact.name}

17 | Joined {localDate} 18 |

{contact.name} is the {contact.title} at {contact.company}

19 |
20 | 21 |
22 |
23 |
-------------------------------------------------------------------------------- /assets/js/components/no-event/EmptyContactCard.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🦗 4 |
5 |
-------------------------------------------------------------------------------- /assets/js/components/patch-event/ContactCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |

{contact.name}

17 | Joined {localDate} 18 |

{contact.name} is the {contact.title} at {contact.company}

19 |
20 | 21 |
22 |
23 |
-------------------------------------------------------------------------------- /assets/js/components/patch-event/ContactList.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | {#if contacts.length > 0} 17 | 43 | {/if} 44 | {#each contacts as contact (contact.id)} 45 | 51 |
52 | {:else} 53 | No contacts! 54 | {/each} 55 |
56 |
57 | {#if selected} 58 | 59 | {:else if contacts.length > 0} 60 | 61 | {/if} 62 |
63 |
64 | -------------------------------------------------------------------------------- /assets/js/components/patch-event/EmptyContactCard.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🦗 4 |
5 |
-------------------------------------------------------------------------------- /assets/js/components/push-event/Contact.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | {contact.id} 7 | {contact.name} 8 | {contact.title} 9 | {contact.company} 10 | {contact.event} 11 | 12 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/js/components/push-event/ContactForm.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
54 | 58 | 62 | 66 | 70 | 71 |
{formResult}
72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | {#each contacts as contact (contact.id)} 89 | 90 | {:else} 91 | 92 | 94 | 98 | {/each} 99 | 100 |
80 | NameTitleCompanyEvent 85 |
93 | No contacts! 95 | 96 | 97 |
101 |
102 | -------------------------------------------------------------------------------- /assets/js/components/simple-svelte-components/HelloSvelte.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

This is {content}, in a silly font.

8 | 9 |
10 | 11 | 18 | -------------------------------------------------------------------------------- /assets/js/components/simple-svelte-components/Nested.svelte: -------------------------------------------------------------------------------- 1 |

This is other content, but as a nested Svelte component.

2 | -------------------------------------------------------------------------------- /assets/js/hooks.js: -------------------------------------------------------------------------------- 1 | import HelloSvelte from "./components/simple-svelte-components/HelloSvelte.svelte" 2 | import ContactForm from "./components/push-event/ContactForm.svelte" 3 | import ContactList from "./components/patch-event/ContactList.svelte" 4 | import NoEvent from "./components/no-event/ClientSide.svelte" 5 | import E2E from "./components/e-2-e/ChatWindow.svelte" 6 | 7 | const components = { 8 | HelloSvelte, 9 | ContactForm, 10 | ContactList, 11 | NoEvent, 12 | E2E 13 | } 14 | 15 | function parsedProps(el) { 16 | const props = el.getAttribute('data-props') 17 | return props ? JSON.parse(props) : {} 18 | } 19 | 20 | const SvelteComponent = { 21 | mounted() { 22 | const componentName = this.el.getAttribute('data-name') 23 | if (!componentName) { 24 | throw new Error('Component name must be provided') 25 | } 26 | 27 | const requiredApp = components[componentName] 28 | if (!requiredApp) { 29 | throw new Error(`Unable to find ${componentName} component. Did you forget to import it into hooks.js?`) 30 | } 31 | 32 | const request = (event, data, callback) => { 33 | this.pushEvent(event, data, callback) 34 | } 35 | 36 | const goto = (href) => { 37 | liveSocket.pushHistoryPatch(href, "push", this.el) 38 | } 39 | 40 | this._instance = new requiredApp({ 41 | target: this.el, 42 | props: {...parsedProps(this.el), request, goto }, 43 | }) 44 | }, 45 | 46 | updated() { 47 | const request = (event, data, callback) => { 48 | this.pushEvent(event, data, callback) 49 | } 50 | 51 | const goto = (href) => { 52 | liveSocket.pushHistoryPatch(href, "push", this.el) 53 | } 54 | 55 | this._instance.$$set({...parsedProps(this.el), request, goto }) 56 | }, 57 | 58 | destroyed() { 59 | this._instance?.$destroy() 60 | } 61 | } 62 | 63 | export default { 64 | SvelteComponent, 65 | } -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@faker-js/faker": "^7.5.0", 4 | "autoprefixer": "^10.4.8", 5 | "daisyui": "^2.24.0", 6 | "esbuild": "^0.15.6", 7 | "esbuild-style-plugin": "^1.6.0", 8 | "esbuild-svelte": "^0.7.1", 9 | "postcss": "^8.4.16", 10 | "svelte": "^3.49.0", 11 | "tailwindcss": "^3.1.8" 12 | }, 13 | "dependencies": { 14 | "phoenix": "file:../deps/phoenix", 15 | "phoenix_html": "file:../deps/phoenix_html", 16 | "phoenix_live_view": "file:../deps/phoenix_live_view" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const themeColors = { 2 | // shades generated from https://tailwind.ink/ 3 | steel: { 4 | DEFAULT: '#272d3a', 5 | '50': '#f8f9f9', 6 | '100': '#ebf1f5', 7 | '200': '#d3dfea', 8 | '300': '#a9bdd0', 9 | '400': '#7896ad', 10 | '500': '#5c738d', 11 | '600': '#4a596f', 12 | '700': '#414a57', 13 | '800': '#272d3a', 14 | '900': '#181b25', 15 | }, 16 | seagreen: { 17 | DEFAULT: '#81B29A', 18 | '50': '#f4f7f5', 19 | '100': '#e2efee', 20 | '200': '#bee3d9', 21 | '300': '#81b29a', 22 | '400': '#49a386', 23 | '500': '#34865f', 24 | '600': '#2c6d47', 25 | '700': '#265338', 26 | '800': '#1b392a', 27 | '900': '#12231e', 28 | }, 29 | chestnut: { 30 | DEFAULT: '#AD5D4E', 31 | '50': '#fcfbf9', 32 | '100': '#faf0e5', 33 | '200': '#f5d5c8', 34 | '300': '#e8ab9a', 35 | '400': '#de7c6c', 36 | '500': '#ad5d4e', 37 | '600': '#af3e31', 38 | '700': '#872e25', 39 | '800': '#5e201a', 40 | '900': '#3a1410', 41 | }, 42 | orchid: { 43 | DEFAULT: '#C490D1', 44 | '50': '#fafafb', 45 | '100': '#f4eff8', 46 | '200': '#e8d3f1', 47 | '300': '#d0ace0', 48 | '400': '#c490d1', 49 | '500': '#a95cb7', 50 | '600': '#8e409b', 51 | '700': '#6b3077', 52 | '800': '#4a2250', 53 | '900': '#2b152e', 54 | }, 55 | } 56 | 57 | module.exports = { 58 | content: [ 59 | './js/**/*.svelte', 60 | "../lib/**/*.heex", 61 | ], 62 | darkMode: 'media', 63 | theme: { 64 | extend: { 65 | colors: { 66 | ...themeColors, 67 | } 68 | }, 69 | }, 70 | variants: { 71 | extend: { 72 | opacity: ['disabled'], 73 | }, 74 | }, 75 | plugins: [require("daisyui")], 76 | mode: 'jit', 77 | } 78 | -------------------------------------------------------------------------------- /assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 1.0.0, 2021-01-06 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 | progressTimerId, 39 | fadeTimerId, 40 | currentProgress, 41 | showing, 42 | addEvent = function (elem, type, handler) { 43 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 44 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 45 | else elem["on" + type] = handler; 46 | }, 47 | options = { 48 | autoRun: true, 49 | barThickness: 3, 50 | barColors: { 51 | 0: "rgba(26, 188, 156, .9)", 52 | ".25": "rgba(52, 152, 219, .9)", 53 | ".50": "rgba(241, 196, 15, .9)", 54 | ".75": "rgba(230, 126, 34, .9)", 55 | "1.0": "rgba(211, 84, 0, .9)", 56 | }, 57 | shadowBlur: 10, 58 | shadowColor: "rgba(0, 0, 0, .6)", 59 | className: null, 60 | }, 61 | repaint = function () { 62 | canvas.width = window.innerWidth; 63 | canvas.height = options.barThickness * 5; // need space for shadow 64 | 65 | var ctx = canvas.getContext("2d"); 66 | ctx.shadowBlur = options.shadowBlur; 67 | ctx.shadowColor = options.shadowColor; 68 | 69 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 70 | for (var stop in options.barColors) 71 | lineGradient.addColorStop(stop, options.barColors[stop]); 72 | ctx.lineWidth = options.barThickness; 73 | ctx.beginPath(); 74 | ctx.moveTo(0, options.barThickness / 2); 75 | ctx.lineTo( 76 | Math.ceil(currentProgress * canvas.width), 77 | options.barThickness / 2 78 | ); 79 | ctx.strokeStyle = lineGradient; 80 | ctx.stroke(); 81 | }, 82 | createCanvas = function () { 83 | canvas = document.createElement("canvas"); 84 | var style = canvas.style; 85 | style.position = "fixed"; 86 | style.top = style.left = style.right = style.margin = style.padding = 0; 87 | style.zIndex = 100001; 88 | style.display = "none"; 89 | if (options.className) canvas.classList.add(options.className); 90 | document.body.appendChild(canvas); 91 | addEvent(window, "resize", repaint); 92 | }, 93 | topbar = { 94 | config: function (opts) { 95 | for (var key in opts) 96 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 97 | }, 98 | show: function () { 99 | if (showing) return; 100 | showing = true; 101 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 102 | if (!canvas) createCanvas(); 103 | canvas.style.opacity = 1; 104 | canvas.style.display = "block"; 105 | topbar.progress(0); 106 | if (options.autoRun) { 107 | (function loop() { 108 | progressTimerId = window.requestAnimationFrame(loop); 109 | topbar.progress( 110 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 111 | ); 112 | })(); 113 | } 114 | }, 115 | progress: function (to) { 116 | if (typeof to === "undefined") return currentProgress; 117 | if (typeof to === "string") { 118 | to = 119 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 120 | ? currentProgress 121 | : 0) + parseFloat(to); 122 | } 123 | currentProgress = to > 1 ? 1 : to; 124 | repaint(); 125 | return currentProgress; 126 | }, 127 | hide: function () { 128 | if (!showing) return; 129 | showing = false; 130 | if (progressTimerId != null) { 131 | window.cancelAnimationFrame(progressTimerId); 132 | progressTimerId = null; 133 | } 134 | (function loop() { 135 | if (topbar.progress("+.1") >= 1) { 136 | canvas.style.opacity -= 0.05; 137 | if (canvas.style.opacity <= 0.05) { 138 | canvas.style.display = "none"; 139 | fadeTimerId = null; 140 | return; 141 | } 142 | } 143 | fadeTimerId = window.requestAnimationFrame(loop); 144 | })(); 145 | }, 146 | }; 147 | 148 | if (typeof module === "object" && typeof module.exports === "object") { 149 | module.exports = topbar; 150 | } else if (typeof define === "function" && define.amd) { 151 | define(function () { 152 | return topbar; 153 | }); 154 | } else { 155 | this.topbar = topbar; 156 | } 157 | }.call(this, window, document)); 158 | -------------------------------------------------------------------------------- /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 :swiphly, 11 | ecto_repos: [Swiphly.Repo] 12 | 13 | # Configures the endpoint 14 | config :swiphly, SwiphlyWeb.Endpoint, 15 | url: [host: "localhost"], 16 | render_errors: [view: SwiphlyWeb.ErrorView, accepts: ~w(html json), layout: false], 17 | pubsub_server: Swiphly.PubSub, 18 | live_view: [signing_salt: "8F/E4EjE"] 19 | 20 | # Configures the mailer 21 | # 22 | # By default it uses the "Local" adapter which stores the emails 23 | # locally. You can see the emails in your browser, at "/dev/mailbox". 24 | # 25 | # For production it's recommended to configure a different adapter 26 | # at the `config/runtime.exs`. 27 | config :swiphly, Swiphly.Mailer, adapter: Swoosh.Adapters.Local 28 | 29 | # Swoosh API client is needed for adapters other than SMTP. 30 | config :swoosh, :api_client, false 31 | 32 | # Configures Elixir's Logger 33 | config :logger, :console, 34 | format: "$time $metadata[$level] $message\n", 35 | metadata: [:request_id] 36 | 37 | # Use Jason for JSON parsing in Phoenix 38 | config :phoenix, :json_library, Jason 39 | 40 | # Import environment specific config. This must remain at the bottom 41 | # of this file so it overrides the configuration defined above. 42 | import_config "#{config_env()}.exs" 43 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | config :swiphly, Swiphly.Repo, 5 | database: Path.expand("../swiphly_dev.db", Path.dirname(__ENV__.file)), 6 | pool_size: 5, 7 | stacktrace: true, 8 | show_sensitive_data_on_connection_error: true 9 | 10 | # For development, we disable any cache and enable 11 | # debugging and code reloading. 12 | # 13 | # The watchers configuration can be used to run external 14 | # watchers to your application. For example, we use it 15 | # with esbuild to bundle .js and .css sources. 16 | config :swiphly, SwiphlyWeb.Endpoint, 17 | # Binding to loopback ipv4 address prevents access from other machines. 18 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 19 | http: [ip: {127, 0, 0, 1}, port: 4000], 20 | check_origin: false, 21 | code_reloader: true, 22 | debug_errors: true, 23 | secret_key_base: "QUWviuyGL/Sq8sPltD2d9GyGmxA30VMcOXWKSCWKTTERRZgwNcQlv5/bozIBbfo+", 24 | watchers: [ 25 | # Run asset build script whenever you change files 26 | # This replaces the default esbuilt that ships with Phoenix, to support additional plugins 27 | # More information in the Phoenix docs - https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins 28 | node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)] 29 | ] 30 | 31 | # ## SSL Support 32 | # 33 | # In order to use HTTPS in development, a self-signed 34 | # certificate can be generated by running the following 35 | # Mix task: 36 | # 37 | # mix phx.gen.cert 38 | # 39 | # Note that this task requires Erlang/OTP 20 or later. 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 :swiphly, SwiphlyWeb.Endpoint, 57 | live_reload: [ 58 | patterns: [ 59 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 60 | ~r"priv/gettext/.*(po)$", 61 | ~r"lib/swiphly_web/(live|views)/.*(ex)$", 62 | ~r"lib/swiphly_web/templates/.*(eex)$" 63 | ] 64 | ] 65 | 66 | # Do not include metadata nor timestamps in development logs 67 | config :logger, :console, format: "[$level] $message\n" 68 | 69 | # Set a higher stacktrace during development. Avoid configuring such 70 | # in production as building large stacktraces may be expensive. 71 | config :phoenix, :stacktrace_depth, 20 72 | 73 | # Initialize plugs at runtime for faster development compilation 74 | config :phoenix, :plug_init_mode, :runtime 75 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :swiphly, SwiphlyWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" 13 | 14 | # Do not print debug messages in production 15 | config :logger, level: :info 16 | 17 | # ## SSL Support 18 | # 19 | # To get SSL working, you will need to add the `https` key 20 | # to the previous section and set your `:url` port to 443: 21 | # 22 | # config :swiphly, SwiphlyWeb.Endpoint, 23 | # ..., 24 | # url: [host: "example.com", port: 443], 25 | # https: [ 26 | # ..., 27 | # port: 443, 28 | # cipher_suite: :strong, 29 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 30 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 31 | # ] 32 | # 33 | # The `cipher_suite` is set to `:strong` to support only the 34 | # latest and more secure SSL ciphers. This means old browsers 35 | # and clients may not be supported. You can set it to 36 | # `:compatible` for wider support. 37 | # 38 | # `:keyfile` and `:certfile` expect an absolute path to the key 39 | # and cert in disk or a relative path inside priv, for example 40 | # "priv/ssl/server.key". For all supported SSL configuration 41 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 42 | # 43 | # We also recommend setting `force_ssl` in your endpoint, ensuring 44 | # no data is ever sent via http, always redirecting to https: 45 | # 46 | # config :swiphly, SwiphlyWeb.Endpoint, 47 | # force_ssl: [hsts: true] 48 | # 49 | # Check `Plug.SSL` for all available options in `force_ssl`. 50 | -------------------------------------------------------------------------------- /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/swiphly 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 :swiphly, SwiphlyWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | database_path = 25 | System.get_env("DATABASE_PATH") || 26 | raise """ 27 | environment variable DATABASE_PATH is missing. 28 | For example: /etc/swiphly/swiphly.db 29 | """ 30 | 31 | config :swiphly, Swiphly.Repo, 32 | database: database_path, 33 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5") 34 | 35 | # The secret key base is used to sign/encrypt cookies and other secrets. 36 | # A default value is used in config/dev.exs and config/test.exs but you 37 | # want to use a different value for prod and you most likely don't want 38 | # to check this value into version control, so we use an environment 39 | # variable instead. 40 | secret_key_base = 41 | System.get_env("SECRET_KEY_BASE") || 42 | raise """ 43 | environment variable SECRET_KEY_BASE is missing. 44 | You can generate one by calling: mix phx.gen.secret 45 | """ 46 | 47 | host = System.get_env("PHX_HOST") || "example.com" 48 | port = String.to_integer(System.get_env("PORT") || "4000") 49 | 50 | config :swiphly, SwiphlyWeb.Endpoint, 51 | url: [host: host, port: 443, scheme: "https"], 52 | http: [ 53 | # Enable IPv6 and bind on all interfaces. 54 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 55 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 56 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 57 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 58 | port: port 59 | ], 60 | secret_key_base: secret_key_base 61 | 62 | # ## Configuring the mailer 63 | # 64 | # In production you need to configure the mailer to use a different adapter. 65 | # Also, you may need to configure the Swoosh API client of your choice if you 66 | # are not using SMTP. Here is an example of the configuration: 67 | # 68 | # config :swiphly, Swiphly.Mailer, 69 | # adapter: Swoosh.Adapters.Mailgun, 70 | # api_key: System.get_env("MAILGUN_API_KEY"), 71 | # domain: System.get_env("MAILGUN_DOMAIN") 72 | # 73 | # For this example you need include a HTTP client required by Swoosh API client. 74 | # Swoosh supports Hackney and Finch out of the box: 75 | # 76 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 77 | # 78 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 79 | end 80 | -------------------------------------------------------------------------------- /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 :swiphly, Swiphly.Repo, 9 | database: Path.expand("../swiphly_test.db", Path.dirname(__ENV__.file)), 10 | pool_size: 5, 11 | pool: Ecto.Adapters.SQL.Sandbox 12 | 13 | # We don't run a server during test. If one is required, 14 | # you can enable the server option below. 15 | config :swiphly, SwiphlyWeb.Endpoint, 16 | http: [ip: {127, 0, 0, 1}, port: 4002], 17 | secret_key_base: "FIy3nOIG7kIymIlimWfflQjYXa9AVD3V07xX8u/BPdGzEuDSB/lgGQRruaOEcTrf", 18 | server: false 19 | 20 | # In test we don't send emails. 21 | config :swiphly, Swiphly.Mailer, adapter: Swoosh.Adapters.Test 22 | 23 | # Print only warnings and errors during test 24 | config :logger, level: :warn 25 | 26 | # Initialize plugs at runtime for faster test compilation 27 | config :phoenix, :plug_init_mode, :runtime 28 | -------------------------------------------------------------------------------- /lib/swiphly.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly do 2 | @moduledoc """ 3 | Swiphly 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/swiphly/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.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 | # Start the Ecto repository 12 | Swiphly.Repo, 13 | # Start the Telemetry supervisor 14 | SwiphlyWeb.Telemetry, 15 | # Start the PubSub system 16 | {Phoenix.PubSub, name: Swiphly.PubSub}, 17 | # Start the Endpoint (http/https) 18 | SwiphlyWeb.Endpoint 19 | # Start a worker by calling: Swiphly.Worker.start_link(arg) 20 | # {Swiphly.Worker, arg} 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: Swiphly.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 | SwiphlyWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/swiphly/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Mailer do 2 | use Swoosh.Mailer, otp_app: :swiphly 3 | end 4 | -------------------------------------------------------------------------------- /lib/swiphly/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Repo do 2 | use Ecto.Repo, 3 | otp_app: :swiphly, 4 | adapter: Ecto.Adapters.SQLite3 5 | end 6 | -------------------------------------------------------------------------------- /lib/swiphly/schema.ex: -------------------------------------------------------------------------------- 1 | # Define a module to be used as base 2 | defmodule Swiphly.Schema do 3 | # from https://stackoverflow.com/questions/58206597/how-to-set-datetime-in-ecto-schemas-and-timestamp-with-time-zone-timestamp 4 | defmacro __using__(_) do 5 | quote do 6 | use TypedEctoSchema 7 | 8 | # ------------------------------------ 9 | @timestamps_opts [type: :utc_datetime_usec] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/swiphly/visitors.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Visitors do 2 | @moduledoc """ 3 | The Visitors context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias Swiphly.Repo 8 | 9 | alias Swiphly.Visitors.Contact 10 | 11 | @doc """ 12 | Returns the list of contacts. 13 | 14 | ## Examples 15 | 16 | iex> list_contacts() 17 | [%Contact{}, ...] 18 | 19 | """ 20 | def list_contacts do 21 | Contact |> order_by(desc: :inserted_at) |> Repo.all() 22 | end 23 | 24 | @doc """ 25 | Gets a single contact. 26 | 27 | Raises `Ecto.NoResultsError` if the Contact does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_contact!(123) 32 | %Contact{} 33 | 34 | iex> get_contact!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_contact!(id), do: Repo.get!(Contact, id) 39 | 40 | @doc """ 41 | Creates a contact. 42 | 43 | ## Examples 44 | 45 | iex> create_contact(%{field: value}) 46 | {:ok, %Contact{}} 47 | 48 | iex> create_contact(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_contact(attrs \\ %{}) do 53 | result = %Contact{} 54 | |> Contact.changeset(attrs) 55 | |> Repo.insert() 56 | 57 | case result do 58 | {:ok, contact} -> 59 | Phoenix.PubSub.broadcast!(Swiphly.PubSub, "contacts", {:new_contact, contact}) 60 | result 61 | _ -> result 62 | end 63 | end 64 | 65 | @doc """ 66 | Updates a contact. 67 | 68 | ## Examples 69 | 70 | iex> update_contact(contact, %{field: new_value}) 71 | {:ok, %Contact{}} 72 | 73 | iex> update_contact(contact, %{field: bad_value}) 74 | {:error, %Ecto.Changeset{}} 75 | 76 | """ 77 | def update_contact(%Contact{} = contact, attrs) do 78 | contact 79 | |> Contact.changeset(attrs) 80 | |> Repo.update() 81 | end 82 | 83 | @doc """ 84 | Deletes a contact. 85 | 86 | ## Examples 87 | 88 | iex> delete_contact(contact) 89 | {:ok, %Contact{}} 90 | 91 | iex> delete_contact(contact) 92 | {:error, %Ecto.Changeset{}} 93 | 94 | """ 95 | def delete_contact(%Contact{} = contact) do 96 | Repo.delete(contact) 97 | end 98 | 99 | @doc """ 100 | Returns an `%Ecto.Changeset{}` for tracking contact changes. 101 | 102 | ## Examples 103 | 104 | iex> change_contact(contact) 105 | %Ecto.Changeset{data: %Contact{}} 106 | 107 | """ 108 | def change_contact(%Contact{} = contact, attrs \\ %{}) do 109 | Contact.changeset(contact, attrs) 110 | end 111 | 112 | alias Swiphly.Visitors.Chat 113 | 114 | @doc """ 115 | Returns the list of chats. 116 | 117 | ## Examples 118 | 119 | iex> list_chats() 120 | [%Chat{}, ...] 121 | 122 | """ 123 | def list_chats do 124 | Chat |> order_by(desc: :inserted_at) |> Repo.all() 125 | end 126 | 127 | @doc """ 128 | Gets a single chat. 129 | 130 | Raises `Ecto.NoResultsError` if the Chat does not exist. 131 | 132 | ## Examples 133 | 134 | iex> get_chat!(123) 135 | %Chat{} 136 | 137 | iex> get_chat!(456) 138 | ** (Ecto.NoResultsError) 139 | 140 | """ 141 | def get_chat!(id), do: Repo.get!(Chat, id) 142 | 143 | @doc """ 144 | Creates a chat. 145 | 146 | ## Examples 147 | 148 | iex> create_chat(%{field: value}) 149 | {:ok, %Chat{}} 150 | 151 | iex> create_chat(%{field: bad_value}) 152 | {:error, %Ecto.Changeset{}} 153 | 154 | """ 155 | def create_chat(attrs \\ %{}) do 156 | result = %Chat{} 157 | |> Chat.changeset(attrs) 158 | |> Repo.insert() 159 | 160 | case result do 161 | {:ok, chat} -> 162 | Phoenix.PubSub.broadcast!(Swiphly.PubSub, "chats", {:new_chat, chat}) 163 | result 164 | _ -> result 165 | end 166 | end 167 | 168 | @doc """ 169 | Updates a chat. 170 | 171 | ## Examples 172 | 173 | iex> update_chat(chat, %{field: new_value}) 174 | {:ok, %Chat{}} 175 | 176 | iex> update_chat(chat, %{field: bad_value}) 177 | {:error, %Ecto.Changeset{}} 178 | 179 | """ 180 | def update_chat(%Chat{} = chat, attrs) do 181 | chat 182 | |> Chat.changeset(attrs) 183 | |> Repo.update() 184 | end 185 | 186 | @doc """ 187 | Deletes a chat. 188 | 189 | ## Examples 190 | 191 | iex> delete_chat(chat) 192 | {:ok, %Chat{}} 193 | 194 | iex> delete_chat(chat) 195 | {:error, %Ecto.Changeset{}} 196 | 197 | """ 198 | def delete_chat(%Chat{} = chat) do 199 | Repo.delete(chat) 200 | end 201 | 202 | @doc """ 203 | Returns an `%Ecto.Changeset{}` for tracking chat changes. 204 | 205 | ## Examples 206 | 207 | iex> change_chat(chat) 208 | %Ecto.Changeset{data: %Chat{}} 209 | 210 | """ 211 | def change_chat(%Chat{} = chat, attrs \\ %{}) do 212 | Chat.changeset(chat, attrs) 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /lib/swiphly/visitors/chat.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Visitors.Chat do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | # NOTE you would want to enforce references through ecto associations 6 | # We use a simpler approach here for demonstration purposes 7 | @derive {Jason.Encoder, only: [:id, :contact_id, :message, :inserted_at]} 8 | schema "chats" do 9 | field :contact_id, :integer 10 | field :message, :string 11 | 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(chat, attrs) do 17 | chat 18 | |> cast(attrs, [:contact_id, :message]) 19 | |> validate_required([:contact_id, :message]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/swiphly/visitors/contact.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Visitors.Contact do 2 | use Swiphly.Schema 3 | import Ecto.Changeset 4 | 5 | @derive {Jason.Encoder, only: [:id, :name, :company, :title, :event, :inserted_at]} 6 | schema "contacts" do 7 | field :company, :string 8 | field :event, :string 9 | field :name, :string 10 | field :title, :string 11 | 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(contact, attrs) do 17 | contact 18 | |> cast(attrs, [:name, :title, :company, :event]) 19 | |> validate_required([:name, :title, :company, :event]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/swiphly_web.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use SwiphlyWeb, :controller 9 | use SwiphlyWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, 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 any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: SwiphlyWeb 23 | 24 | import Plug.Conn 25 | import SwiphlyWeb.Gettext 26 | alias SwiphlyWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/swiphly_web/templates", 34 | namespace: SwiphlyWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def live_view do 46 | quote do 47 | use Phoenix.LiveView, 48 | layout: {SwiphlyWeb.LayoutView, "live.html"} 49 | 50 | unquote(view_helpers()) 51 | end 52 | end 53 | 54 | def live_component do 55 | quote do 56 | use Phoenix.LiveComponent 57 | 58 | unquote(view_helpers()) 59 | end 60 | end 61 | 62 | def component do 63 | quote do 64 | use Phoenix.Component 65 | 66 | unquote(view_helpers()) 67 | end 68 | end 69 | 70 | def router do 71 | quote do 72 | use Phoenix.Router 73 | 74 | import Plug.Conn 75 | import Phoenix.Controller 76 | import Phoenix.LiveView.Router 77 | end 78 | end 79 | 80 | def channel do 81 | quote do 82 | use Phoenix.Channel 83 | import SwiphlyWeb.Gettext 84 | end 85 | end 86 | 87 | defp view_helpers do 88 | quote do 89 | # Use all HTML functionality (forms, tags, etc) 90 | use Phoenix.HTML 91 | 92 | # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) 93 | import Phoenix.LiveView.Helpers 94 | 95 | # Import basic rendering functionality (render, render_layout, etc) 96 | import Phoenix.View 97 | 98 | import SwiphlyWeb.ErrorHelpers 99 | import SwiphlyWeb.Gettext 100 | alias SwiphlyWeb.Router.Helpers, as: Routes 101 | end 102 | end 103 | 104 | @doc """ 105 | When used, dispatch to the appropriate controller/view/etc. 106 | """ 107 | defmacro __using__(which) when is_atom(which) do 108 | apply(__MODULE__, which, []) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/swiphly_web/components/svelte_component.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.SvelteComponent do 2 | use SwiphlyWeb, :live_component 3 | require Logger 4 | 5 | def render(assigns) do 6 | ~H""" 7 |
8 | """ 9 | end 10 | 11 | defp json(nil), do: "" 12 | defp json(props) do 13 | case Jason.encode(props) do 14 | {:ok, data} -> data 15 | {:error, err} -> 16 | Logger.error("Could not JSON encode props: #{inspect err}") 17 | "" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/swiphly_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PageController do 2 | use SwiphlyWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/swiphly_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :swiphly 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: "_swiphly_key", 10 | signing_salt: "XIyvKJrw" 11 | ] 12 | 13 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 14 | 15 | # Serve at "/" the static files from "priv/static" directory. 16 | # 17 | # You should set gzip to true if you are running phx.digest 18 | # when deploying your static files in production. 19 | plug Plug.Static, 20 | at: "/", 21 | from: :swiphly, 22 | gzip: false, 23 | only: ~w(assets fonts images favicon.ico robots.txt) 24 | 25 | # Code reloading can be explicitly enabled under the 26 | # :code_reloader configuration of your endpoint. 27 | if code_reloading? do 28 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 29 | plug Phoenix.LiveReloader 30 | plug Phoenix.CodeReloader 31 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :swiphly 32 | end 33 | 34 | plug Phoenix.LiveDashboard.RequestLogger, 35 | param_key: "request_logger", 36 | cookie_key: "request_logger" 37 | 38 | plug Plug.RequestId 39 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 40 | 41 | plug Plug.Parsers, 42 | parsers: [:urlencoded, :multipart, :json], 43 | pass: ["*/*"], 44 | json_decoder: Phoenix.json_library() 45 | 46 | plug Plug.MethodOverride 47 | plug Plug.Head 48 | plug Plug.Session, @session_options 49 | plug SwiphlyWeb.Router 50 | end 51 | -------------------------------------------------------------------------------- /lib/swiphly_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.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 SwiphlyWeb.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: :swiphly 24 | end 25 | -------------------------------------------------------------------------------- /lib/swiphly_web/live/end_to_end.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.EndToEnd do 2 | use SwiphlyWeb, :live_view 3 | 4 | alias Swiphly.Visitors 5 | 6 | require Logger 7 | 8 | @impl true 9 | def mount(_params, _session, socket) do 10 | Phoenix.PubSub.subscribe(Swiphly.PubSub, "contacts") 11 | Phoenix.PubSub.subscribe(Swiphly.PubSub, "chats") 12 | 13 | socket = 14 | socket 15 | |> assign(:contacts, Visitors.list_contacts()) 16 | |> assign(:last_chat, List.first(Visitors.list_chats())) 17 | 18 | {:ok, socket} 19 | end 20 | 21 | @impl true 22 | def render(assigns) do 23 | ~H""" 24 | <.live_component module={SwiphlyWeb.SvelteComponent} id="contacts" name="E2E" props={%{contacts: @contacts, last_chat: @last_chat}} /> 25 | """ 26 | end 27 | 28 | @impl true 29 | def handle_info({:new_contact, contact}, socket) do 30 | socket = 31 | socket 32 | |> assign(:contacts, [contact | socket.assigns.contacts]) 33 | 34 | {:noreply, socket} 35 | end 36 | 37 | @impl true 38 | def handle_info({:new_chat, chat}, socket) do 39 | socket = 40 | socket 41 | |> assign(:last_chat, chat) 42 | 43 | {:noreply, socket} 44 | end 45 | 46 | @impl true 47 | def handle_event("create", params, socket) do 48 | case Visitors.create_chat(params) do 49 | {:ok, chat} -> 50 | socket = 51 | socket 52 | |> assign(:last_chat, chat) 53 | 54 | {:reply, %{success: true}, socket} 55 | 56 | {:error, changeset} -> 57 | error = 58 | changeset.errors 59 | |> Enum.map(fn {field, {failure, _}} -> "#{field} #{failure}" end) 60 | |> Enum.join(", ") 61 | 62 | {:reply, %{success: false, reason: error}, socket} 63 | 64 | _ -> 65 | {:reply, %{success: false}, socket} 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/swiphly_web/live/no_event.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.NoEvent do 2 | use SwiphlyWeb, :live_view 3 | 4 | alias Swiphly.Visitors 5 | 6 | require Logger 7 | 8 | @impl true 9 | def mount(_params, _session, socket) do 10 | socket = 11 | socket 12 | |> assign(:contacts, Visitors.list_contacts()) 13 | 14 | {:ok, socket} 15 | end 16 | 17 | @impl true 18 | def render(assigns) do 19 | ~H""" 20 | <.live_component module={SwiphlyWeb.SvelteComponent} id="contacts" name="NoEvent" props={%{contacts: @contacts}} /> 21 | """ 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/swiphly_web/live/patch_event.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PatchEvent do 2 | use SwiphlyWeb, :live_view 3 | 4 | alias Swiphly.Visitors 5 | 6 | require Logger 7 | 8 | @impl true 9 | def mount(_params, _session, socket) do 10 | sort = "name" 11 | 12 | socket = 13 | socket 14 | |> assign(:contacts, sort_contacts(Visitors.list_contacts(), sort)) 15 | |> assign(:selected, nil) 16 | |> assign(:sort, sort) 17 | 18 | {:ok, socket} 19 | end 20 | 21 | @impl true 22 | def render(assigns) do 23 | ~H""" 24 | <.live_component module={SwiphlyWeb.SvelteComponent} id="contacts" name="ContactList" props={%{contacts: @contacts, selected: @selected, sort: @sort}} /> 25 | """ 26 | end 27 | 28 | @impl true 29 | def handle_params(%{"contact" => contact_id, "sort" => sort}, _uri, socket) do 30 | {contact_id, _} = Integer.parse(contact_id) 31 | selected = Enum.find(socket.assigns.contacts, fn %{id: id} -> id == contact_id end) 32 | 33 | socket = 34 | socket 35 | |> assign(:contacts, sort_contacts(socket.assigns.contacts, sort)) 36 | |> assign(:sort, sort) 37 | |> assign(:selected, selected) 38 | 39 | {:noreply, socket} 40 | end 41 | 42 | @impl true 43 | def handle_params(%{"contact" => contact_id}, _uri, socket) do 44 | {contact_id, _} = Integer.parse(contact_id) 45 | selected = Enum.find(socket.assigns.contacts, fn %{id: id} -> id == contact_id end) 46 | 47 | socket = 48 | socket 49 | |> assign(:selected, selected) 50 | 51 | {:noreply, socket} 52 | end 53 | 54 | @impl true 55 | def handle_params(%{"sort" => sort}, _uri, socket) do 56 | socket = 57 | socket 58 | |> assign(:contacts, sort_contacts(socket.assigns.contacts, sort)) 59 | |> assign(:sort, sort) 60 | 61 | {:noreply, socket} 62 | end 63 | 64 | @impl true 65 | def handle_params(_params, _uri, socket) do 66 | socket = 67 | socket 68 | |> assign(:selected, nil) 69 | 70 | {:noreply, socket} 71 | end 72 | 73 | defp sort_contacts(contacts, sort) do 74 | case sort do 75 | "name" -> 76 | Enum.sort_by(contacts, & &1.name) 77 | 78 | "created" -> 79 | Enum.sort_by(contacts, & &1.inserted_at, DateTime) 80 | 81 | _ -> 82 | contacts 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/swiphly_web/live/push_event.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PushEvent do 2 | use SwiphlyWeb, :live_view 3 | 4 | alias Swiphly.Visitors 5 | 6 | @impl true 7 | def mount(_params, _session, socket) do 8 | socket = 9 | socket 10 | |> assign(:contacts, Visitors.list_contacts()) 11 | 12 | {:ok, socket} 13 | end 14 | 15 | @impl true 16 | def render(assigns) do 17 | ~H""" 18 | <.live_component module={SwiphlyWeb.SvelteComponent} id="contacts" name="ContactForm" props={%{contacts: @contacts}} /> 19 | """ 20 | end 21 | 22 | @impl true 23 | def handle_event("create", params, %{assigns: %{contacts: contacts}} = socket) do 24 | case Visitors.create_contact(params) do 25 | {:ok, contact} -> 26 | socket = 27 | socket 28 | |> assign(:contacts, [contact] ++ contacts) 29 | 30 | {:reply, %{success: true}, socket} 31 | 32 | {:error, changeset} -> 33 | error = 34 | changeset.errors 35 | |> Enum.map(fn {field, {failure, _}} -> "#{field} #{failure}" end) 36 | |> Enum.join(", ") 37 | 38 | {:reply, %{success: false, reason: error}, socket} 39 | 40 | _ -> 41 | {:reply, %{success: false}, socket} 42 | end 43 | end 44 | 45 | @impl true 46 | def handle_event("delete", %{"contact_id" => id}, %{assigns: %{contacts: contacts}} = socket) do 47 | with contact <- Visitors.get_contact!(id), 48 | {:ok, _} <- Visitors.delete_contact(contact) do 49 | socket = 50 | socket 51 | |> assign(:contacts, Enum.reject(contacts, fn c -> c.id == id end)) 52 | 53 | {:noreply, socket} 54 | else 55 | _ -> 56 | {:noreply, socket} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/swiphly_web/live/simple_svelte_component.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.SimpleSvelteComponent do 2 | use SwiphlyWeb, :live_view 3 | 4 | def mount(_params, _session, socket) do 5 | {:ok, assign(socket, content: "content from a socket assigns, loaded as a prop")} 6 | end 7 | 8 | def render(assigns) do 9 | ~H""" 10 | <.live_component module={SwiphlyWeb.SvelteComponent} id="hero" name="HelloSvelte" props={%{content: @content}} /> 11 | """ 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/swiphly_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.Router do 2 | use SwiphlyWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {SwiphlyWeb.LayoutView, :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 "/", SwiphlyWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :index 21 | live "/simple-svelte-component", SimpleSvelteComponent 22 | live "/push-event", PushEvent 23 | live "/patch-event", PatchEvent 24 | live "/no-event", NoEvent 25 | live "/e-2-e", EndToEnd 26 | end 27 | 28 | # Other scopes may use custom stacks. 29 | # scope "/api", SwiphlyWeb do 30 | # pipe_through :api 31 | # end 32 | 33 | # Enables LiveDashboard only for development 34 | # 35 | # If you want to use the LiveDashboard in production, you should put 36 | # it behind authentication and allow only admins to access it. 37 | # If your application does not have an admins-only section yet, 38 | # you can use Plug.BasicAuth to set up some basic authentication 39 | # as long as you are also using SSL (which you should anyway). 40 | if Mix.env() in [:dev, :test] do 41 | import Phoenix.LiveDashboard.Router 42 | 43 | scope "/" do 44 | pipe_through :browser 45 | 46 | live_dashboard "/dashboard", metrics: SwiphlyWeb.Telemetry 47 | end 48 | end 49 | 50 | # Enables the Swoosh mailbox preview in development. 51 | # 52 | # Note that preview only shows emails that were sent by the same 53 | # node running the Phoenix server. 54 | if Mix.env() == :dev do 55 | scope "/dev" do 56 | pipe_through :browser 57 | 58 | forward "/mailbox", Plug.Swoosh.MailboxPreview 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/swiphly_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.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.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # Database Metrics 34 | summary("swiphly.repo.query.total_time", 35 | unit: {:native, :millisecond}, 36 | description: "The sum of the other measurements" 37 | ), 38 | summary("swiphly.repo.query.decode_time", 39 | unit: {:native, :millisecond}, 40 | description: "The time spent decoding the data received from the database" 41 | ), 42 | summary("swiphly.repo.query.query_time", 43 | unit: {:native, :millisecond}, 44 | description: "The time spent executing the query" 45 | ), 46 | summary("swiphly.repo.query.queue_time", 47 | unit: {:native, :millisecond}, 48 | description: "The time spent waiting for a database connection" 49 | ), 50 | summary("swiphly.repo.query.idle_time", 51 | unit: {:native, :millisecond}, 52 | description: 53 | "The time the connection spent waiting before being checked out for the query" 54 | ), 55 | 56 | # VM Metrics 57 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 58 | summary("vm.total_run_queue_lengths.total"), 59 | summary("vm.total_run_queue_lengths.cpu"), 60 | summary("vm.total_run_queue_lengths.io") 61 | ] 62 | end 63 | 64 | defp periodic_measurements do 65 | [ 66 | # A module, function and arguments to be invoked periodically. 67 | # This function must call :telemetry.execute/3 and a metric must be added above. 68 | # {SwiphlyWeb, :count_users, []} 69 | ] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/swiphly_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /lib/swiphly_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /lib/swiphly_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= live_title_tag assigns[:page_title] || "Swiphly", suffix: " · Phoenix Framework" %> 9 | 10 | 11 | 12 | 13 |
14 | Phoenix Framework Logo 15 |
16 | 17 | 18 | 19 |
20 | Svelte Framework Logo 21 |

22 | E2E Reactivity using Svelte with Phoenix LiveView 23 |

24 |
25 |
26 |
27 | 36 |
37 | <%= @inner_content %> 38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/swiphly_web/templates/page/index.html.heex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

Peace of mind from prototype to production

4 |
5 | 6 |
7 |
8 |

Resources

9 | 20 |
21 |
22 |

Help

23 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /lib/swiphly_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_name(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message using gettext. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # When using gettext, we typically pass the strings we want 25 | # to translate as a static argument: 26 | # 27 | # # Translate "is invalid" in the "errors" domain 28 | # dgettext("errors", "is invalid") 29 | # 30 | # # Translate the number of files with plural rules 31 | # dngettext("errors", "1 file", "%{count} files", count) 32 | # 33 | # Because the error messages we show in our forms and APIs 34 | # are defined inside Ecto, we need to translate them dynamically. 35 | # This requires us to call the Gettext module passing our gettext 36 | # backend as first argument. 37 | # 38 | # Note we use the "errors" domain, which means translations 39 | # should be written to the errors.po file. The :count option is 40 | # set by Ecto and indicates we should also apply plural rules. 41 | if count = opts[:count] do 42 | Gettext.dngettext(SwiphlyWeb.Gettext, "errors", msg, msg, count, opts) 43 | else 44 | Gettext.dgettext(SwiphlyWeb.Gettext, "errors", msg, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/swiphly_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.ErrorView do 2 | use SwiphlyWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/swiphly_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.LayoutView do 2 | use SwiphlyWeb, :view 3 | 4 | # Phoenix LiveDashboard is available only in development by default, 5 | # so we instruct Elixir to not warn if the dashboard route is missing. 6 | @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} 7 | end 8 | -------------------------------------------------------------------------------- /lib/swiphly_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PageView do 2 | use SwiphlyWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :swiphly, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {Swiphly.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.6.11"}, 37 | {:phoenix_ecto, "~> 4.4"}, 38 | {:ecto_sql, "~> 3.6"}, 39 | {:ecto_sqlite3, ">= 0.0.0"}, 40 | {:typed_ecto_schema, "~> 0.4.1", runtime: false}, 41 | {:phoenix_html, "~> 3.0"}, 42 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 43 | {:phoenix_live_view, "~> 0.17.5"}, 44 | {:floki, ">= 0.30.0", only: :test}, 45 | {:phoenix_live_dashboard, "~> 0.6"}, 46 | {:swoosh, "~> 1.3"}, 47 | {:telemetry_metrics, "~> 0.6"}, 48 | {:telemetry_poller, "~> 1.0"}, 49 | {:gettext, "~> 0.18"}, 50 | {:jason, "~> 1.2"}, 51 | {:plug_cowboy, "~> 2.5"} 52 | ] 53 | end 54 | 55 | # Aliases are shortcuts or tasks specific to the current project. 56 | # For example, to install project dependencies and perform other setup tasks, run: 57 | # 58 | # $ mix setup 59 | # 60 | # See the documentation for `Mix` for more info on aliases. 61 | defp aliases do 62 | [ 63 | setup: ["deps.get", "ecto.setup"], 64 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 65 | "ecto.reset": ["ecto.drop", "ecto.setup"], 66 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 67 | "assets.deploy": ["cmd --cd assets node build.js --deploy", "phx.digest"] 68 | ] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, 3 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 4 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 6 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 7 | "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, 8 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 9 | "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, 10 | "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, 11 | "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.8.0", "00f88a44b37b98a7555ba640acf612efe1ca80eeb64c9538607e294426bb22a0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "3804d804ed2df05376a6d8cb99a489d22b83ffebd078919ad880f15ac36f8338"}, 12 | "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, 13 | "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, 14 | "exqlite": {:hex, :exqlite, "0.11.4", "d659b1e12797787097670d2d9fe183bc40563c78f849c57f762fe043c655c5b6", [:make, :mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "514f1246ffb2d14287a34d74b7c40fbbf58485720c2b3de9156662c6063dfe87"}, 15 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 16 | "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"}, 17 | "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, 18 | "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, 19 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 20 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 21 | "phoenix": {:hex, :phoenix, "1.6.11", "29f3c0fd12fa1fc4d4b05e341578e55bc78d96ea83a022587a7e276884d397e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1664e34f80c25ea4918fbadd957f491225ef601c0e00b4e644b1a772864bfbc2"}, 22 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, 23 | "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, 24 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, 25 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, 26 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.11", "205f6aa5405648c76f2abcd57716f42fc07d8f21dd8ea7b262dd12b324b50c95", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7177791944b7f90ed18f5935a6a5f07f760b36f7b3bdfb9d28c57440a3c43f99"}, 27 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, 28 | "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, 29 | "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, 30 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, 31 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, 32 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 33 | "swoosh": {:hex, :swoosh, "1.7.4", "f967d9b2659e81bab241b96267aae1001d35c2beea2df9c03dcf47b007bf566f", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1553d994b4cf069162965e63de1e1c53d8236e127118d21e56ce2abeaa3f25b4"}, 34 | "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, 35 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, 36 | "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, 37 | "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, 38 | } 39 | -------------------------------------------------------------------------------- /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 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220830185553_create_contacts.exs: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Repo.Migrations.CreateContacts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:contacts) do 6 | add :name, :string 7 | add :title, :string 8 | add :company, :string 9 | add :event, :string 10 | 11 | timestamps() 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220831021612_create_chats.exs: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.Repo.Migrations.CreateChats do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:chats) do 6 | add :contact_id, :integer 7 | add :message, :string 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /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 | # Swiphly.Repo.insert!(%Swiphly.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/jumpwire-ai/elixirconf2022/00d9fde08604830b9f95b224554f419c28fe598a/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/images/phoenix-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumpwire-ai/elixirconf2022/00d9fde08604830b9f95b224554f419c28fe598a/priv/static/images/phoenix-logo.png -------------------------------------------------------------------------------- /priv/static/images/phoenix-logo.png:Zone.Identifier: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumpwire-ai/elixirconf2022/00d9fde08604830b9f95b224554f419c28fe598a/priv/static/images/phoenix-logo.png:Zone.Identifier -------------------------------------------------------------------------------- /priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumpwire-ai/elixirconf2022/00d9fde08604830b9f95b224554f419c28fe598a/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /priv/static/images/svelte-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumpwire-ai/elixirconf2022/00d9fde08604830b9f95b224554f419c28fe598a/priv/static/images/svelte-logo.png -------------------------------------------------------------------------------- /priv/static/images/svelte-logo.png:Zone.Identifier: -------------------------------------------------------------------------------- 1 | [ZoneTransfer] 2 | ZoneId=3 3 | ReferrerUrl=https://raw.githubusercontent.com/ 4 | HostUrl=https://raw.githubusercontent.com/sveltejs/svelte/29052aba7d0b78316d3a52aef1d7ddd54fe6ca84/site/static/images/svelte-android-chrome-512.png 5 | -------------------------------------------------------------------------------- /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/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.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 SwiphlyWeb.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 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import SwiphlyWeb.ConnCase 26 | 27 | alias SwiphlyWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint SwiphlyWeb.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | Swiphly.DataCase.setup_sandbox(tags) 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.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 Swiphly.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 Swiphly.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import Swiphly.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | Swiphly.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!(Swiphly.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/visitors_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.VisitorsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Swiphly.Visitors` context. 5 | """ 6 | 7 | @doc """ 8 | Generate a contact. 9 | """ 10 | def contact_fixture(attrs \\ %{}) do 11 | {:ok, contact} = 12 | attrs 13 | |> Enum.into(%{ 14 | company: "some company", 15 | event: "some event", 16 | name: "some name", 17 | title: "some title" 18 | }) 19 | |> Swiphly.Visitors.create_contact() 20 | 21 | contact 22 | end 23 | 24 | @doc """ 25 | Generate a chat. 26 | """ 27 | def chat_fixture(attrs \\ %{}) do 28 | {:ok, chat} = 29 | attrs 30 | |> Enum.into(%{ 31 | contact_id: 42, 32 | message: "some message" 33 | }) 34 | |> Swiphly.Visitors.create_chat() 35 | 36 | chat 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/swiphly/visitors_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Swiphly.VisitorsTest do 2 | use Swiphly.DataCase 3 | 4 | alias Swiphly.Visitors 5 | 6 | describe "contacts" do 7 | alias Swiphly.Visitors.Contact 8 | 9 | import Swiphly.VisitorsFixtures 10 | 11 | @invalid_attrs %{company: nil, event: nil, name: nil, title: nil} 12 | 13 | test "list_contacts/0 returns all contacts" do 14 | contact = contact_fixture() 15 | assert Visitors.list_contacts() == [contact] 16 | end 17 | 18 | test "get_contact!/1 returns the contact with given id" do 19 | contact = contact_fixture() 20 | assert Visitors.get_contact!(contact.id) == contact 21 | end 22 | 23 | test "create_contact/1 with valid data creates a contact" do 24 | valid_attrs = %{company: "some company", event: "some event", name: "some name", title: "some title"} 25 | 26 | assert {:ok, %Contact{} = contact} = Visitors.create_contact(valid_attrs) 27 | assert contact.company == "some company" 28 | assert contact.event == "some event" 29 | assert contact.name == "some name" 30 | assert contact.title == "some title" 31 | end 32 | 33 | test "create_contact/1 with invalid data returns error changeset" do 34 | assert {:error, %Ecto.Changeset{}} = Visitors.create_contact(@invalid_attrs) 35 | end 36 | 37 | test "update_contact/2 with valid data updates the contact" do 38 | contact = contact_fixture() 39 | update_attrs = %{company: "some updated company", event: "some updated event", name: "some updated name", title: "some updated title"} 40 | 41 | assert {:ok, %Contact{} = contact} = Visitors.update_contact(contact, update_attrs) 42 | assert contact.company == "some updated company" 43 | assert contact.event == "some updated event" 44 | assert contact.name == "some updated name" 45 | assert contact.title == "some updated title" 46 | end 47 | 48 | test "update_contact/2 with invalid data returns error changeset" do 49 | contact = contact_fixture() 50 | assert {:error, %Ecto.Changeset{}} = Visitors.update_contact(contact, @invalid_attrs) 51 | assert contact == Visitors.get_contact!(contact.id) 52 | end 53 | 54 | test "delete_contact/1 deletes the contact" do 55 | contact = contact_fixture() 56 | assert {:ok, %Contact{}} = Visitors.delete_contact(contact) 57 | assert_raise Ecto.NoResultsError, fn -> Visitors.get_contact!(contact.id) end 58 | end 59 | 60 | test "change_contact/1 returns a contact changeset" do 61 | contact = contact_fixture() 62 | assert %Ecto.Changeset{} = Visitors.change_contact(contact) 63 | end 64 | end 65 | 66 | describe "chats" do 67 | alias Swiphly.Visitors.Chat 68 | 69 | import Swiphly.VisitorsFixtures 70 | 71 | @invalid_attrs %{contact_id: nil, message: nil} 72 | 73 | test "list_chats/0 returns all chats" do 74 | chat = chat_fixture() 75 | assert Visitors.list_chats() == [chat] 76 | end 77 | 78 | test "get_chat!/1 returns the chat with given id" do 79 | chat = chat_fixture() 80 | assert Visitors.get_chat!(chat.id) == chat 81 | end 82 | 83 | test "create_chat/1 with valid data creates a chat" do 84 | valid_attrs = %{contact_id: 42, message: "some message"} 85 | 86 | assert {:ok, %Chat{} = chat} = Visitors.create_chat(valid_attrs) 87 | assert chat.contact_id == 42 88 | assert chat.message == "some message" 89 | end 90 | 91 | test "create_chat/1 with invalid data returns error changeset" do 92 | assert {:error, %Ecto.Changeset{}} = Visitors.create_chat(@invalid_attrs) 93 | end 94 | 95 | test "update_chat/2 with valid data updates the chat" do 96 | chat = chat_fixture() 97 | update_attrs = %{contact_id: 43, message: "some updated message"} 98 | 99 | assert {:ok, %Chat{} = chat} = Visitors.update_chat(chat, update_attrs) 100 | assert chat.contact_id == 43 101 | assert chat.message == "some updated message" 102 | end 103 | 104 | test "update_chat/2 with invalid data returns error changeset" do 105 | chat = chat_fixture() 106 | assert {:error, %Ecto.Changeset{}} = Visitors.update_chat(chat, @invalid_attrs) 107 | assert chat == Visitors.get_chat!(chat.id) 108 | end 109 | 110 | test "delete_chat/1 deletes the chat" do 111 | chat = chat_fixture() 112 | assert {:ok, %Chat{}} = Visitors.delete_chat(chat) 113 | assert_raise Ecto.NoResultsError, fn -> Visitors.get_chat!(chat.id) end 114 | end 115 | 116 | test "change_chat/1 returns a chat changeset" do 117 | chat = chat_fixture() 118 | assert %Ecto.Changeset{} = Visitors.change_chat(chat) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /test/swiphly_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PageControllerTest do 2 | use SwiphlyWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/swiphly_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.ErrorViewTest do 2 | use SwiphlyWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(SwiphlyWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(SwiphlyWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/swiphly_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.LayoutViewTest do 2 | use SwiphlyWeb.ConnCase, async: true 3 | 4 | # When testing helpers, you may want to import Phoenix.HTML and 5 | # use functions such as safe_to_string() to convert the helper 6 | # result into an HTML string. 7 | # import Phoenix.HTML 8 | end 9 | -------------------------------------------------------------------------------- /test/swiphly_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SwiphlyWeb.PageViewTest do 2 | use SwiphlyWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Swiphly.Repo, :manual) 3 | --------------------------------------------------------------------------------