Skip to content

Commit a6c2eef

Browse files
committed
implement embed component that can be used in slides/blogs
1 parent 8dca8bb commit a6c2eef

3 files changed

Lines changed: 258 additions & 20 deletions

File tree

eyg/src/eyg/sync/browser.gleam

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,32 @@ pub fn do_sync(tasks, message) {
3030
}
3131
}
3232

33-
pub fn do_load(message) {
34-
let task = {
35-
use registrations <- t.do(supabase.get_registrations())
36-
let registrations =
37-
dict.from_list(
38-
list.map(registrations, fn(registration) {
39-
#(registration.name, registration.package_id)
40-
}),
41-
)
33+
pub fn load_task() {
34+
use registrations <- t.do(supabase.get_registrations())
35+
let registrations =
36+
dict.from_list(
37+
list.map(registrations, fn(registration) {
38+
#(registration.name, registration.package_id)
39+
}),
40+
)
4241

43-
use releases <- t.do(supabase.get_releases())
44-
let releases =
45-
list.group(releases, fn(release) { release.package_id })
46-
|> dict.map_values(fn(_, releases) {
47-
list.map(releases, fn(release) { #(release.version, release) })
48-
|> dict.from_list
49-
})
42+
use releases <- t.do(supabase.get_releases())
43+
let releases =
44+
list.group(releases, fn(release) { release.package_id })
45+
|> dict.map_values(fn(_, releases) {
46+
list.map(releases, fn(release) { #(release.version, release) })
47+
|> dict.from_list
48+
})
5049

51-
use fragments <- t.do(supabase.get_fragments())
50+
use fragments <- t.do(supabase.get_fragments())
5251

53-
t.done(dump.Dump(registrations, releases, fragments))
54-
}
52+
t.done(dump.Dump(registrations, releases, fragments))
53+
}
5554

55+
pub fn do_load(message) {
5656
fn(d) {
5757
{
58-
use result <- promise.map(browser.run(task))
58+
use result <- promise.map(browser.run(load_task()))
5959
d(message(sync.DumpDownLoaded(result)))
6060
}
6161
Nil

website/src/website/dev.gleam

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn do_main(args) {
3131
[] as args | ["develop", ..args] -> develop(args)
3232
["deploy"] -> deploy(args)
3333
["email"] -> email()
34+
["embed"] -> embed()
3435

3536
_ ->
3637
promise.resolve(snag.error("no runner for: " <> args |> string.join(" ")))
@@ -114,3 +115,13 @@ fn email() {
114115

115116
node.run(task, ".")
116117
}
118+
119+
fn embed() {
120+
let task = {
121+
use bundle <- t.do(t.bundle("website/embed", "run"))
122+
t.write("../../../me/2025-01-24/eyg-predictable-and-useful/embed.js", <<
123+
bundle:utf8,
124+
>>)
125+
}
126+
node.run(task, ".")
127+
}

website/src/website/embed.gleam

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import eyg/sync/browser as remote
2+
import eyg/sync/sync
3+
import eygir/decode
4+
import gleam/int
5+
import gleam/io
6+
import gleam/javascript/array
7+
import gleam/javascript/promise
8+
import gleam/javascript/promisex
9+
import gleam/list
10+
import gleam/option.{None, Some}
11+
import gleam/string
12+
import harness/impl/browser as harness
13+
import harness/impl/spotless/netlify
14+
import harness/impl/spotless/netlify/deploy_site as netlify_deploy_site
15+
import harness/impl/spotless/twitter
16+
import harness/impl/spotless/twitter/tweet
17+
import lustre
18+
import lustre/attribute as a
19+
import lustre/effect
20+
import lustre/element as lelement
21+
import lustre/element/html as h
22+
import lustre/event
23+
import midas/browser
24+
import morph/editable as e
25+
import plinth/browser/document
26+
import plinth/browser/element
27+
import plinth/javascript/console
28+
import website/components/snippet
29+
import website/routes/editor
30+
31+
pub fn run() {
32+
let scripts = document.query_selector_all("[type='application/json+eyg']")
33+
let cache = sync.init(sync.test_origin)
34+
use result <- promise.map(browser.run(remote.load_task()))
35+
let assert Ok(dump) = result
36+
let cache = sync.load(cache, dump)
37+
38+
list.index_map(array.to_list(scripts), fn(script, i) {
39+
console.log(script)
40+
let id = "eyg" <> int.to_string(i)
41+
let json = element.inner_text(script)
42+
43+
element.insert_adjacent_html(
44+
script,
45+
element.AfterEnd,
46+
"<div id=\"" <> id <> "\"></div>",
47+
)
48+
let json = string.replace(json, "&quot;", "\"")
49+
let assert Ok(source) = decode.from_json(json)
50+
// ORIGIN is not used when pulling from supabase
51+
let app = lustre.application(init, update, render)
52+
let assert Ok(_) = lustre.start(app, "#" <> id, #(source, cache))
53+
})
54+
}
55+
56+
pub type State {
57+
State(menu: editor.Submenu, code: snippet.Snippet)
58+
}
59+
60+
fn init(config) {
61+
let #(source, cache) = config
62+
let source =
63+
e.from_expression(source)
64+
|> e.open_all
65+
let snippet = snippet.init(source, [], effects(), cache)
66+
let state = State(editor.Closed, snippet)
67+
#(state, effect.none())
68+
}
69+
70+
fn dispatch_to_snippet(promise) {
71+
effect.from(fn(d) {
72+
promisex.aside(promise, fn(message) { d(SnippetMessage(message)) })
73+
})
74+
}
75+
76+
fn dispatch_nothing(_promise) {
77+
effect.none()
78+
}
79+
80+
pub type Message {
81+
MenuMessage(editor.MenuMessage)
82+
SnippetMessage(snippet.Message)
83+
}
84+
85+
fn update(state, message) {
86+
let State(menu, snippet) = state
87+
case message {
88+
MenuMessage(editor.ActionClicked(k)) ->
89+
update(state, SnippetMessage(snippet.UserPressedCommandKey(k)))
90+
91+
MenuMessage(editor.ChangeSubmenu(new)) -> {
92+
let submenu = case new == menu {
93+
False -> new
94+
True -> editor.Closed
95+
}
96+
#(State(..state, menu: submenu), effect.none())
97+
}
98+
SnippetMessage(message) -> {
99+
let #(snippet, eff) = snippet.update(snippet, message)
100+
let #(failure, snippet_effect) = case eff {
101+
snippet.Nothing -> #(None, effect.none())
102+
snippet.Failed(failure) -> #(Some(failure), effect.none())
103+
snippet.AwaitRunningEffect(p) -> #(
104+
None,
105+
dispatch_to_snippet(snippet.await_running_effect(p)),
106+
)
107+
snippet.FocusOnCode -> #(
108+
None,
109+
dispatch_nothing(snippet.focus_on_buffer()),
110+
)
111+
snippet.FocusOnInput -> #(
112+
None,
113+
dispatch_nothing(snippet.focus_on_input()),
114+
)
115+
snippet.ToggleHelp -> #(None, effect.none())
116+
snippet.MoveAbove -> #(None, effect.none())
117+
snippet.MoveBelow -> #(None, effect.none())
118+
snippet.ReadFromClipboard -> #(
119+
None,
120+
dispatch_to_snippet(snippet.read_from_clipboard()),
121+
)
122+
snippet.WriteToClipboard(text) -> #(
123+
None,
124+
dispatch_to_snippet(snippet.write_to_clipboard(text)),
125+
)
126+
snippet.Conclude(_, _, _) -> #(None, effect.none())
127+
}
128+
io.debug(failure)
129+
#(State(editor.Closed, snippet), snippet_effect)
130+
}
131+
}
132+
}
133+
134+
fn render(state) {
135+
let State(menu, snippet) = state
136+
137+
h.pre(
138+
[
139+
a.class("eyg-embed language-eyg"),
140+
a.style([
141+
#("position", "relative"),
142+
#("margin", "0"),
143+
#("padding", "0"),
144+
#("overflow", "initial"),
145+
]),
146+
// This is needed to stop the component interfering with remark slides
147+
event.on("keypress", fn(event) {
148+
event.stop_propagation(event)
149+
Error([])
150+
}),
151+
],
152+
[
153+
// TODO show error
154+
render_menu(snippet, menu, False) |> lelement.map(MenuMessage),
155+
..snippet.bare_render(snippet, None)
156+
|> list.map(fn(e) { lelement.map(e, SnippetMessage) })
157+
],
158+
)
159+
}
160+
161+
fn effects() {
162+
harness.effects()
163+
|> list.append([
164+
#(
165+
netlify_deploy_site.l,
166+
#(
167+
netlify_deploy_site.lift(),
168+
netlify_deploy_site.reply(),
169+
netlify_deploy_site.blocking(netlify.local, _),
170+
),
171+
),
172+
#(
173+
tweet.l,
174+
#(tweet.lift(), tweet.reply(), tweet.blocking(
175+
twitter.client_id,
176+
twitter.redirect_uri,
177+
True,
178+
_,
179+
)),
180+
),
181+
])
182+
}
183+
184+
fn render_menu(snippet, submenu, display_help) {
185+
let snippet.Snippet(status: status, source: source, ..) = snippet
186+
let #(top, subcontent) = editor.menu_content(status, source.0, submenu)
187+
h.div(
188+
[
189+
a.class("eyg-menu-container"),
190+
a.style([
191+
#("position", "absolute"),
192+
#("left", "0"),
193+
#("top", "50%"),
194+
#("transform", "translate(calc(-100% - 10px), -50%)"),
195+
#("grid-template-columns", "max-content max-content"),
196+
#("overflow-x", "hidden"),
197+
#("overflow-y", "auto"),
198+
#("display", "grid"),
199+
]),
200+
],
201+
[
202+
render_column(top, display_help),
203+
case subcontent {
204+
None -> lelement.none()
205+
Some(#(_key, subitems)) -> render_column(subitems, display_help)
206+
},
207+
],
208+
)
209+
}
210+
211+
fn render_column(items, display_help) {
212+
h.div(
213+
[
214+
a.style([
215+
#("padding-top", ".5rem"),
216+
#("padding-bottom", ".5rem"),
217+
#("justify-content", "flex-end"),
218+
#("flex-direction", "column"),
219+
#("display", "flex"),
220+
]),
221+
],
222+
list.map(items, fn(entry) {
223+
let #(i, text, k) = entry
224+
editor.button(k, [editor.icon(i, text, display_help)])
225+
}),
226+
)
227+
}

0 commit comments

Comments
 (0)