Skip to content

Commit ce1e6c3

Browse files
committed
Add example Phoenix application
1 parent b367c8d commit ce1e6c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3113
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
import_deps: [:ecto, :ecto_sql, :phoenix],
3+
subdirectories: ["priv/*/migrations"],
4+
plugins: [Phoenix.LiveView.HTMLFormatter],
5+
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
6+
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Temporary files, for example, from tests.
23+
/tmp/
24+
25+
# Ignore package tarball (built via "mix hex.build").
26+
electric_phoenix-*.tar
27+
28+
# Ignore assets that are produced by build tools.
29+
/priv/static/assets/
30+
31+
# Ignore digested assets cache.
32+
/priv/static/cache_manifest.json
33+
34+
# In case you use Node.js/npm, you want to ignore these.
35+
npm-debug.log
36+
/assets/node_modules/
37+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Elixir Phoenix Example Application
2+
3+
Shows an example of using [Electric's Postgresql sync
4+
engine](https://electric-sql.com/) to maintain identical across multiple
5+
browser windows.
6+
7+
Instead of subscribing to an internal Phoenix pub-sub system, each LiveView
8+
instance instead subscribes to the same [Electric
9+
Shape](https://electric-sql.com/docs/guides/shapes).
10+
11+
Because of this, updates to the database from any client are synced immediately
12+
to all other connected clients without any extra work by the developer.
13+
14+
## Getting started
15+
16+
To start your Phoenix server:
17+
18+
- Run `mix setup` to install and setup dependencies
19+
- Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
20+
21+
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
22+
23+
If you open two separate windows, you will see you changes happen
24+
simultaneously in both windows.
25+
26+
## Implementation
27+
28+
See the [`Electric.PhoenixExampleWeb.TodoLive.Index` module](./lib/electric_phoenix_example_web/live/todo_live/index.ex) and the [`Electric.Phoenix` documentation](https://hexdocs.pm/electric_phoenix/).
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import "tailwindcss/base";
2+
@import "tailwindcss/components";
3+
@import "tailwindcss/utilities";
4+
5+
/* This file is for your main application CSS */
6+
7+
#blurb p + p {
8+
@apply pt-2;
9+
}
10+
#blurb a {
11+
@apply text-violet-500;
12+
}
13+
#blurb a:hover {
14+
@apply underline;
15+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
2+
// to get started and then uncomment the line below.
3+
// import "./user_socket.js"
4+
5+
// You can include dependencies in two ways.
6+
//
7+
// The simplest option is to put them in assets/vendor and
8+
// import them using relative paths:
9+
//
10+
// import "../vendor/some-package.js"
11+
//
12+
// Alternatively, you can `npm install some-package --prefix assets` and import
13+
// them using a path starting with the package name:
14+
//
15+
// import "some-package"
16+
//
17+
18+
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
19+
import "phoenix_html"
20+
// Establish Phoenix Socket and LiveView configuration.
21+
import {Socket} from "phoenix"
22+
import {LiveSocket} from "phoenix_live_view"
23+
import topbar from "../vendor/topbar"
24+
25+
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
26+
let liveSocket = new LiveSocket("/live", Socket, {
27+
longPollFallbackMs: 2500,
28+
params: {_csrf_token: csrfToken}
29+
})
30+
31+
// Show progress bar on live navigation and form submits
32+
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
33+
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
34+
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
35+
36+
// connect if there are any LiveViews on the page
37+
liveSocket.connect()
38+
39+
// expose liveSocket on window for web console debug logs and latency simulation:
40+
// >> liveSocket.enableDebug()
41+
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
42+
// >> liveSocket.disableLatencySim()
43+
window.liveSocket = liveSocket
44+
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// See the Tailwind configuration guide for advanced usage
2+
// https://tailwindcss.com/docs/configuration
3+
4+
const plugin = require("tailwindcss/plugin");
5+
const fs = require("fs");
6+
const path = require("path");
7+
8+
module.exports = {
9+
content: [
10+
"./js/**/*.js",
11+
"../lib/electric_phoenix_example_web.ex",
12+
"../lib/electric_phoenix_example_web/**/*.*ex",
13+
],
14+
theme: {
15+
extend: {
16+
colors: {
17+
brand: "#FD4F00",
18+
},
19+
},
20+
},
21+
plugins: [
22+
require("@tailwindcss/forms"),
23+
// Allows prefixing tailwind classes with LiveView classes to add rules
24+
// only when LiveView classes are applied, for example:
25+
//
26+
// <div class="phx-click-loading:animate-ping">
27+
//
28+
plugin(({ addVariant }) =>
29+
addVariant("phx-click-loading", [
30+
".phx-click-loading&",
31+
".phx-click-loading &",
32+
]),
33+
),
34+
plugin(({ addVariant }) =>
35+
addVariant("phx-submit-loading", [
36+
".phx-submit-loading&",
37+
".phx-submit-loading &",
38+
]),
39+
),
40+
plugin(({ addVariant }) =>
41+
addVariant("phx-change-loading", [
42+
".phx-change-loading&",
43+
".phx-change-loading &",
44+
]),
45+
),
46+
47+
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
48+
// See your `CoreComponents.icon/1` for more information.
49+
//
50+
plugin(function ({ matchComponents, theme }) {
51+
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized");
52+
let values = {};
53+
let icons = [
54+
["", "/24/outline"],
55+
["-solid", "/24/solid"],
56+
["-mini", "/20/solid"],
57+
["-micro", "/16/solid"],
58+
];
59+
icons.forEach(([suffix, dir]) => {
60+
fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
61+
let name = path.basename(file, ".svg") + suffix;
62+
values[name] = { name, fullPath: path.join(iconsDir, dir, file) };
63+
});
64+
});
65+
matchComponents(
66+
{
67+
hero: ({ name, fullPath }) => {
68+
let content = fs
69+
.readFileSync(fullPath)
70+
.toString()
71+
.replace(/\r?\n|\r/g, "");
72+
let size = theme("spacing.6");
73+
if (name.endsWith("-mini")) {
74+
size = theme("spacing.5");
75+
} else if (name.endsWith("-micro")) {
76+
size = theme("spacing.4");
77+
}
78+
return {
79+
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
80+
"-webkit-mask": `var(--hero-${name})`,
81+
mask: `var(--hero-${name})`,
82+
"mask-repeat": "no-repeat",
83+
"background-color": "currentColor",
84+
"vertical-align": "middle",
85+
display: "inline-block",
86+
width: size,
87+
height: size,
88+
};
89+
},
90+
},
91+
{ values },
92+
);
93+
}),
94+
],
95+
};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* @license MIT
3+
* topbar 2.0.0, 2023-02-04
4+
* https://buunguyen.github.io/topbar
5+
* Copyright (c) 2021 Buu Nguyen
6+
*/
7+
(function (window, document) {
8+
"use strict";
9+
10+
// https://gist.github.com/paulirish/1579671
11+
(function () {
12+
var lastTime = 0;
13+
var vendors = ["ms", "moz", "webkit", "o"];
14+
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15+
window.requestAnimationFrame =
16+
window[vendors[x] + "RequestAnimationFrame"];
17+
window.cancelAnimationFrame =
18+
window[vendors[x] + "CancelAnimationFrame"] ||
19+
window[vendors[x] + "CancelRequestAnimationFrame"];
20+
}
21+
if (!window.requestAnimationFrame)
22+
window.requestAnimationFrame = function (callback, element) {
23+
var currTime = new Date().getTime();
24+
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25+
var id = window.setTimeout(function () {
26+
callback(currTime + timeToCall);
27+
}, timeToCall);
28+
lastTime = currTime + timeToCall;
29+
return id;
30+
};
31+
if (!window.cancelAnimationFrame)
32+
window.cancelAnimationFrame = function (id) {
33+
clearTimeout(id);
34+
};
35+
})();
36+
37+
var canvas,
38+
currentProgress,
39+
showing,
40+
progressTimerId = null,
41+
fadeTimerId = null,
42+
delayTimerId = null,
43+
addEvent = function (elem, type, handler) {
44+
if (elem.addEventListener) elem.addEventListener(type, handler, false);
45+
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
46+
else elem["on" + type] = handler;
47+
},
48+
options = {
49+
autoRun: true,
50+
barThickness: 3,
51+
barColors: {
52+
0: "rgba(26, 188, 156, .9)",
53+
".25": "rgba(52, 152, 219, .9)",
54+
".50": "rgba(241, 196, 15, .9)",
55+
".75": "rgba(230, 126, 34, .9)",
56+
"1.0": "rgba(211, 84, 0, .9)",
57+
},
58+
shadowBlur: 10,
59+
shadowColor: "rgba(0, 0, 0, .6)",
60+
className: null,
61+
},
62+
repaint = function () {
63+
canvas.width = window.innerWidth;
64+
canvas.height = options.barThickness * 5; // need space for shadow
65+
66+
var ctx = canvas.getContext("2d");
67+
ctx.shadowBlur = options.shadowBlur;
68+
ctx.shadowColor = options.shadowColor;
69+
70+
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
71+
for (var stop in options.barColors)
72+
lineGradient.addColorStop(stop, options.barColors[stop]);
73+
ctx.lineWidth = options.barThickness;
74+
ctx.beginPath();
75+
ctx.moveTo(0, options.barThickness / 2);
76+
ctx.lineTo(
77+
Math.ceil(currentProgress * canvas.width),
78+
options.barThickness / 2
79+
);
80+
ctx.strokeStyle = lineGradient;
81+
ctx.stroke();
82+
},
83+
createCanvas = function () {
84+
canvas = document.createElement("canvas");
85+
var style = canvas.style;
86+
style.position = "fixed";
87+
style.top = style.left = style.right = style.margin = style.padding = 0;
88+
style.zIndex = 100001;
89+
style.display = "none";
90+
if (options.className) canvas.classList.add(options.className);
91+
document.body.appendChild(canvas);
92+
addEvent(window, "resize", repaint);
93+
},
94+
topbar = {
95+
config: function (opts) {
96+
for (var key in opts)
97+
if (options.hasOwnProperty(key)) options[key] = opts[key];
98+
},
99+
show: function (delay) {
100+
if (showing) return;
101+
if (delay) {
102+
if (delayTimerId) return;
103+
delayTimerId = setTimeout(() => topbar.show(), delay);
104+
} else {
105+
showing = true;
106+
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
107+
if (!canvas) createCanvas();
108+
canvas.style.opacity = 1;
109+
canvas.style.display = "block";
110+
topbar.progress(0);
111+
if (options.autoRun) {
112+
(function loop() {
113+
progressTimerId = window.requestAnimationFrame(loop);
114+
topbar.progress(
115+
"+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
116+
);
117+
})();
118+
}
119+
}
120+
},
121+
progress: function (to) {
122+
if (typeof to === "undefined") return currentProgress;
123+
if (typeof to === "string") {
124+
to =
125+
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
126+
? currentProgress
127+
: 0) + parseFloat(to);
128+
}
129+
currentProgress = to > 1 ? 1 : to;
130+
repaint();
131+
return currentProgress;
132+
},
133+
hide: function () {
134+
clearTimeout(delayTimerId);
135+
delayTimerId = null;
136+
if (!showing) return;
137+
showing = false;
138+
if (progressTimerId != null) {
139+
window.cancelAnimationFrame(progressTimerId);
140+
progressTimerId = null;
141+
}
142+
(function loop() {
143+
if (topbar.progress("+.1") >= 1) {
144+
canvas.style.opacity -= 0.05;
145+
if (canvas.style.opacity <= 0.05) {
146+
canvas.style.display = "none";
147+
fadeTimerId = null;
148+
return;
149+
}
150+
}
151+
fadeTimerId = window.requestAnimationFrame(loop);
152+
})();
153+
},
154+
};
155+
156+
if (typeof module === "object" && typeof module.exports === "object") {
157+
module.exports = topbar;
158+
} else if (typeof define === "function" && define.amd) {
159+
define(function () {
160+
return topbar;
161+
});
162+
} else {
163+
this.topbar = topbar;
164+
}
165+
}.call(this, window, document));

0 commit comments

Comments
 (0)