+ These apps are built with
+ Local LiveView
+ — Phoenix LiveView running entirely in your browser thanks to
+ Popcorn. The source code is available
+ on GitHub.
+
+
+
+
+
\ No newline at end of file
diff --git a/docker/demos/nginx.conf b/docker/demos/nginx.conf
new file mode 100644
index 000000000..76b6d315f
--- /dev/null
+++ b/docker/demos/nginx.conf
@@ -0,0 +1,72 @@
+worker_processes 1;
+pid /run/nginx.pid;
+error_log /dev/stderr warn;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ access_log off;
+ default_type text/html;
+ include /etc/nginx/mime.types;
+ sendfile on;
+
+ map $http_upgrade $connection_upgrade {
+ default upgrade;
+ "" close;
+ }
+
+ server {
+ # __PORT__ is substituted by the entrypoint (defaults to 4000).
+ listen __PORT__;
+ # Relative redirects: the container port is not the public port/scheme.
+ absolute_redirect off;
+
+ location = / {
+ root /var/www/demos;
+ try_files /index.html =404;
+ }
+
+ location = /kanban {
+ return 301 /kanban/;
+ }
+
+ # Strips the /kanban prefix; the app generates prefixed URLs itself
+ # via URL_PATH=/kanban.
+ location /kanban/ {
+ proxy_pass http://127.0.0.1:4001/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location = /burrito {
+ return 301 /burrito/;
+ }
+
+ location /burrito/ {
+ proxy_pass http://127.0.0.1:4002/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location = /pong {
+ return 301 /pong/;
+ }
+
+ location /pong/ {
+ proxy_pass http://127.0.0.1:4003/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+ }
+}
diff --git a/examples/local-lv-burrito/assets/js/app.js b/examples/local-lv-burrito/assets/js/app.js
index 9c96a0269..44a3c588e 100644
--- a/examples/local-lv-burrito/assets/js/app.js
+++ b/examples/local-lv-burrito/assets/js/app.js
@@ -330,11 +330,16 @@ window.toggleLatency = function () {
// ─── LiveSocket Setup ─────────────────────────────────────────────────────────
+// When deployed under a path prefix (URL_PATH, e.g. /burrito behind a
+// prefix-stripping proxy) the socket and bundle URLs need that prefix too;
+// derive it from this bundle's own URL so dev (no prefix) works unchanged.
+const basePath = new URL(import.meta.url).pathname.replace(/\/assets\/.*$/, "");
+
const csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
-const liveSocket = new LiveSocket("/live", Socket, {
+const liveSocket = new LiveSocket(`${basePath}/live`, Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: { ScrollSync, InfoModal, LLVLoader },
@@ -351,7 +356,8 @@ liveSocket.connect();
window.liveSocket = liveSocket;
window.llvEngine = await LLVEngine.create(liveSocket, {
- bundlePaths: ["/assets/js/wasm/bundle.avm"],
+ bundlePaths: [`${basePath}/assets/js/wasm/bundle.avm`],
+ llvSocketURI: `${basePath}/llv_socket`,
});
window.dispatchEvent(new CustomEvent("llv:ready"));
diff --git a/examples/local-lv-burrito/config/runtime.exs b/examples/local-lv-burrito/config/runtime.exs
index e76e4a5a8..62f3d26cf 100644
--- a/examples/local-lv-burrito/config/runtime.exs
+++ b/examples/local-lv-burrito/config/runtime.exs
@@ -18,7 +18,9 @@ if config_env() == :prod do
config :burrito, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :burrito, BurritoWeb.Endpoint,
- url: [host: host, port: 443, scheme: "https"],
+ # URL_PATH lets the app be served under a path prefix (e.g. /burrito
+ # behind a reverse proxy that strips the prefix).
+ url: [host: host, port: 443, scheme: "https", path: System.get_env("URL_PATH", "/")],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port
diff --git a/examples/local-lv-burrito/mix.exs b/examples/local-lv-burrito/mix.exs
index 0a6bc2cb9..f7324585b 100644
--- a/examples/local-lv-burrito/mix.exs
+++ b/examples/local-lv-burrito/mix.exs
@@ -9,7 +9,6 @@ defmodule Burrito.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
- releases: releases(),
deps: deps(),
compilers: [:phoenix_live_view] ++ Mix.compilers(),
listeners: [Phoenix.CodeReloader]
@@ -50,20 +49,11 @@ defmodule Burrito.MixProject do
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"},
- {:local_live_view,
- path: System.get_env("LOCAL_LIVE_VIEW_PATH", "../../local-live-view"), runtime: false},
+ {:local_live_view, path: System.get_env("LOCAL_LIVE_VIEW_PATH", "../../local-live-view")},
{:local, path: "local"}
]
end
- defp releases do
- [
- burrito: [
- applications: [local_live_view: :load]
- ]
- ]
- end
-
defp aliases do
[
build: ["setup"],
diff --git a/examples/local-lv-kanban/assets/js/app.js b/examples/local-lv-kanban/assets/js/app.js
index b8df1c9e3..494939c44 100644
--- a/examples/local-lv-kanban/assets/js/app.js
+++ b/examples/local-lv-kanban/assets/js/app.js
@@ -25,8 +25,13 @@ import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/local_lv_kanban"
import topbar from "../vendor/topbar"
+// When deployed under a path prefix (URL_PATH, e.g. /kanban behind a
+// prefix-stripping proxy) the socket and bundle URLs need that prefix too;
+// derive it from this bundle's own URL so dev (no prefix) works unchanged.
+const basePath = new URL(import.meta.url).pathname.replace(/\/assets\/.*$/, "")
+
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-const liveSocket = new LiveSocket("/live", Socket, {
+const liveSocket = new LiveSocket(`${basePath}/live`, Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks},
@@ -41,7 +46,10 @@ window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
liveSocket.connect();
import { LLVEngine } from "local_live_view";
-await LLVEngine.create(liveSocket, { bundlePaths: ["/assets/js/wasm/bundle.avm"] });
+await LLVEngine.create(liveSocket, {
+ bundlePaths: [`${basePath}/assets/js/wasm/bundle.avm`],
+ llvSocketURI: `${basePath}/llv_socket`,
+});
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
diff --git a/examples/local-lv-kanban/config/runtime.exs b/examples/local-lv-kanban/config/runtime.exs
index 50e54f5bd..3d103999f 100644
--- a/examples/local-lv-kanban/config/runtime.exs
+++ b/examples/local-lv-kanban/config/runtime.exs
@@ -58,7 +58,9 @@ if config_env() == :prod do
config :local_lv_kanban, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :local_lv_kanban, LocalLvKanbanWeb.Endpoint,
- url: [host: host, port: 443, scheme: "https"],
+ # URL_PATH lets the app be served under a path prefix (e.g. /kanban
+ # behind a reverse proxy that strips the prefix).
+ url: [host: host, port: 443, scheme: "https", path: System.get_env("URL_PATH", "/")],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
diff --git a/examples/local-lv-kanban/lib/local_lv_kanban/release.ex b/examples/local-lv-kanban/lib/local_lv_kanban/release.ex
new file mode 100644
index 000000000..60c816496
--- /dev/null
+++ b/examples/local-lv-kanban/lib/local_lv_kanban/release.ex
@@ -0,0 +1,43 @@
+defmodule LocalLvKanban.Release do
+ @moduledoc """
+ Used for executing DB release tasks when run in production, without Mix
+ installed.
+ """
+ @app :local_lv_kanban
+
+ def migrate do
+ load_app()
+
+ for repo <- repos() do
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
+ end
+ end
+
+ def rollback(repo, version) do
+ load_app()
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
+ end
+
+ # Not part of the phx.gen.release template: runs priv/repo/seeds.exs, which
+ # is idempotent, so this is safe on every deploy.
+ def seed do
+ load_app()
+
+ for repo <- repos() do
+ {:ok, _, _} =
+ Ecto.Migrator.with_repo(repo, fn _repo ->
+ seeds = Path.join([Application.app_dir(@app, "priv"), "repo", "seeds.exs"])
+ if File.exists?(seeds), do: Code.eval_file(seeds)
+ end)
+ end
+ end
+
+ defp repos do
+ Application.fetch_env!(@app, :ecto_repos)
+ end
+
+ defp load_app do
+ Application.ensure_all_started(:ssl)
+ Application.ensure_loaded(@app)
+ end
+end
diff --git a/examples/local-lv-kanban/lib/local_lv_kanban_web/live/boards_live.ex b/examples/local-lv-kanban/lib/local_lv_kanban_web/live/boards_live.ex
index 3d101c242..258f6e86d 100644
--- a/examples/local-lv-kanban/lib/local_lv_kanban_web/live/boards_live.ex
+++ b/examples/local-lv-kanban/lib/local_lv_kanban_web/live/boards_live.ex
@@ -58,7 +58,22 @@ defmodule LocalLvKanbanWeb.BoardsLive do
<.flash kind={:error} flash={@flash} />
-
Kanban boards
+
Kanban boards
+
+
+ Create, browse and edit kanban-style boards.
+ The board view supports drag & drop,
+ optimistic updates, all the modals/forms are handled locally,
+ and it's 100% Elixir.
+ Create a board, then disconnect from the network and see how it behaves.
+ It also demonstrates server synchronization via LLV's push_server_event
+ mechanism - reconnect and open two windows side-by-side to see it in action.
+ The source code is available
+ here.
+