this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Gleam fe fix editor (#57)

* Split up site.gleam

* Implement Editor functions like markdown rendering, tab switching, resizing/moving and a functioning fold/unfold

authored by

Mar and committed by
GitHub
734bb1df e6ca0ad6

+556 -174
+1
.gitattributes
··· 2 2 * text=auto 3 3 *.svg linguist-detectable 4 4 *_ffi.ts linguist-language=Gleam 5 + *_ffi.mjs linguist-language=Gleam
+12
frontend/README.md
··· 1 + # Gleam frontend! 2 + 3 + This is the Lumina frontend, written in Gleam. 4 + 5 + ## Why the externals? 6 + 7 + The Gleam frontend is written in Gleam, but it uses a lot of JavaScript. This is because the Gleam ecosystem is still young and the existing libraries do not cover all the functionality we need. We hope that in the future we can replace the JavaScript with Gleam code. 8 + 9 + (Also, I was kinda lazy and didn't want to be translating JavaScript to Gleam all day.) 10 + 11 + 12 + Anyways: If you want to help reduce the amount of JavaScript in the frontend, please do!
+3 -3
frontend/TODO.md
··· 7 7 - [ ] Verify compile moment thru FEJSON 8 8 - [ ] User action reporting 9 9 - [ ] New post creation 10 - - [ ] Log out 10 + - [x] Log out 11 11 - [ ] Sub page loading 12 12 - [x] Loading sub pages (main, side) 13 13 - [ ] Migrating to Handlebars parsing on the client side, instead of server-leveraged strings. 14 14 - [ ] Loading special sub pages (editor, settings) 15 - - [ ] Sub page logic 15 + - [x] Sub page logic 16 16 - [ ] Editor 17 - - [ ] Editor UI 17 + - [x] Editor UI 18 18 - [ ] Editor tab switching 19 19 - [ ] Editor Markdown parsing 20 20 - [x] Sign in
+1
frontend/gleam.toml
··· 23 23 gleam_javascript = ">= 0.13.0 and < 1.0.0" 24 24 gleam_json = ">= 2.0.0 and < 3.0.0" 25 25 pprint = ">= 1.0.4 and < 2.0.0" 26 + gleam_regexp = ">= 1.0.0 and < 2.0.0" 26 27 27 28 [dev-dependencies] 28 29 gleeunit = ">= 1.0.0 and < 2.0.0"
+37
frontend/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" }, 6 + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 7 + { name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" }, 8 + { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, 9 + { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, 10 + { name = "gleam_fetch", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2FCFC0A85A6D5A83076889EEDD02D6779C02FEF6FDE0263C4D3B46D0785EAB7A" }, 11 + { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, 12 + { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, 13 + { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, 14 + { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" }, 15 + { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, 16 + { name = "gleam_stdlib", version = "0.52.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "50703862DF26453B277688FFCDBE9DD4AC45B3BD9742C0B370DB62BC1629A07D" }, 17 + { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" }, 18 + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 19 + { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" }, 20 + { name = "plinth", version = "0.5.6", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "7A9BEAB98F4BC6859803D5ABCD0F3B4C3578A747F12BF32F9E234F8325F5F0E0" }, 21 + { name = "pprint", version = "1.0.4", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "C310A98BDC0995644847C3C8702DE19656D6BCD638B2A8A358B97824379ECAA1" }, 22 + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "../shared" }, 23 + ] 24 + 25 + [requirements] 26 + gleam_fetch = { version = ">= 1.0.1 and < 2.0.0" } 27 + gleam_http = { version = ">= 3.7.0 and < 4.0.0" } 28 + gleam_javascript = { version = ">= 0.13.0 and < 1.0.0" } 29 + gleam_json = { version = ">= 2.0.0 and < 3.0.0" } 30 + gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } 31 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 32 + gleamy_lights = { version = ">= 2.3.0 and < 3.0.0" } 33 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 34 + lustre = { version = ">= 4.5.0 and < 5.0.0" } 35 + plinth = { version = ">= 0.5.0 and < 1.0.0" } 36 + pprint = { version = ">= 1.0.4 and < 2.0.0" } 37 + shared = { path = "../shared/" }
+156
frontend/src/editor_ffi.mjs
··· 1 + export function postfoldout() { 2 + window.dragEditor = (e) => { 3 + e.preventDefault(); 4 + window.editorposition3 = e.clientX; 5 + window.editorposition4 = e.clientY; 6 + document.onmouseup = window.stopEditorDragging; 7 + document.onmousemove = window.editorDrag; 8 + }; 9 + window.editorDrag = (e) => { 10 + e.preventDefault(); 11 + const divPostEditor = document.querySelector("div#posteditor"); 12 + divPostEditor.style.width = "70VH"; 13 + divPostEditor.style.height = "calc(50VW - 30VH)"; 14 + divPostEditor.style.position = ""; 15 + divPostEditor.style.marginTop = ""; 16 + divPostEditor.style.marginBottom = ""; 17 + divPostEditor.style.marginLeft = ""; 18 + divPostEditor.style.marginRight = ""; 19 + 20 + window.editorposition1 = window.editorposition3 - e.clientX; 21 + window.editorposition2 = (() => { 22 + const o = window.editorposition4 - e.clientY; 23 + if (divPostEditor.offsetTop - o < 20) { 24 + return divPostEditor.offsetTop - 40; 25 + } 26 + return o; 27 + })(); 28 + window.editorposition3 = e.clientX; 29 + window.editorposition4 = e.clientY; 30 + divPostEditor.style.top = `${ 31 + divPostEditor.offsetTop - window.editorposition2 32 + }px`; 33 + divPostEditor.style.left = `${ 34 + divPostEditor.offsetLeft - window.editorposition1 35 + }px`; 36 + }; 37 + 38 + window.stopEditorDragging = () => { 39 + document.onmouseup = null; 40 + document.onmousemove = null; 41 + }; 42 + document 43 + .getElementById("editorwindowh") 44 + .addEventListener("mousedown", window.dragEditor); 45 + window.editorFullScreenMode = (e) => { 46 + e.preventDefault(); 47 + const divPostEditor = document.querySelector("div#posteditor"); 48 + divPostEditor.style.width = "95VW"; 49 + divPostEditor.style.height = "85VH"; 50 + divPostEditor.style.position = "fixed"; 51 + divPostEditor.style.marginTop = "auto"; 52 + divPostEditor.style.marginBottom = "auto"; 53 + divPostEditor.style.marginLeft = "auto"; 54 + divPostEditor.style.marginRight = "auto"; 55 + divPostEditor.style.top = "60px"; 56 + divPostEditor.style.bottom = "0"; 57 + divPostEditor.style.left = "0"; 58 + divPostEditor.style.right = "0"; 59 + }; 60 + document 61 + .getElementById("editorwindowh") 62 + .addEventListener("dblclick", window.editorFullScreenMode); 63 + document.body.dataset.editorOpen = "true"; 64 + } 65 + export function switcheditormode(elm, renderMarkdownShort, renderMarkdownLong) { 66 + const activeElement = document.activeElement; 67 + const modenames = ["short", "long", "embed"]; 68 + const desiredmode = elm.dataset.modeOpener; 69 + for (const modename of modenames) { 70 + const opener = document.querySelector( 71 + `nav#editormodepicker [data-mode-opener="${modename}"]`, 72 + ); 73 + const field = document.querySelector( 74 + `div#editorwindowm [data-mode-field="${modename}"]`, 75 + ); 76 + 77 + if (modename === desiredmode) { 78 + opener.className = 79 + "editor-switcher flex items-center justify-center p-0 bg-orange-100 border-2 border-b-0 rounded-md rounded-b-none cursor-default border-emerald-600 dark:text-orange-100 dark:bg-neutral-800 text-brown-800 dark:border-zinc-400"; 80 + field.classList.add("block"); 81 + field.classList.remove("hidden"); 82 + } else { 83 + opener.className = 84 + "editor-switcher flex items-center justify-center p-0 border-2 rounded-md cursor-pointer bg-emerald-200 dark:bg-teal-800 border-emerald-600 dark:text-orange-100 hover:text-white hover:bg-gray-700 text-brown-800 dark:border-zinc-400"; 85 + field.classList.add("hidden"); 86 + field.classList.remove("block"); 87 + } 88 + } 89 + switch (desiredmode) { 90 + case "short": 91 + { 92 + document 93 + .getElementById("editor-short-input") 94 + .addEventListener("change", renderMarkdownShort); 95 + setInterval(renderMarkdownShort, 400); 96 + renderMarkdownShort(); 97 + 98 + document.addEventListener("keydown", (ev) => { 99 + if ( 100 + ev.key === "Enter" && 101 + activeElement === 102 + document.getElementById("editor-short-container") 103 + ) { 104 + document.getElementById("editor-short-input").focus(); 105 + } else if ( 106 + ev.key === "Escape" && 107 + activeElement === 108 + document.getElementById("editor-short-input") 109 + ) { 110 + activeElement.blur(); 111 + } 112 + }); 113 + 114 + document 115 + .getElementById("editor-short-container") 116 + .addEventListener("click", () => { 117 + document.getElementById("editor-short-input").focus(); 118 + }); 119 + document.getElementById("editor-short-input").focus(); 120 + } 121 + break; 122 + case "long": 123 + { 124 + document 125 + .getElementById("editor-long-input") 126 + .addEventListener("change", renderMarkdownLong); 127 + renderMarkdownLong(); 128 + 129 + document.addEventListener("keydown", (ev) => { 130 + if ( 131 + ev.key === "Enter" && 132 + activeElement === 133 + document.getElementById("editor-long-container") 134 + ) { 135 + document.getElementById("editor-long-input").focus(); 136 + } else if ( 137 + ev.key === "Escape" && 138 + activeElement === 139 + document.getElementById("editor-long-input") 140 + ) { 141 + activeElement.blur(); 142 + } 143 + }); 144 + 145 + document 146 + .getElementById("editor-long-container") 147 + .addEventListener("click", () => { 148 + document.getElementById("editor-long-input").focus(); 149 + }); 150 + document.getElementById("editor-long-input").focus(); 151 + } 152 + break; 153 + default: 154 + break; 155 + } 156 + }
+3
frontend/src/elementactions_ffi.ts
··· 40 40 export function getWindowLocationHash() { 41 41 return window.location.hash; 42 42 } 43 + export function getValue(elem: HTMLInputElement) { 44 + return elem.value; 45 + }
+15 -13
frontend/src/frontend/other/element_actions.gleam
··· 1 1 // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman 2 2 // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. 3 + import gleam/http 4 + import gleam/http/request.{type Request} 3 5 import gleam/string 4 6 import plinth/browser/element.{type Element} 5 7 import plinth/browser/window 6 - import gleam/http/request.{type Request} 7 - import gleam/http 8 8 9 9 @external(javascript, "../../elementactions_ffi.ts", "disableElement") 10 10 pub fn disable_element(a: Element) -> nil ··· 44 44 } 45 45 46 46 pub fn phone_home() -> Request(String) { 47 - request.new() 48 - |> request.set_scheme({ 49 - let origin = window.origin() 50 - case origin { 51 - "http://" <> _ -> http.Http 52 - "https://" <> _ -> http.Https 53 - _ -> http.Https 54 - } 55 - }) 56 - 57 - |> request.set_host(get_window_host()) 47 + request.new() 48 + |> request.set_scheme({ 49 + let origin = window.origin() 50 + case origin { 51 + "http://" <> _ -> http.Http 52 + "https://" <> _ -> http.Https 53 + _ -> http.Https 54 + } 55 + }) 56 + |> request.set_host(get_window_host()) 58 57 } 58 + 59 + @external(javascript, "../../elementactions_ffi.ts", "getValue") 60 + pub fn get_value(a: Element) -> String
+14
frontend/src/frontend/other/rust_kind_of_unwrap.gleam
··· 1 + import plinth/javascript/console 2 + 3 + /// Be bad! 4 + /// This is a bad way to unwrap a Result. It will panic if the Result is an Error. 5 + /// However, we know what's in the HTML, so we can safely unwrap it. 6 + pub fn unwrap(p: Result(a, e)) -> a { 7 + case p { 8 + Ok(a) -> a 9 + Error(e) -> { 10 + console.error(e) 11 + panic as "Failed to unwrap Result" 12 + } 13 + } 14 + }
+16 -158
frontend/src/frontend/page/site.gleam
··· 1 1 // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman 2 2 // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. 3 3 4 + import frontend/other/element_actions 4 5 import frontend/other/rendering 6 + import frontend/page/site/editor 7 + import frontend/page/site/subpages 5 8 import gleam/bool 6 - import gleam/javascript/array 7 - import gleam/list 8 - import lumina/shared/shared_fepage_com.{ 9 - type FEPageServeResponse, FEPageServeResponse, 10 - } 11 - import plinth/browser/event 12 - import plinth/javascript/global 13 - import plinth/javascript/storage 14 - 15 - import frontend/other/element_actions 16 9 import gleam/dict.{type Dict} 17 10 import gleam/dynamic 18 11 import gleam/fetch 19 12 import gleam/http 20 13 import gleam/http/request 21 14 import gleam/http/response 15 + import gleam/javascript/array 22 16 import gleam/javascript/promise 23 17 import gleam/json 18 + import gleam/list 24 19 import gleam/string 25 20 import gleamy_lights/console 26 21 import gleamy_lights/premixed 27 22 import gleamy_lights/premixed/gleam_colours 23 + import lumina/shared/shared_fepage_com.{ 24 + type FEPageServeResponse, FEPageServeResponse, 25 + } 28 26 import plinth/browser/document 29 27 import plinth/browser/element 28 + import plinth/browser/event 30 29 import plinth/browser/window 30 + import plinth/javascript/global 31 + import plinth/javascript/storage 31 32 32 33 pub fn home_render() { 33 34 console.log( ··· 86 87 let assert Ok(a) = document.query_selector("#mobile-home-nav") 87 88 a 88 89 }, 89 - fn() { editor_unfold() }, 90 + fn() { editor.unfold() }, 90 91 "editor", 91 92 False, 92 93 ), ··· 118 119 global.set_interval(60, fn() { 119 120 check_if_page_needs_to_be_switched(sub_page_list) 120 121 }) 121 - editor_fold() 122 + editor.fold() 122 123 { 123 124 let assert Ok(a) = 124 125 document.get_element_by_id("switchpageNotificationsTrigger") ··· 132 133 let assert Ok(a) = document.get_element_by_id("editorTrigger") 133 134 a 134 135 |> element.add_event_listener("click", fn(_) { 135 - trigger_editor() 136 + editor.trigger() 136 137 Nil 137 138 }) 138 139 } ··· 155 156 case event |> event.key() |> string.lowercase() { 156 157 "e" -> { 157 158 event |> event.prevent_default() 158 - trigger_editor() 159 + editor.trigger() 159 160 } 160 161 "h" -> { 161 162 event |> event.prevent_default() ··· 172 173 Nil 173 174 } 174 175 175 - fn editor_unfold() { 176 - let assert Ok(mobiletimelineswitcher) = 177 - document.query_selector("#mobiletimelineswitcher") 178 - let assert Ok(posteditor) = document.query_selector("div#posteditor") 179 - let errormsg = 180 - "<p class=\"w-full h-full text-black bg-white dark:text-white dark:bg-black\">Failed to load post editor.</p>" 181 - mobiletimelineswitcher |> element_actions.hide_element() 182 - posteditor |> element_actions.show_element() 183 - case document.body() |> element.dataset_get("editorOpen") { 184 - Ok("initial") -> { 185 - fetch_editor() 186 - Nil 187 - } 188 - _ -> Nil 189 - } 190 - 191 - todo 192 - } 193 - 194 - fn trigger_editor() { 195 - let hash = element_actions.get_window_location_hash() 196 - 197 - case document.body() |> element.dataset_get("editorOpen") { 198 - Ok("true") -> { 199 - console.info( 200 - "triggerEditor: got called, but editor is already open. Refolding editor instead.", 201 - ) 202 - editor_fold() 203 - } 204 - _ -> { 205 - case hash == "editor" { 206 - True -> { 207 - // Editor glitched out, going back to retry... 208 - console.log("triggerEditor: retrying...") 209 - element_actions.go_back() 210 - global.set_timeout(600, fn() { 211 - element_actions.set_window_location_hash("editor") 212 - Nil 213 - }) 214 - Nil 215 - } 216 - False -> { 217 - element_actions.set_window_location_hash("editor") 218 - } 219 - } 220 - } 221 - } 222 - } 223 - 224 - fn editor_fold() { 225 - let assert Ok(posteditor) = document.query_selector("div#posteditor") 226 - posteditor |> element_actions.hide_element() 227 - case document.body() |> element.dataset_get("editorOpen") { 228 - Ok(_) -> 229 - document.body() |> element.set_attribute("data-editor-open", "false") 230 - Error(_) -> 231 - document.body() |> element.set_attribute("data-editor-open", "initial") 232 - } 233 - } 234 - 235 - fn fetch_page(page: String, then: fn(Result(FEPageServeResponse, Nil)) -> Nil) { 236 - { 237 - let req = 238 - { 239 - let assert Ok(a) = request.to(window.origin() <> "/api/fe/fetch-page") 240 - a 241 - } 242 - |> request.set_body("{\"location\": \"" <> page <> "\"}") 243 - |> request.set_header("Content-Type", "application/json") 244 - |> request.set_method(http.Post) 245 - use resp <- promise.try_await(fetch.send(req)) 246 - use resp <- promise.try_await(fetch.read_text_body(resp)) 247 - promise.resolve(Ok(resp)) 248 - } 249 - |> promise.await(fn(a: Result(response.Response(String), fetch.FetchError)) { 250 - case a { 251 - Ok(b) -> { 252 - case 253 - json.decode( 254 - from: b.body, 255 - using: dynamic.decode3( 256 - FEPageServeResponse, 257 - dynamic.field("main", dynamic.string), 258 - dynamic.field("side", dynamic.string), 259 - dynamic.field("message", dynamic.list(dynamic.int)), 260 - ), 261 - ) 262 - { 263 - Ok(c) -> then(Ok(c)) 264 - Error(_) -> then(Error(Nil)) 265 - } 266 - } 267 - Error(_) -> then(Error(Nil)) 268 - } 269 - promise.resolve(Nil) 270 - }) 271 - } 272 - 273 - fn fetch_editor() { 274 - use presp <- fetch_page("editor", _) 275 - case presp { 276 - Ok(resp) -> { 277 - console.log("Page: " <> premixed.text_lightblue(string.inspect(resp))) 278 - let resp: FEPageServeResponse = resp 279 - let message_list = resp.message 280 - case 281 - bool.and( 282 - message_list |> list.contains(1) |> bool.negate, 283 - message_list |> list.contains(2) |> bool.negate, 284 - ) 285 - { 286 - True -> { 287 - // document.querySelector("div#posteditor").innerHTML = 288 - // response.data.main; 289 - // window.history.back(); 290 - { 291 - let assert Ok(a) = document.query_selector("div#posteditor") 292 - a 293 - } 294 - |> element.set_inner_html(resp.main) 295 - element_actions.go_back() 296 - Nil 297 - } 298 - False -> { 299 - // document.querySelector("div#posteditor").innerHTML = 300 - // errormsg; 301 - { 302 - let assert Ok(a) = document.query_selector("div#posteditor") 303 - a 304 - } 305 - |> element.set_inner_html( 306 - "<p class=\"w-full h-full text-black bg-white dark:text-white dark:bg-black\">Failed to load post editor.</p>", 307 - ) 308 - Nil 309 - } 310 - } 311 - } 312 - Error(_) -> { 313 - Nil 314 - } 315 - } 316 - } 317 - 318 176 pub fn index_render() { 319 177 console.log( 320 178 "Detected you are on the " ··· 434 292 "class", 435 293 "border-2 px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-md", 436 294 ) 437 - use resp <- fetch_page(location) 295 + use resp <- subpages.fetch(location) 438 296 case resp { 439 297 Error(_) -> { 440 298 console.error("Failed to fetch page." |> premixed.text_error_red())
+249
frontend/src/frontend/page/site/editor.gleam
··· 1 + import frontend/other/element_actions 2 + import frontend/other/rust_kind_of_unwrap.{unwrap} 3 + import frontend/page/site/subpages 4 + import gleam/bool 5 + import gleam/dynamic/decode 6 + import gleam/fetch 7 + import gleam/http.{Post} 8 + import gleam/http/request 9 + import gleam/javascript/array 10 + import gleam/javascript/promise 11 + import gleam/json 12 + import gleam/list 13 + import gleam/regexp 14 + import gleamy_lights/console 15 + import lumina/shared/shared_fepage_com.{ 16 + type FEPageServeResponse, FEPageServeResponse, 17 + } 18 + import plinth/browser/document 19 + import plinth/browser/element 20 + import plinth/javascript/global 21 + 22 + pub fn fetch() { 23 + use presp <- subpages.fetch("editor", _) 24 + case presp { 25 + Ok(resp) -> { 26 + // console.log("Page: " <> premixed.text_lightblue(string.inspect(resp))) 27 + let resp: FEPageServeResponse = resp 28 + let message_list = resp.message 29 + case 30 + bool.and( 31 + message_list |> list.contains(1) |> bool.negate, 32 + message_list |> list.contains(2) |> bool.negate, 33 + ) 34 + { 35 + True -> { 36 + { 37 + let assert Ok(a) = document.query_selector("div#posteditor") 38 + a 39 + } 40 + |> element.set_inner_html(resp.main) 41 + element_actions.go_back() 42 + Nil 43 + } 44 + False -> { 45 + { 46 + let assert Ok(a) = document.query_selector("div#posteditor") 47 + a 48 + } 49 + |> element.set_inner_html( 50 + "<p class=\"w-full h-full text-black bg-white dark:text-white dark:bg-black\">Failed to load post editor.</p>", 51 + ) 52 + Nil 53 + } 54 + } 55 + document.query_selector("button#bttn_closeeditor") 56 + |> unwrap 57 + |> element.add_event_listener("click", fn(_) { fold() }) 58 + document.query_selector("main") 59 + |> unwrap 60 + |> element.add_event_listener("click", fn(_) { fold() }) 61 + document.query_selector("nav#editormodepicker [data-mode-opener='short']") 62 + |> unwrap 63 + |> switch_editor_mode(render_markdown_short, render_markdown_long) 64 + document.query_selector_all(".editor-switcher") 65 + |> array.to_list 66 + |> list.each(fn(elm) { 67 + element.add_event_listener(elm, "click", fn(_) { 68 + switch_editor_mode(elm, render_markdown_short, render_markdown_long) 69 + }) 70 + }) 71 + } 72 + Error(_) -> { 73 + Nil 74 + } 75 + } 76 + } 77 + 78 + pub fn fold() { 79 + let assert Ok(posteditor) = document.query_selector("div#posteditor") 80 + posteditor |> element_actions.hide_element() 81 + case document.body() |> element.dataset_get("editorOpen") { 82 + Ok(_) -> 83 + document.body() |> element.set_attribute("data-editor-open", "false") 84 + Error(_) -> 85 + document.body() |> element.set_attribute("data-editor-open", "initial") 86 + } 87 + } 88 + 89 + pub fn unfold() { 90 + let assert Ok(mobiletimelineswitcher) = 91 + document.query_selector("#mobiletimelineswitcher") 92 + let assert Ok(posteditor) = document.query_selector("div#posteditor") 93 + mobiletimelineswitcher |> element_actions.hide_element() 94 + posteditor |> element_actions.show_element() 95 + case document.body() |> element.dataset_get("editorOpen") { 96 + Ok("initial") -> { 97 + fetch() 98 + Nil 99 + } 100 + _ -> Nil 101 + } 102 + global.set_timeout(100, fn() { post_fold_out() }) 103 + Nil 104 + } 105 + 106 + @external(javascript, "../../../editor_ffi.mjs", "postfoldout") 107 + fn post_fold_out() -> nil 108 + 109 + pub fn trigger() { 110 + let hash = element_actions.get_window_location_hash() 111 + 112 + case document.body() |> element.dataset_get("editorOpen") { 113 + Ok("true") -> { 114 + console.info( 115 + "triggerEditor: got called, but editor is already open. Refolding editor instead.", 116 + ) 117 + fold() 118 + } 119 + _ -> { 120 + case hash == "editor" { 121 + True -> { 122 + // Editor glitched out, going back to retry... 123 + console.log("triggerEditor: retrying...") 124 + element_actions.go_back() 125 + global.set_timeout(600, fn() { 126 + element_actions.set_window_location_hash("editor") 127 + Nil 128 + }) 129 + Nil 130 + } 131 + False -> { 132 + element_actions.set_window_location_hash("editor") 133 + } 134 + } 135 + } 136 + } 137 + } 138 + 139 + /// Switches the editor mode between short and full. 140 + /// 141 + /// Is not implemented in Gleam yet. 142 + /// Requires some functions that have been implemented in Gleam already as params. 143 + @external(javascript, "../../../editor_ffi.mjs", "switcheditormode") 144 + fn switch_editor_mode( 145 + elm: element.Element, 146 + render_markdown_short: fn() -> Nil, 147 + render_markdown_long: fn() -> Nil, 148 + ) -> Nil 149 + 150 + fn render_markdown_short() { 151 + let editor_short_preview = 152 + document.get_element_by_id("editor-short-preview") |> unwrap 153 + document.get_element_by_id("editor-short-input") 154 + |> unwrap 155 + |> element_actions.get_value 156 + |> regexp.replace( 157 + each: regexp.from_string("/\\*\\*(.*?)\\*\\*/g") |> unwrap, 158 + in: _, 159 + with: "<b>$1</b>", 160 + ) 161 + |> regexp.replace( 162 + regexp.from_string("/\\*(.*?)\\*/g") |> unwrap, 163 + _, 164 + "<i>$1</i>", 165 + ) 166 + |> regexp.replace(regexp.from_string("/_(.*?)_/g") |> unwrap, _, "<i>$1</i>") 167 + |> regexp.replace( 168 + regexp.from_string("/~(.*?)~/g") |> unwrap, 169 + _, 170 + "<del>$1</del>", 171 + ) 172 + |> regexp.replace( 173 + regexp.from_string("/\\^(.*?)\\^/g") |> unwrap, 174 + _, 175 + "<sup>$1</sup>", 176 + ) 177 + |> regexp.replace( 178 + regexp.from_string("/`(.*?)`/g") |> unwrap, 179 + _, 180 + "<code class=\"text-blue-500 bg-slate-200 dark:text-blue-200 dark:bg-slate-600 m-1\">$1</code>", 181 + ) 182 + |> element.set_inner_html(editor_short_preview, _) 183 + } 184 + 185 + fn render_markdown_long() { 186 + let editor_long_input = 187 + document.get_element_by_id("editor-long-input") |> unwrap 188 + 189 + case editor_long_input |> element_actions.get_value { 190 + "" -> { 191 + Nil 192 + } 193 + _ -> { 194 + element_actions.phone_home() 195 + |> request.set_method(Post) 196 + |> request.set_path("/api/fe/editor_fetch_markdownpreview") 197 + |> request.set_body( 198 + json.object([ 199 + #("a", json.string(editor_long_input |> element_actions.get_value)), 200 + ]) 201 + |> json.to_string, 202 + ) 203 + |> request.set_header("Content-Type", "application/json") 204 + |> fetch.send() 205 + |> promise.try_await(fetch.read_json_body) 206 + |> promise.await(fn(resp) { 207 + let assert Ok(resp) = resp 208 + let w = 209 + decode.run(resp.body, json_long_markdown_preview_response_decoder()) 210 + case w { 211 + Ok(JsonLongMarkdownPreviewResponse(True, answer)) -> { 212 + { 213 + let assert Ok(a) = 214 + document.query_selector("div#editor-long-preview") 215 + a 216 + } 217 + |> element.set_inner_html(answer) 218 + Nil 219 + } 220 + _ -> { 221 + { 222 + let assert Ok(a) = 223 + document.query_selector("div#editor-long-preview") 224 + a 225 + } 226 + |> element.set_inner_html( 227 + "<p class=\"w-full h-full text-black bg-white dark:text-white dark:bg-black\">Failed to render markdown.</p>", 228 + ) 229 + Nil 230 + } 231 + } 232 + promise.resolve(Nil) 233 + }) 234 + Nil 235 + } 236 + } 237 + } 238 + 239 + type JsonLongMarkdownPreviewResponse { 240 + JsonLongMarkdownPreviewResponse(ok: Bool, html_content: String) 241 + } 242 + 243 + fn json_long_markdown_preview_response_decoder() -> decode.Decoder( 244 + JsonLongMarkdownPreviewResponse, 245 + ) { 246 + use ok <- decode.field("Ok", decode.bool) 247 + use html_content <- decode.field("htmlContent", decode.string) 248 + decode.success(JsonLongMarkdownPreviewResponse(ok:, html_content:)) 249 + }
+49
frontend/src/frontend/page/site/subpages.gleam
··· 1 + import gleam/dynamic 2 + import gleam/fetch 3 + import gleam/http 4 + import gleam/http/request 5 + import gleam/http/response 6 + import gleam/javascript/promise 7 + import gleam/json 8 + import lumina/shared/shared_fepage_com.{ 9 + type FEPageServeResponse, FEPageServeResponse, 10 + } 11 + import plinth/browser/window 12 + 13 + pub fn fetch(page: String, then: fn(Result(FEPageServeResponse, Nil)) -> Nil) { 14 + { 15 + let req = 16 + { 17 + let assert Ok(a) = request.to(window.origin() <> "/api/fe/fetch-page") 18 + a 19 + } 20 + |> request.set_body("{\"location\": \"" <> page <> "\"}") 21 + |> request.set_header("Content-Type", "application/json") 22 + |> request.set_method(http.Post) 23 + use resp <- promise.try_await(fetch.send(req)) 24 + use resp <- promise.try_await(fetch.read_text_body(resp)) 25 + promise.resolve(Ok(resp)) 26 + } 27 + |> promise.await(fn(a: Result(response.Response(String), fetch.FetchError)) { 28 + case a { 29 + Ok(b) -> { 30 + case 31 + json.decode( 32 + from: b.body, 33 + using: dynamic.decode3( 34 + FEPageServeResponse, 35 + dynamic.field("main", dynamic.string), 36 + dynamic.field("side", dynamic.string), 37 + dynamic.field("message", dynamic.list(dynamic.int)), 38 + ), 39 + ) 40 + { 41 + Ok(c) -> then(Ok(c)) 42 + Error(_) -> then(Error(Nil)) 43 + } 44 + } 45 + Error(_) -> then(Error(Nil)) 46 + } 47 + promise.resolve(Nil) 48 + }) 49 + }