Created
April 26, 2026 09:31
-
-
Save sukidhar/63bb5a47e8fe34af2f4c81e95b5413df to your computer and use it in GitHub Desktop.
Single file Phoenix app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode16() | |
| secret_base = :crypto.strong_rand_bytes(32) |> Base.encode16() | |
| host = "localhost" | |
| Application.put_env(:phoenix, :json_library, Jason) | |
| Application.put_env(:sample, Sample.Endpoint, | |
| adapter: Bandit.PhoenixAdapter, | |
| url: [host: host], | |
| http: [ | |
| ip: {0, 0, 0, 0, 0, 0, 0, 0}, | |
| port: String.to_integer(System.get_env("PORT") || "4000") | |
| ], | |
| server: true, | |
| live_view: [signing_salt: signing_salt], | |
| secret_key_base: secret_base, | |
| pubsub_server: Sample.PubSub | |
| ) | |
| Mix.install([ | |
| {:bandit, "~> 1.5"}, | |
| {:jason, "~> 1.0"}, | |
| {:phoenix, "~> 1.8"}, | |
| {:phoenix_live_view, path: "../phoenix_live_view"} | |
| ]) | |
| defmodule Sample.ErrorView do | |
| def render(_, _), do: "error" | |
| end | |
| defmodule Sample.HomeLive do | |
| use Phoenix.LiveView, layout: {__MODULE__, :live} | |
| @upload_dir Path.expand("uploads", __DIR__) | |
| def mount(_params, _session, socket) do | |
| File.mkdir_p!(@upload_dir) | |
| socket = | |
| socket | |
| |> assign(:uploaded_files, []) | |
| |> allow_upload(:files, accept: :any, max_entries: 5, max_file_size: 10_000_000) | |
| {:ok, socket} | |
| end | |
| def live(assigns) do | |
| ~H""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} /> | |
| <style> | |
| * { font-size: 1.1em; } | |
| body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; } | |
| button { margin: 0.25rem; padding: 0.5rem 1rem; cursor: pointer; } | |
| .drop-zone { border: 2px dashed #aaa; border-radius: 8px; padding: 2rem; text-align: center; margin: 1rem 0; transition: border-color 0.2s; } | |
| .drop-zone.active { border-color: #29d; background: #f0f8ff; } | |
| .entry { display: flex; align-items: center; gap: 1rem; padding: 0.5rem 0; } | |
| .progress { width: 100px; height: 8px; background: #eee; border-radius: 4px; overflow: hidden; } | |
| .progress-bar { height: 100%; background: #29d; transition: width 0.2s; } | |
| .error { color: #c33; font-size: 0.9em; } | |
| .file-list { margin-top: 1rem; } | |
| .file-list li { padding: 0.25rem 0; } | |
| </style> | |
| </head> | |
| <body> | |
| {@inner_content} | |
| <script src="/assets/phoenix.js"></script> | |
| <script src="/assets/phoenix_live_view.js"></script> | |
| <script> | |
| let Hooks = {} | |
| Hooks.FileEntry = { | |
| mounted() { | |
| const ref = this.el.dataset.ref | |
| const inputId = this.el.dataset.inputId | |
| const input = document.getElementById(inputId) | |
| console.log("FileEntry mounted, ref:", ref, "inputId:", inputId, "input:", input) | |
| this.el.querySelector("[data-role='ref-display']").textContent = LiveView.getFileURLForUpload(input, ref) | |
| }, | |
| updated() { | |
| const ref = this.el.dataset.ref | |
| const inputId = this.el.dataset.inputId | |
| const input = document.getElementById(inputId) | |
| console.log("FileEntry updated, ref:", ref, "inputId:", inputId, "input:", input) | |
| this.el.querySelector("[data-role='ref-display']").textContent = LiveView.getFileURLForUpload(input, ref) | |
| } | |
| } | |
| let liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, {hooks: Hooks}) | |
| liveSocket.connect() | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| end | |
| def render(assigns) do | |
| ~H""" | |
| <h1>File Uploader</h1> | |
| <form id="upload-form" phx-submit="save" phx-change="validate"> | |
| <div id="drop-zone" class="drop-zone" phx-drop-target={@uploads.files.ref}> | |
| <p>Drag & drop files here or</p> | |
| <.live_file_input upload={@uploads.files} /> | |
| </div> | |
| <div :for={entry <- @uploads.files.entries} id={"file-#{entry.ref}"} phx-hook="FileEntry" data-ref={entry.ref} data-input-id={@uploads.files.ref} class="entry"> | |
| <span>{entry.client_name}</span> | |
| <span>{entry.progress}%</span> | |
| <button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref}>×</button> | |
| <p :for={err <- upload_errors(@uploads.files, entry)} class="error">{inspect(err)}</p> | |
| <p data-role="ref-display"></p> | |
| </div> | |
| <button :if={@uploads.files.entries != []} type="submit">Upload</button> | |
| </form> | |
| <div :if={@uploaded_files != []} class="file-list"> | |
| <h2>Uploaded</h2> | |
| <ul> | |
| <li :for={file <- @uploaded_files}>{file}</li> | |
| </ul> | |
| </div> | |
| """ | |
| end | |
| def handle_event("validate", _params, socket) do | |
| {:noreply, socket} | |
| end | |
| def handle_event("save", _params, socket) do | |
| uploaded_files = | |
| consume_uploaded_entries(socket, :files, fn %{path: path}, entry -> | |
| dest = Path.join(@upload_dir, entry.client_name) | |
| File.cp!(path, dest) | |
| {:ok, entry.client_name} | |
| end) | |
| {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))} | |
| end | |
| def handle_event("cancel-upload", %{"ref" => ref}, socket) do | |
| {:noreply, cancel_upload(socket, :files, ref)} | |
| end | |
| end | |
| defmodule Sample.Router do | |
| use Phoenix.Router | |
| import Phoenix.LiveView.Router | |
| pipeline :browser do | |
| plug :accepts, ["html"] | |
| plug :fetch_session | |
| plug :put_root_layout, false | |
| plug :protect_from_forgery | |
| plug :put_secure_browser_headers | |
| end | |
| scope "/" do | |
| pipe_through :browser | |
| live "/", Sample.HomeLive | |
| end | |
| end | |
| defmodule Sample.Endpoint do | |
| use Phoenix.Endpoint, otp_app: :sample | |
| socket "/live", Phoenix.LiveView.Socket | |
| plug Plug.Static, | |
| at: "/assets", | |
| from: {:phoenix, "priv/static"}, | |
| only: ~w(phoenix.js phoenix.min.js) | |
| plug Plug.Static, | |
| at: "/assets", | |
| from: {:phoenix_live_view, "priv/static"}, | |
| only: ~w(phoenix_live_view.js phoenix_live_view.min.js) | |
| plug Plug.Session, | |
| store: :cookie, | |
| key: "_sample_key", | |
| signing_salt: "sample" | |
| plug Sample.Router | |
| end | |
| if System.get_env("EXS_DRY_RUN") == "true" do | |
| System.halt(0) | |
| else | |
| {:ok, _} = Supervisor.start_link([ | |
| {Phoenix.PubSub, name: Sample.PubSub}, | |
| Sample.Endpoint | |
| ], strategy: :one_for_one) | |
| Process.sleep(:infinity) | |
| end |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have used the sample from here and customized it to work with local phoenix liveview clone which happens to be on sibling folder.