My personal website, in gleam+lustre!
0
fork

Configure Feed

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

New site is LIVE

+1052 -105
+1 -1
gleam.toml
··· 25 25 links = [ 26 26 { rel = "preconnect", href = "https://fontlay.com", crossorigin = "" }, 27 27 { rel = "shortcut icon", href = "/strawmelonjuice.png", type = "image/x-icon" }, 28 - { rel = "stylesheet", href = "" }, 28 + { rel = "me", href = "https://pony.social/@strawmelonjuice" }, 29 29 ] 30 30 stylesheets = [ 31 31 { href = "/styles.css" },
+4
justfile
··· 34 34 [private] 35 35 lustre_dev_start: 36 36 gleam run -m lustre/dev start 37 + 38 + # Builds and pushes to the server. Make sure to have access to strawmelonservices ;) 39 + push: build 40 + rsync -avz --delete ./dist/ strawmelonservices:/root/docker/strawmelonjuice-site/
+24
license.txt
··· 1 + The content in this repository and on this site it mine, but can be freely shared with attribution in the form of a link to the original website. 2 + The code however, is licensed under the MIT license, of which the full text is included below. 3 + 4 + MIT License 5 + 6 + Copyright (c) 2026 M.L.C 'strawmelonjuice' Bloeiman 7 + 8 + Permission is hereby granted, free of charge, to any person obtaining a copy 9 + of this software and associated documentation files (the "Software"), to deal 10 + in the Software without restriction, including without limitation the rights 11 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 + copies of the Software, and to permit persons to whom the Software is 13 + furnished to do so, subject to the following conditions: 14 + 15 + The above copyright notice and this permission notice shall be included in all 16 + copies or substantial portions of the Software. 17 + 18 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 + SOFTWARE.
+71 -1
site.css
··· 54 54 @apply w-screen h-screen; 55 55 } 56 56 57 - footer { 57 + /* Make sure the 'go comment' button is visible even on small screens. */ 58 + .widget .form-controls { 59 + flex-wrap: wrap; 60 + } 61 + 62 + /* As to not collide with chilp's footers, we had to be a bit more specific */ 63 + #app .footer { 58 64 position: fixed; 59 65 bottom: 0; 60 66 -webkit-animation: seconds 1.0s forwards; ··· 85 91 opacity: 0; 86 92 left: -9999px; 87 93 } 94 + } 95 + 96 + 97 + .link-list-container { 98 + margin: auto; 99 + } 100 + 101 + .link-list-container .link-list-button { 102 + display: block; 103 + background: #ff6f91; 104 + color: white; 105 + text-decoration: none; 106 + padding: 1em; 107 + margin: 1em 0; 108 + border-radius: 8px; 109 + font-weight: bold; 110 + transition: background 0.3s ease; 111 + } 112 + 113 + .link-list-container .link-list-button:hover { 114 + background: #ff4f78; 115 + } 116 + 117 + .link-list-container .note { 118 + font-size: 0.9em; 119 + margin-top: 1em; 120 + } 121 + 122 + .link-list-container .submenu { 123 + border-radius: 10px; 124 + padding: 1em; 125 + margin-top: 2em; 126 + } 127 + 128 + .link-list-container .submenu .submenu-h3 { 129 + margin-top: 0; 130 + font-size: 1.2em; 131 + } 132 + 133 + .link-list-container .submenu p { 134 + font-size: 0.95em; 135 + margin-bottom: 1em; 136 + } 137 + 138 + .link-list-container .button-row { 139 + display: flex; 140 + justify-content: center; 141 + gap: 1em; 142 + flex-wrap: wrap; 143 + } 144 + 145 + .link-list-container .button-row a { 146 + flex: 1 1 40%; 147 + background: #ff6f91; 148 + color: white; 149 + text-decoration: none; 150 + padding: 0.8em; 151 + border-radius: 8px; 152 + font-weight: bold; 153 + transition: background 0.3s ease; 154 + } 155 + 156 + .link-list-container .button-row a:hover { 157 + background: #ff4f78; 88 158 } 89 159 }
+914 -90
src/homepage.gleam
··· 1 + import gleam/function 2 + import gleam/string 3 + 1 4 // Post data 2 5 3 6 fn posts() { 4 7 [ 5 8 Post( 6 9 id: 0, 7 - title: "Test", 8 - summary: "This is a test post to check that everything is working.", 10 + title: "New site, who dis?", 11 + summary: "I rewrote this site from scratch!", 9 12 published: "2026-03-11", 10 - revised: Some("Shortly after"), 11 - body: File(Djot, "./written-contents/test.dj"), 13 + revised: None, 14 + body: File(Djot, "./written-contents/blog/personal/2026/new-site.dj"), 12 15 aliases: ["first"], 13 - comments: MastodonStatusLink("pony.social", "115911235653686237"), 14 - category: "Testing", 16 + comments: MastodonStatusLink("pony.social", "116213072855558563"), 17 + category: "Site meta", 15 18 tags: [ 16 - "test", 17 - "hello world", 18 19 "gleam", 20 + "lustre", 19 21 "djot", 20 - "markdown", 21 22 "blogging", 22 - "lifecycle", 23 - "post management", 24 - "content management system", 25 23 "website development", 26 24 "web development", 27 25 "programming", 28 26 "coding", 29 27 "software engineering", 30 28 "technology", 29 + "meta", 31 30 "personal blog", 32 - "blog-personal", 31 + ".blog-personal", 33 32 ], 34 33 ), 35 34 ] 36 35 } 37 36 37 + const personal_start_page_links: List(#(String, String)) = [ 38 + #("🔍 DuckDuckGo NoAI", "https://noai.duckduckgo.com/"), 39 + #("🦄 Fontys Portal", "https://portal.fontysict.nl/SitePages/Home.aspx"), 40 + #( 41 + "🖧 Strawmelonjuice Forge", 42 + "https://forge.strawmelonjuice.com/strawmelonjuice?tab=repositories", 43 + ), 44 + #("🖼️ StrawmediaJuice", "https://media.strawmelonjuice.com/"), 45 + #("🖧 Codeberg", "https://codeberg.org/strawmelonjuice"), 46 + #( 47 + "❄️ NixPkgs Search", 48 + "https://search.nixos.org/packages?channel=25.11&query=", 49 + ), 50 + #("📼 FastDL", "https://fastdl.app"), 51 + ] 52 + 38 53 const highlighted_posts = [0] 39 54 55 + const badges = [ 56 + Badge( 57 + img_url: "/img/badges/feminism.gif", 58 + img_alt: "feminism", 59 + img_title: "intersectional feminism!", 60 + clickable_url: "", 61 + text_badge: "", 62 + ), 63 + // No longer true! 64 + // Badge( 65 + // img_url: "/img/badges/blinkiesCafe-xD.gif", 66 + // img_alt: "This page works better on a computer than on a smartphone :3", 67 + // img_title: "This page works better on a computer than on a smartphone :3", 68 + // clickable_url: "/img/badges/blinkiesCafe-xD.gif", 69 + // text_badge: "", 70 + // ), 71 + // I mean, I do have a tumblr, but I use other platforms more nowadays. 72 + // Badge( 73 + // img_url: "/img/badges/blinkiesCafe-tumblr-grrll.gif", 74 + // img_alt: "Tumblr", 75 + // img_title: "Tumblr Tumblr Tumblr", 76 + // clickable_url: "https://www.tumblr.com/strawmelonjuice", 77 + // text_badge: "", 78 + // ), 79 + Badge( 80 + img_url: "/img/badges/e29.jpg", 81 + img_alt: "The Sims addict", 82 + img_title: "The Sims addict", 83 + clickable_url: "", 84 + text_badge: "", 85 + ), 86 + Badge( 87 + img_url: "https://gleam.run/images/lucy/lucy.svg", 88 + img_alt: "", 89 + img_title: "Written in Gleam", 90 + clickable_url: "https://gleam.run/", 91 + text_badge: "Made with Gleam", 92 + ), 93 + Badge( 94 + img_url: "https://yesterweb.org/no-to-web3/img/roly-saynotoweb3.gif", 95 + img_alt: "Keep the web free, say no to web3", 96 + img_title: "Crypto's ewie", 97 + clickable_url: "https://yesterweb.org/no-to-web3/", 98 + text_badge: "", 99 + ), 100 + Badge( 101 + img_url: "/img/badges/blinkiesCafe-autism.gif", 102 + img_alt: "autism", 103 + img_title: "brain go brrr", 104 + clickable_url: "self", 105 + text_badge: "", 106 + ), 107 + Badge( 108 + img_url: "/img/badges/getfirefox.gif", 109 + img_alt: "Get Firefox", 110 + img_title: "GET FIREFOX", 111 + clickable_url: "https://www.mozilla.org/nl/firefox/new/", 112 + text_badge: "", 113 + ), 114 + Badge( 115 + img_url: "/img/badges/blinkiesCafe-L1.gif", 116 + img_alt: "", 117 + img_title: "I GOT AUTISM", 118 + clickable_url: "self", 119 + text_badge: "", 120 + ), 121 + Badge( 122 + img_url: "/img/badges/minecraft.gif", 123 + img_alt: "minecraft", 124 + img_title: "block game good", 125 + clickable_url: "https://polymc.org/", 126 + text_badge: "", 127 + ), 128 + Badge( 129 + img_url: "/img/badges/nerd.gif", 130 + img_alt: "nerd", 131 + img_title: "nerd", 132 + clickable_url: "", 133 + text_badge: "", 134 + ), 135 + Badge( 136 + img_url: "/img/badges/fucknazis.gif", 137 + img_alt: "Fuck nazis", 138 + img_title: "FUCK NAZIS", 139 + clickable_url: "", 140 + text_badge: "", 141 + ), 142 + Badge( 143 + img_url: "/img/badges/antinft.gif", 144 + img_alt: "", 145 + img_title: "NFTs are thrash, and a perfect way to spend money on destroying the world.", 146 + clickable_url: "", 147 + text_badge: "", 148 + ), 149 + Badge( 150 + img_url: "/img/badges/linux80x15.png", 151 + img_alt: "Linux", 152 + img_title: "arch linux btw heheh", 153 + clickable_url: "", 154 + text_badge: "", 155 + ), 156 + Badge( 157 + img_url: "/img/badges/beingaprincessisafulltimejob.gif", 158 + img_alt: "Being a princess is a full time job", 159 + img_title: "Being a princess is a full time job </3", 160 + clickable_url: "", 161 + text_badge: "", 162 + ), 163 + Badge( 164 + img_url: "/img/badges/y2k-compliant.gif", 165 + img_alt: "Y2K-Compliant", 166 + img_title: "We survived! We all did!", 167 + clickable_url: "", 168 + text_badge: "", 169 + ), 170 + Badge( 171 + img_url: "/img/badges/transles80x31.png", 172 + img_alt: "trans-lesbian flag", 173 + img_title: "I'm trans, and... girls... are... awesome!", 174 + clickable_url: "", 175 + text_badge: "", 176 + ), 177 + Badge( 178 + img_url: "/img/badges/nixos.png", 179 + img_alt: "This user uses NixOS", 180 + img_title: "NixOS :3", 181 + clickable_url: "https://forge.strawmelonjuice.com/strawmelonjuice/nix", 182 + text_badge: "", 183 + ), 184 + Badge( 185 + img_url: "https://blinkies.cafe/b/display/blinkiesCafe-badge.gif", 186 + img_alt: "blinkies.cafe", 187 + img_title: "blinkies.cafe | make your own blinkies!", 188 + clickable_url: "https://blinkies.cafe", 189 + text_badge: "", 190 + ), 191 + Badge( 192 + img_url: "/img/badges/0235-rainbowstar.gif", 193 + img_alt: "Silly rainbow star", 194 + img_title: "SILLY!", 195 + clickable_url: "", 196 + text_badge: "", 197 + ), 198 + ] 199 + 40 200 // The site itself 41 201 42 202 import chilp ··· 68 228 69 229 // Some types for your consideration 70 230 231 + type Badge { 232 + Badge( 233 + clickable_url: String, 234 + img_url: String, 235 + img_alt: String, 236 + img_title: String, 237 + text_badge: String, 238 + ) 239 + } 240 + 71 241 type Model { 72 242 Model( 73 243 posts: Dict(Int, NormalizedPost), ··· 137 307 AllAndEverything 138 308 Category(String) 139 309 Tagged(String) 310 + Links 311 + PersonalStartPage 312 + } 313 + 314 + fn render_badge(badge: Badge) -> Element(msg) { 315 + let img = 316 + html.img([ 317 + attribute.src(badge.img_url), 318 + attribute.alt(badge.img_alt), 319 + attribute.title(badge.img_title), 320 + attribute.class("badge badge-dash badge-primary lg rounded-none m-1 p-0"), 321 + ]) 322 + let img = case badge.text_badge { 323 + "" -> img 324 + text -> 325 + html.div( 326 + [ 327 + attribute.class( 328 + "badge badge-dash badge-primary text-primary-content bg-primary lg rounded-none m-1 p-2 text-sm", 329 + ), 330 + ], 331 + [ 332 + html.img([ 333 + attribute.src(badge.img_url), 334 + attribute.alt(badge.img_alt), 335 + attribute.title(badge.img_title), 336 + attribute.class("w-4 h-4"), 337 + ]), 338 + html.span([attribute.class("text-[10px]")], [html.text(text)]), 339 + ], 340 + ) 341 + } 342 + case badge.clickable_url { 343 + "" -> img 344 + "self" -> html.a([attribute.href(badge.img_url)], [img]) 345 + url -> html.a([attribute.href(url)], [img]) 346 + } 140 347 } 141 348 142 349 fn post_normalize(takes: Post) -> NormalizedPost { ··· 145 352 title:, 146 353 summary:, 147 354 body: _, 148 - aliases: _, 355 + aliases:, 149 356 comments:, 150 357 published:, 151 358 revised:, 152 359 category:, 153 360 tags:, 154 361 ) = takes 362 + list.each(aliases, fn(alias) { 363 + let message = 364 + "Alias '" 365 + <> alias 366 + <> "' for post " 367 + <> int.to_string(id) 368 + <> " is invalid." 369 + case string.contains(alias, " ") { 370 + True -> panic as message 371 + False -> Nil 372 + } 373 + case string.contains(alias, "/") { 374 + True -> panic as message 375 + False -> Nil 376 + } 377 + }) 378 + let category = category |> string.replace(" ", "_") 379 + let tags = list.map(tags, fn(tag) { string.replace(tag, " ", "_") }) 155 380 NormalizedPost( 156 381 id:, 157 382 title:, ··· 203 428 ) 204 429 205 430 ["me"] -> Me 431 + ["me", "links"] -> Links 206 432 ["me", "portfolio"] -> Portfolio 433 + 434 + ["startpage"] -> PersonalStartPage 207 435 208 436 ["everything"] -> AllAndEverything 209 437 ··· 216 444 Index -> "/" 217 445 Me -> "/me" 218 446 Posts -> "/posts" 219 - PostById(post_id) -> "/post/" <> int.to_string(post_id) 447 + PostById(post_id) -> { 448 + let slug = { 449 + let m = list.find(posts(), fn(p) { p.id == post_id }) 450 + case m { 451 + Error(Nil) -> int.to_string(post_id) 452 + Ok(p) -> { 453 + case p.aliases { 454 + [] -> int.to_string(post_id) 455 + [alias, ..] -> alias 456 + } 457 + } 458 + } 459 + } 460 + "/post/" <> slug 461 + } 220 462 Portfolio -> "/me/portfolio" 463 + Links -> "/me/links" 221 464 AllAndEverything -> "/everything" 465 + PersonalStartPage -> "/startpage" 222 466 NotFound(_) -> "/404" 223 467 Tagged(v) -> "/posts/tagged/" <> v 224 468 Category(c) -> "/posts/category/" <> c ··· 264 508 } 265 509 266 510 fn view(model: Model) -> Element(Msg) { 511 + let main = fn(content: List(Element(Msg)), alter: fn(String) -> String) -> Element( 512 + Msg, 513 + ) { 514 + html.main( 515 + [ 516 + attribute.class( 517 + "mx-auto my-0 max-w-2xl bg-primary bg-opacity-15 text-primary-content md:rounded-md md:mt-14 p-5 md:p-12 md:h-fit" 518 + |> alter, 519 + ), 520 + ], 521 + content, 522 + ) 523 + } 524 + let into_altered_main = fn( 525 + content: List(Element(Msg)), 526 + alter: fn(String) -> String, 527 + ) -> Element(Msg) { 528 + [ 529 + content |> main(alter), 530 + html.hr([attribute.class("my-20 text-base-100")]), 531 + ] 532 + |> element.fragment 533 + } 534 + let into_main = fn(content: List(Element(Msg))) -> Element(Msg) { 535 + [ 536 + content |> main(function.identity), 537 + html.hr([attribute.class("my-20 text-base-100")]), 538 + ] 539 + |> element.fragment 540 + } 541 + let into_main_with_badges = fn(content: List(Element(Msg))) -> Element(Msg) { 542 + [ 543 + content |> main(function.identity), 544 + html.div( 545 + [ 546 + attribute.class( 547 + "bg-accent mt-4 text-accent-content mx-auto max-w-2xl md:rounded-md md:mb-14 p-2 md:p-12 md:h-fit", 548 + ), 549 + ], 550 + list.map(badges |> list.shuffle(), render_badge), 551 + ), 552 + html.hr([attribute.class("my-20 text-base-100")]), 553 + ] 554 + |> element.fragment 555 + } 556 + 557 + element.fragment([ 558 + header(model), 559 + // Header end 560 + { 561 + case model.route { 562 + Index -> view_index(model) |> into_main_with_badges 563 + Posts -> view_posts(model, AllPosts) |> into_main 564 + Category(category) -> 565 + view_posts(model, PostsInCategory(category)) |> into_main 566 + Tagged(tag) -> view_posts(model, PostsWithTag(tag)) |> into_main 567 + PostById(post_id) -> 568 + view_post(model, post_id) 569 + |> into_altered_main(string.replace(_, "bg-primary", "bg-[#FAEBEB]")) 570 + Me -> view_me() |> into_main_with_badges 571 + Links -> view_links() |> into_main 572 + Portfolio -> 573 + view_portfolio() 574 + |> into_altered_main(string.replace(_, "max-w-2xl", "max-w-6xl")) 575 + NotFound(_) -> view_not_found() |> into_main_with_badges 576 + AllAndEverything -> view_all_and_everything(model) |> into_main 577 + PersonalStartPage -> view_personal_start_page() |> into_main_with_badges 578 + } 579 + }, 580 + 581 + html.footer( 582 + [ 583 + attribute.class( 584 + "footer sm:footer-horizontal footer-center bg-base-300 text-base-content p-4 text-xs", 585 + ), 586 + ], 587 + [ 588 + html.aside([], [ 589 + html.p([], [ 590 + html.text("This site was made in Gleam!"), 591 + html.a( 592 + [ 593 + attribute.href( 594 + "https://forge.strawmelonjuice.com/strawmelonjuice/homepage", 595 + ), 596 + attribute.target("_blank"), 597 + attribute.class("link link-accent ml-1"), 598 + ], 599 + [html.text("View source.")], 600 + ), 601 + html.text( 602 + " -- code is licensed under MIT License, content is mine, but can be freely shared with attribution in the form of a link to the original website.", 603 + ), 604 + ]), 605 + ]), 606 + ], 607 + ), 608 + ]) 609 + } 610 + 611 + fn header(model: Model) -> Element(Msg) { 267 612 let view_header_link = fn(target: Route, text: String) -> Element(Msg) { 268 613 let current = model.route 269 614 let is_active = case current, target { 270 615 PostById(_), Posts -> True 616 + Tagged(_), Posts -> True 617 + Category(_), Posts -> True 618 + Links, Me -> True 619 + // Portfolio, Me -> True 271 620 _, _ -> current == target 272 621 } 273 622 ··· 286 635 [ 287 636 view_header_link(Index, "Home"), 288 637 view_header_link(Portfolio, "Portfolio"), 289 - view_header_link(Me, "Me"), 638 + view_header_link(Me, "About me"), 290 639 view_header_link(Posts, "Posts"), 291 640 ] 292 641 |> element.fragment 293 - element.fragment([ 294 - // Header start 295 - html.nav([attribute.class("navbar bg-secondary shadow-sm")], [ 642 + // Header start 643 + html.nav( 644 + [ 645 + attribute.class( 646 + "navbar bg-secondary shadow-sm border-accent-content border-b-2", 647 + ), 648 + ], 649 + [ 296 650 html.div([attribute.class("navbar-start")], [ 297 651 html.div([attribute.class("dropdown md:hidden")], [ 298 652 html.div( ··· 337 691 html.ul([attribute.class("menu menu-horizontal px-1 hidden md:flex")], [ 338 692 header_links, 339 693 ]), 340 - // Search 694 + // Search 341 695 html.a([href(AllAndEverything)], [ 342 696 html.button( 343 697 [ ··· 379 733 ]), 380 734 html.div([attribute.class("navbar-end")], [ 381 735 socials(), 736 + html.div([attribute.class("mx-2")], []), 737 + html.a([href(Links), attribute.class("hidden md:block")], [ 738 + html.button( 739 + [ 740 + attribute.class("btn btn-ghost btn-circle"), 741 + ], 742 + [ 743 + svg.svg( 744 + [ 745 + attribute("xmlns", "http://www.w3.org/2000/svg"), 746 + attribute("viewBox", "0 0 15.36 15.36"), 747 + attribute("version", "1.1"), 748 + attribute("fill", "currentColor"), 749 + attribute("height", "24"), 750 + attribute("width", "24"), 751 + ], 752 + [ 753 + svg.g( 754 + [ 755 + attribute( 756 + "transform", 757 + "matrix(.03 0 0 .03 -.00016582 -.00038274)", 758 + ), 759 + attribute.id("_40_Hyperlink"), 760 + ], 761 + [ 762 + svg.g([attribute("fill-rule", "evenodd")], [ 763 + svg.path([ 764 + attribute("data-name", "Path 51"), 765 + attribute( 766 + "d", 767 + "m503.47 132.27-123.73-123.73a29.17 29.17 0 0 0-41.234 0l-123.73 123.73a29.15 29.15 0 0 0 0 41.265l20.6 20.6 123.73-123.73 82.5 82.5-123.73 123.73 20.633 20.632a29.169 29.169 0 0 0 41.234 0l123.73-123.73a29.207 29.207 0 0 0 1e-3 -41.265z", 768 + ), 769 + ]), 770 + svg.path([ 771 + attribute("data-name", "Path 52"), 772 + attribute( 773 + "d", 774 + "m276.63 317.87-123.73 123.73-82.5-82.5 123.73-123.73-20.633-20.632a29.167 29.167 0 0 0-41.233 0l-123.73 123.73a29.208 29.208 0 0 0 0 41.265l123.73 123.73a29.169 29.169 0 0 0 41.233 0l123.73-123.73a29.152 29.152 0 0 0 0-41.265z", 775 + ), 776 + ]), 777 + svg.path([ 778 + attribute("data-name", "Path 53"), 779 + attribute( 780 + "d", 781 + "m173.5 338.47a29.153 29.153 0 0 0 41.266 0l123.73-123.73a29.157 29.157 0 0 0-41.265-41.2l-123.74 123.73a29.133 29.133 0 0 0 0 41.203z", 782 + ), 783 + ]), 784 + ]), 785 + ], 786 + ), 787 + ], 788 + ), 789 + ], 790 + ), 791 + ]), 382 792 ]), 793 + ], 794 + ) 795 + } 796 + 797 + fn view_personal_start_page() -> List(Element(Msg)) { 798 + let links = 799 + list.map(personal_start_page_links, fn(item) { 800 + let name = item.0 801 + let url = item.1 802 + html.a([attribute.class("btn btn-block my-1"), attribute.href(url)], [ 803 + element.text(name), 804 + ]) 805 + }) 806 + 807 + [ 808 + html.p([attribute.class("mb-6 text-xs")], [ 809 + element.text( 810 + "This start page is mostly for my own use, but feel free to use it as well! I just wanted a simple page with links to my most used sites. ", 811 + ), 812 + link(Index, "Go back home!"), 813 + element.text(" :)"), 383 814 ]), 384 - // Header end 385 - html.main( 386 - [ 387 - attribute.class( 388 - "mx-auto max-w-2xl bg-primary bg-opacity-15 text-primary-content md:rounded-md md:my-14 p-5 md:p-12 md:h-fit mb-18", 815 + ..links 816 + ] 817 + } 818 + 819 + fn view_portfolio() -> List(Element(Msg)) { 820 + [ 821 + html.div([attribute.class("p-8 max-w-6xl mx-auto")], [ 822 + html.h1([attribute.class("text-4xl font-bold mb-8")], [ 823 + html.text("My Work"), 824 + ]), 825 + html.h2( 826 + [ 827 + attribute.class( 828 + "text-2xl font-semibold mb-6 border-b border-base-300 pb-2", 829 + ), 830 + ], 831 + [html.text("Active Projects")], 832 + ), 833 + html.div( 834 + [ 835 + attribute.class("grid grid-cols-1 md:grid-cols-3 gap-3 mb-12 w-full"), 836 + ], 837 + [ 838 + view_portfolio_card_active( 839 + id: "rac", 840 + tech: ["Gleam", "Lustre", "Tauri", "Rust", "School"], 841 + name: "Ranger Among Critters", 842 + description: "An old-school-pokemon-inspired game, I picked up again as a school project. Made with Gleam, Lustre and Tauri.", 843 + wip: True, 844 + more: None, 845 + links: [ 846 + #( 847 + "Forgejo", 848 + "https://forge.strawmelonjuice.com/mlcbloeiman-school/HBO-ICT_startsemester_ranger-among-critters", 849 + ), 850 + ], 851 + ), 852 + view_portfolio_card_active( 853 + id: "sweetnhouse", 854 + tech: ["Gleam", "Lustre"], 855 + name: "Sweet n House", 856 + description: "Household planning and management software for neurodivergent households.", 857 + wip: True, 858 + more: None, 859 + links: [ 860 + #( 861 + "Forgejo", 862 + "https://forge.strawmelonjuice.com/strawmelonjuice/SweetnHouse", 863 + ), 864 + ], 865 + ), 866 + view_portfolio_card_active( 867 + id: "luminaproject", 868 + tech: ["Rust", "Gleam"], 869 + name: "Lumina", 870 + description: "Federated social media focused on media sharing and \"bubbles.\"", 871 + more: Some([ 872 + html.p([attribute.class("mt-6")], [ 873 + html.text( 874 + "One of my oldest concepts still alive. A social media-ish project, obviously slightly overambitious :)", 875 + ), 876 + html.br([]), 877 + html.text( 878 + "Lumina is a federated social media platform, similar to maybe Mastodon, but with a focus on media sharing and communities (\"bubbles\", similar to Facebook groups but without the administration that comes with them). And with a different, maybe more old-school, approach to federation. The project is still in its early stages, but I do work on it whenever I can.", 879 + ), 880 + ]), 881 + html.p([attribute.class("mt-6")], [ 882 + html.text( 883 + "Lumina is also referred to as \"Lumina/Peonies\", this is because Peonies will possibly be the official instance of Lumina, once it's ready.", 884 + ), 885 + ]), 886 + html.p([attribute.class("mt-6")], [ 887 + html.a( 888 + [ 889 + attribute.href( 890 + "https://forge.strawmelonjuice.com/strawmelonjuice/Lumina/src/branch/development/notes", 891 + ), 892 + attribute.class("link-info-content underline"), 893 + ], 894 + [ 895 + html.text("Read Lumina's dev notes here"), 896 + ], 897 + ), 898 + ]), 899 + ]), 900 + links: [ 901 + #( 902 + "Forgejo", 903 + "https://forge.strawmelonjuice.com/strawmelonjuice/Lumina", 904 + ), 905 + ], 906 + wip: True, 907 + ), 908 + 909 + view_portfolio_card_active( 910 + id: "at-zero", 911 + tech: ["Python", "Rust", "AT Protocol", "ZeroNet"], 912 + name: "AT-Zero", 913 + description: "Using the AT Protocol on top of ZeroNet to combine easy access with true decentralization. A long shot.", 914 + more: None, 915 + links: [ 916 + #("Tangled", "https://tangled.org/strawmelonjuice.com/AT-Zero"), 917 + ], 918 + wip: True, 919 + ), 920 + view_portfolio_card_active( 921 + description: "The very website you're on right now. Made with Gleam and Lustre, and a lot of love.", 922 + links: [ 923 + #("Homepage", "/"), 924 + #( 925 + "Forgejo", 926 + "https://forge.strawmelonjuice.com/strawmelonjuice/homepage", 927 + ), 928 + ], 929 + id: "homepage", 930 + tech: ["Gleam", "Lustre"], 931 + name: "This website!", 932 + more: None, 933 + wip: True, 934 + ), 935 + ] 936 + |> list.shuffle(), 937 + ), 938 + html.h2( 939 + [ 940 + attribute.class( 941 + "text-2xl font-semibold mb-6 border-b border-base-300 pb-2", 942 + ), 943 + ], 944 + [html.text("2025: Academic & Professional")], 945 + ), 946 + html.div([attribute.class("grid grid-cols-1 md:grid-cols-2 gap-6")], [ 947 + view_portfolio_card_archive( 948 + title: "Internship @ ASML", 949 + description: "Modernizing and integrating TestLink (PHP) into the workflow of a global tech leader.", 950 + link: #( 951 + "GitHub (not mine)", 952 + "https://github.com/TestLinkOpenSourceTRMS/testlink-code", 953 + ), 389 954 ), 390 - ], 391 - { 392 - case model.route { 393 - Index -> view_index(model) 394 - Posts -> view_posts(model, AllPosts) 395 - Category(category) -> view_posts(model, PostsInCategory(category)) 396 - Tagged(tag) -> view_posts(model, PostsWithTag(tag)) 397 - PostById(post_id) -> view_post(model, post_id) 398 - Me -> view_about() 399 - Portfolio -> view_portfolio() 400 - NotFound(_) -> view_not_found() 401 - AllAndEverything -> view_all_and_everything(model) 402 - } 403 - }, 404 - ), 405 - html.footer( 955 + view_portfolio_card_archive( 956 + title: "Belscript Stage 🇳🇱", 957 + description: "Hulptool voor studenten bij het bellen naar stageplekken. Voor mij een manier om mijn toenmalige school de Gleam-programmeertaal te tonen.", 958 + link: #( 959 + "Live Demo", 960 + "https://strawmelonjuice.github.io/school_belscript_bellen_stage/", 961 + ), 962 + ), 963 + ]), 964 + ]), 965 + ] 966 + } 967 + 968 + fn view_portfolio_card_archive( 969 + title title: String, 970 + description description: String, 971 + link link: #(String, String), 972 + ) -> Element(Msg) { 973 + html.div([attribute.class("card card-side bg-base-300 shadow-inner")], [ 974 + html.div([attribute.class("card-body")], [ 975 + html.h2([attribute.class("card-title")], [ 976 + html.text(title), 977 + ]), 978 + html.p([attribute.class("text-sm")], [ 979 + html.text(description), 980 + ]), 981 + html.div([attribute.class("card-actions justify-end")], [ 982 + html.a( 983 + [ 984 + attribute.class("btn btn-xs btn-primary"), 985 + attribute.href(link.1), 986 + ], 987 + [html.text(link.0)], 988 + ), 989 + ]), 990 + ]), 991 + ]) 992 + } 993 + 994 + fn view_portfolio_card_active( 995 + id id: String, 996 + tech technologies: List(String), 997 + name name: String, 998 + description description: String, 999 + more expanded_description: Option(List(Element(Msg))), 1000 + links links: List(#(String, String)), 1001 + wip in_progress: Bool, 1002 + ) -> Element(Msg) { 1003 + [ 1004 + html.div( 406 1005 [ 407 1006 attribute.class( 408 - "footer sm:footer-horizontal footer-center bg-base-300 text-base-content p-4 text-xs", 1007 + "card-compact bg-base-200 shadow-xl border border-base-300 hover:border-primary transition-colors", 409 1008 ), 410 1009 ], 411 1010 [ 412 - html.aside([], [ 413 - html.p([], [ 414 - html.text("This site was made in Gleam!"), 415 - html.a( 416 - [ 417 - attribute.href( 418 - "https://forge.strawmelonjuice.com/strawmelonjuice/homepage", 419 - ), 420 - attribute.target("_blank"), 421 - attribute.class("link link-accent ml-1"), 422 - ], 423 - [html.text("View source.")], 424 - ), 425 - html.text( 426 - " -- code is licensed under MIT License, content is mine, but can be freely shared with attribution in the form of a link to the original website.", 427 - ), 1011 + html.div([attribute.class("card-body h-full")], [ 1012 + html.div([attribute.class("flex justify-end w-full flex-wrap")], [ 1013 + list.map(technologies, fn(tech) { 1014 + html.a( 1015 + [ 1016 + href(Tagged(tech)), 1017 + attribute.class( 1018 + "badge badge-accent uppercase text-xs font-bold m-1", 1019 + ), 1020 + ], 1021 + [html.text(tech)], 1022 + ) 1023 + }) 1024 + |> element.fragment, 1025 + ]), 1026 + html.h2([attribute.class("card-title text-secondary-content")], [ 1027 + html.text(name), 1028 + ]), 1029 + html.p([attribute.class("text-sm opacity-80")], [ 1030 + html.text(description), 1031 + ]), 1032 + html.div( 1033 + [ 1034 + attribute.class("flex flex-wrap gap-2 mt-2"), 1035 + attribute.classes([#("hidden", !in_progress)]), 1036 + ], 1037 + [ 1038 + html.div([attribute.class("badge badge-ghost badge-sm italic")], [ 1039 + html.text("In-Progress"), 1040 + ]), 1041 + ], 1042 + ), 1043 + html.div([attribute.class("card-actions justify-end mt-4")], [ 1044 + case expanded_description { 1045 + Some(_) -> 1046 + html.button( 1047 + [ 1048 + attribute("onclick", id <> ".showModal()"), 1049 + attribute.class("btn btn-sm btn-ghost underline"), 1050 + ], 1051 + [html.text("Read More")], 1052 + ) 1053 + 1054 + _ -> element.none() 1055 + }, 1056 + list.map(links, fn(link) { 1057 + html.a( 1058 + [ 1059 + attribute.class("btn btn-sm btn-outline"), 1060 + attribute.href(link.1), 1061 + ], 1062 + [html.text(link.0)], 1063 + ) 1064 + }) 1065 + |> element.fragment, 428 1066 ]), 429 1067 ]), 430 1068 ], 431 1069 ), 432 - ]) 433 - } 434 - 435 - fn view_portfolio() -> List(Element(Msg)) { 436 - [element.text("wawawawawawa (portfolio coming soon)")] 1070 + case expanded_description { 1071 + None -> element.none() 1072 + Some(desc) -> 1073 + html.dialog([attribute.class("modal"), attribute.id(id)], [ 1074 + html.div([attribute.class("modal-box")], [ 1075 + desc |> element.fragment, 1076 + html.div([attribute.class("modal-action")], [ 1077 + html.form([attribute.method("dialog")], [ 1078 + html.button([attribute.class("btn")], [html.text("Close")]), 1079 + ]), 1080 + ]), 1081 + ]), 1082 + ]) 1083 + }, 1084 + ] 1085 + |> element.fragment 437 1086 } 438 1087 439 1088 fn view_all_and_everything(model) -> List(Element(Msg)) { ··· 458 1107 "I've not implemented a search function yet, but you can use control+f to search through everything on this page!", 459 1108 ), 460 1109 html.hr([attribute.class("my-8")]), 1110 + wrap(Me, "About Me", view_me()), 461 1111 wrap(Index, "Homepage", view_index(model)), 462 1112 wrap(Posts, "All Posts", view_posts(model, AllPosts)), 463 - wrap(Me, "About Me", view_about()), 1113 + wrap(Links, "Links page", view_links()), 464 1114 wrap(Portfolio, "Portfolio", view_portfolio()), 1115 + // Hmm... 1116 + wrap(PersonalStartPage, "Personal start page", view_personal_start_page()), 465 1117 ] 466 1118 } 467 1119 ··· 817 1469 } 818 1470 819 1471 fn view_posts(model: Model, filtered: PostFilter) -> List(Element(Msg)) { 820 - let posts = 1472 + let postlist = 821 1473 model.posts 822 1474 |> dict.filter(fn(_id, post) { 823 1475 case filtered { 824 1476 AllPosts -> True 825 1477 PostsInCategory(category) -> post.category == category 826 - PostsWithTag(tag) -> list.contains(post.tags, tag) 1478 + PostsWithTag(tag) -> 1479 + list.contains( 1480 + post.tags |> list.map(string.lowercase), 1481 + tag |> string.lowercase, 1482 + ) 827 1483 } 828 1484 }) 829 1485 |> dict.values 830 1486 |> list.sort(fn(a, b) { int.compare(a.id, b.id) }) 831 - |> list.map(view_post_preview) 832 - 1487 + let posts = case postlist { 1488 + [] -> [ 1489 + html.p([attribute.class("mt-6")], [ 1490 + html.text("No posts found :("), 1491 + ]), 1492 + ] 1493 + _ -> 1494 + postlist 1495 + |> list.map(view_post_preview) 1496 + } 833 1497 [ 834 1498 case filtered { 835 1499 AllPosts -> { ··· 837 1501 title("All posts"), 838 1502 html.p([attribute.class("mt-6")], [ 839 1503 element.text("Filter: "), 840 - html.a([href(Tagged("blog-personal"))], [ 1504 + html.a([href(Tagged(".blog-personal"))], [ 841 1505 html.button([attribute.class("btn btn-sm btn-info")], [ 842 1506 element.text(" Blog posts "), 843 1507 ]), ··· 848 1512 attribute("popovertarget", "popover-1"), 849 1513 attribute.class("btn btn-sm btn-info"), 850 1514 ], 851 - [html.text(" Creative ")], 1515 + [html.text(" Creations ")], 852 1516 ), 853 1517 html.ul( 854 1518 [ ··· 861 1525 ], 862 1526 [ 863 1527 html.li([], [ 864 - html.a([href(Tagged("creative"))], [ 1528 + html.a([href(Tagged(".creative"))], [ 865 1529 html.text("Everything"), 866 1530 ]), 867 1531 ]), 868 1532 html.li([], [ 869 - html.a([href(Tagged("creative-art"))], [ 870 - html.text("Creative content"), 1533 + html.a([href(Tagged(".creative-art"))], [ 1534 + html.text("Drawings and scribbles"), 871 1535 ]), 872 1536 ]), 873 1537 html.li([], [ 874 - html.a([href(Tagged("creative-writings"))], [ 1538 + html.a([href(Tagged(".creative-writings"))], [ 875 1539 html.text("Stories"), 876 1540 ]), 877 1541 ]), ··· 883 1547 } 884 1548 PostsInCategory(category) -> { 885 1549 [ 886 - title("Posts in category: " <> category), 1550 + title("Posts in category: " <> category |> string.replace("_", " ")), 887 1551 html.p([], [link(Posts, "<- See all posts")]), 888 1552 ] 889 1553 |> element.fragment 890 1554 } 1555 + PostsWithTag(".creative") -> { 1556 + [ 1557 + title("Creative works"), 1558 + html.p([], [link(Posts, "<- See all posts instead")]), 1559 + ] 1560 + |> element.fragment 1561 + } 1562 + PostsWithTag(".blog-personal") -> { 1563 + [ 1564 + title("Personal blog posts"), 1565 + html.p([], [link(Posts, "<- See all posts instead")]), 1566 + ] 1567 + |> element.fragment 1568 + } 1569 + PostsWithTag(".creative-art") -> { 1570 + [ 1571 + title("Drawings and scribbles"), 1572 + html.p([], [link(Posts, "<- See all posts instead")]), 1573 + ] 1574 + |> element.fragment 1575 + } 1576 + PostsWithTag(".creative-writings") -> { 1577 + [ 1578 + title("Stories"), 1579 + html.p([], [link(Posts, "<- See all posts instead")]), 1580 + ] 1581 + |> element.fragment 1582 + } 891 1583 PostsWithTag(tag) -> 892 1584 [ 893 - title("Posts with tag: " <> tag), 1585 + title("Posts with tag: " <> tag |> string.replace("_", " ")), 894 1586 html.p([], [link(Posts, "<- See all posts instead")]), 895 1587 ] 896 1588 |> element.fragment ··· 916 1608 [ 917 1609 html.text( 918 1610 "Category: " 919 - <> post.category 1611 + <> post.category |> string.replace("_", " ") 920 1612 <> " • Posted: " 921 1613 <> post.published 922 1614 <> case post.revised { 923 - Some(updated) -> " (updated: " <> updated <> ")" 1615 + Some(updated) -> " • Updated: " <> updated 924 1616 None -> "" 925 1617 }, 926 1618 ), ··· 939 1631 case dict.get(model.posts, post_id) { 940 1632 Error(_) -> view_not_found() 941 1633 Ok(post) -> [ 942 - html.article([], [ 1634 + html.article([attribute.class(" bg-[#FAEBEB]")], [ 943 1635 html.p( 944 1636 [ 945 1637 attribute.class( ··· 948 1640 ], 949 1641 [ 950 1642 element.text("Category: "), 951 - link(Category(post.category), post.category), 1643 + link( 1644 + Category(post.category), 1645 + post.category |> string.replace("_", " "), 1646 + ), 952 1647 html.text( 953 1648 " • Posted: " 954 1649 <> post.published ··· 961 1656 ), 962 1657 title(post.title), 963 1658 leading(post.summary), 964 - html.section([attribute.class("p-2 m-1 ")], [post.body]), 1659 + html.section([attribute.class("p-1 m-1")], [post.body]), 965 1660 ]), 966 1661 html.hr([attribute.class("my-8")]), 967 1662 html.p([attribute.class("mt-10")], [ ··· 981 1676 [html.text("Tags: ")] 982 1677 |> list.append( 983 1678 post.tags 1679 + |> list.filter(fn(tag) { !string.starts_with(tag, ".") }) 984 1680 |> list.rest() 985 1681 |> result.unwrap([]) 986 1682 |> list.map(fn(tag) { 987 - [element.text(", "), link(Tagged(tag), tag)] 1683 + [ 1684 + element.text(", "), 1685 + link(Tagged(tag), tag |> string.replace("_", " ")), 1686 + ] 988 1687 |> element.fragment 989 1688 }) 990 1689 |> list.append( 991 1690 [ 992 1691 { 993 - let tag = post.tags |> list.first() |> result.unwrap("") 1692 + let tag = 1693 + post.tags 1694 + |> list.filter(fn(tag) { 1695 + !string.starts_with(tag, ".") 1696 + }) 1697 + |> list.first() 1698 + |> result.unwrap("") 994 1699 link(Tagged(tag), tag) 995 1700 }, 996 1701 ], ··· 1014 1719 } 1015 1720 } 1016 1721 1017 - fn view_about() -> List(Element(Msg)) { 1722 + fn view_me() -> List(Element(Msg)) { 1018 1723 [ 1019 - html.div([attribute.class("mx-auto w-2/5")], [socials()]), 1724 + html.nav([attribute.class("social-media-icons mx-auto w-fit")], [ 1725 + socials(), 1726 + ]), 1020 1727 html.hr([attribute.class("my-8")]), 1021 1728 title("Me"), 1729 + link(Links, "-> Some links to find me around the web"), 1022 1730 [ 1023 1731 "Hi! I'm Mar, Maeryn, strawmelonjuice or however you may know me.", 1024 1732 "Let me introduce myself a bit!", 1025 1733 "I'm a software developer from the 🇳🇱NL, currently also a Software Engineering student at Fontys University of Applied Sciences, but I've been a tinkerer and a contributor in the OSS world for much longer. I love open source, and I love contributing to it. I do not only code, but also do small bits of art and writing, you could say I am creative in many ways! I think that's actually the reason I love code so much, because it allows me to be creative in a different way.", 1026 - "I also sometimes draw, usually digitally. I have some physical issues with my joints and nerves so I don't really draw a lot, but I do enjoy it sometimes! ...and I write, if I have the time!", 1027 1734 ] 1028 1735 |> list.map(paragraph) 1029 1736 |> element.fragment, 1737 + html.p([attribute.class("mt-6")], [ 1738 + element.text("I also sometimes "), 1739 + link(Tagged("creative-art"), "draw"), 1740 + element.text( 1741 + ", usually digitally. I have some physical issues with my joints and nerves so I don't really draw a lot, but I do enjoy it sometimes! ...and I ", 1742 + ), 1743 + link(Tagged("creative-writings"), "write"), 1744 + element.text(", if I have the time!"), 1745 + ]), 1746 + ] 1747 + } 1748 + 1749 + fn view_links() -> List(Element(Msg)) { 1750 + [ 1751 + html.div([attribute.class("link-list-container")], [ 1752 + html.nav([attribute.class("social-media-icons mx-auto w-fit")], [ 1753 + socials(), 1754 + ]), 1755 + html.hr([attribute.class("my-8")]), 1756 + // html.a( 1757 + // [href(Support), attribute.class("link-list-button")], 1758 + // [html.text("💝 Support Me")], 1759 + // ), 1760 + html.div([attribute.class("submenu bg-primary")], [ 1761 + // html.p([attribute.class("submenu-h3")], [html.text("Explore More")]), 1762 + html.p([], [ 1763 + html.text( 1764 + " Want to dive deeper into my work or learn more about me? Choose your path 1765 + below: ", 1766 + ), 1767 + ]), 1768 + html.a([href(Posts), attribute.class("link-list-button")], [ 1769 + html.text("📖 My blog posts"), 1770 + ]), 1771 + html.a( 1772 + [ 1773 + attribute.href("https://instagram.com/strawmelonjuice"), 1774 + attribute.class("link-list-button"), 1775 + ], 1776 + [html.text("📷 Instagram (request to follow)")], 1777 + ), 1778 + html.a([attribute.href("/me/"), attribute.class("link-list-button")], [ 1779 + html.text("👋 About me"), 1780 + ]), 1781 + html.a( 1782 + [attribute.href("/me/portfolio"), attribute.class("link-list-button")], 1783 + [html.text("😃 My portfolio")], 1784 + ), 1785 + html.p([], [html.text("Want to see code?")]), 1786 + html.div( 1787 + [attribute.class("button-row")], 1788 + [ 1789 + html.a( 1790 + [ 1791 + attribute.href("https://codeberg.org/strawmelonjuice"), 1792 + attribute.target("_blank"), 1793 + ], 1794 + [ 1795 + html.img([ 1796 + attribute.class("h-24 bg-white p-2 rounded-md"), 1797 + attribute.src( 1798 + "https://codeberg.org/Codeberg/Design/raw/branch/main/logo/horizontal/svg/codeberg-logo_horizontal_blue.svg", 1799 + ), 1800 + attribute.alt("Codeberg"), 1801 + ]), 1802 + ], 1803 + ), 1804 + html.a( 1805 + [ 1806 + attribute.href( 1807 + "https://forge.strawmelonjuice.com/strawmelonjuice", 1808 + ), 1809 + attribute.target("_blank"), 1810 + ], 1811 + [ 1812 + html.img([ 1813 + attribute.class("h-24 bg-white p-2 rounded-md"), 1814 + attribute.src( 1815 + "https://forgejo.org/images/forgejo-wordmark.svg", 1816 + ), 1817 + attribute.alt("Forgejo"), 1818 + ]), 1819 + ], 1820 + ), 1821 + html.a( 1822 + [ 1823 + attribute.href("https://github.com/strawmelonjuice"), 1824 + attribute.target("_blank"), 1825 + ], 1826 + [ 1827 + html.img([ 1828 + attribute.class("h-24"), 1829 + attribute.class("h-24 bg-white p-2 rounded-md"), 1830 + attribute.src("/img/GitHub_Lockup_Black.svg"), 1831 + ]), 1832 + ], 1833 + ), 1834 + html.a( 1835 + [ 1836 + attribute.href("https://tangled.org/strawmelonjuice.com/"), 1837 + attribute.target("_blank"), 1838 + attribute.class("flex items-center gap-2"), 1839 + ], 1840 + [ 1841 + html.img([ 1842 + attribute.class("h-24"), 1843 + attribute.src("/img/tangled.svg"), 1844 + ]), 1845 + html.span([attribute.class("font-bold text-xl not-italic")], [ 1846 + html.text("tangled"), 1847 + ]), 1848 + ], 1849 + ), 1850 + ] 1851 + |> list.shuffle(), 1852 + ), 1853 + ]), 1854 + // html.div([attribute.class("note text-info")], [ 1855 + // html.text(" More in the menus on this site! Take a look around! "), 1856 + // ]), 1857 + ]), 1030 1858 ] 1031 1859 } 1032 1860 ··· 1062 1890 1063 1891 fn leading(text: String) -> Element(Msg) { 1064 1892 html.p([attribute.class("mt-0 text-lg")], [html.text(text)]) 1065 - } 1066 - 1067 - fn preleading(text: String) -> Element(Msg) { 1068 - html.p([attribute.class("mb-0 text-sm")], [html.text(text)]) 1069 1893 } 1070 1894 1071 1895 fn paragraph(text: String) -> Element(Msg) {
+5 -1
src/homepage/djotparse.gleam
··· 9 9 djot 10 10 |> preprocess_tables 11 11 |> jot.to_html 12 - |> string.replace("<p", "<p class=\"mt-14\"") 12 + |> string.replace("<p", "<p class=\"mt-8\"") 13 + |> string.replace("<li", "<li class=\"mt-4\"") 14 + |> string.replace("<h1", "<h2 class=\"text-3xl font-light mt-14\"") 15 + |> string.replace("<h2", "<h3 class=\"text-2xl font-light mt-14\"") 16 + |> string.replace("<h3", "<h4 class=\"text-xl font-light mt-12\"") 13 17 |> string.replace("<a ", "<a class=\"link link-accent\" ") 14 18 |> string.replace("<strong", "<strong class=\"font-bold\"") 15 19 |> string.replace("<em", "<em class=\"italic\"")
+33
written-contents/blog/personal/2026/new-site.dj
··· 1 + Hello there! 2 + 3 + If you've been on my site more often, you've probably noticed some changes... 4 + The most annoying may be my new-founded lack of a dark theme, a decision I took consciously. 5 + 6 + No, the site has been redone, completely, from scratch. This has to do with a few things: 7 + 8 + ## 1. Back to the old theme 9 + 10 + My old, way more complicated `Cynthia Full` site used to carry a vibrant colourscheme that fits me to this day, beit a bit adjusted to my maturing eyes. 11 + In the past, my busy life filled with complications demanded something simpler, so I simplified my colourscheme and simplified my website. The Cynthia Mini site of 2025 was born. 12 + I yearned for my 'own' theme, with the 7-colour palette that I picked as a teen, deeming it "Fabuli Korreal". That is now 8 years ago and I have not found a more fun palette yet! 13 + 14 + ## 2. Cynthia's curse 15 + 16 + My site didn't take any time for me to manage at all, however, I was reliant on the ecosystem I created myself. While only using Cynthia Mini, I was also trying to get 17 + Cynthia Full to a new version, and decided Cynthia Mini had to get a simplification update, meanwhile I was writing a blogging generator framework (Cynthia ByYou) 18 + that also fitted into the Cynthia family... 19 + 20 + ...Long story short, Cynthia became quite the time-consuming ecosystem, even though I didn't even use the version I was updating myself. (I used a fork of Cynthia Mini) 21 + 22 + I decided that my site deserved the time it needed, but the generator for it may not. The site is now written in Gleam, instead of in config files! 23 + 24 + ## 3. A new era 25 + 26 + Of course, rewriting my site and restyling it is a big something. I am however glad to tell you all the ways in which this new site is better than the old one: 27 + 28 + - My great colourscheme is back! And no more dark theme, my site is bright and colourful, just like I like it. 29 + - Cynthia Mini v1 used to use hash urls for every page, which was a nightmare to manage and to link to. Now, the urls are clean and simple. 30 + - Comments are back and now with a Mastodon backend! This is a change that was planned to be integrated in Cynthia Mini v2, but since I don't have to wait for that to be complete, it's already in my site now! 31 + - The site is more narrow, even on big screens, which makes the experience more comfortable and less overwhelming and more consistent with the mobile experience. 32 + 33 + And yes, you can find the source code for this site on [my Forge](https://forge.strawmelonjuice.com/strawmelonjuice/homepage). I think I'll be porting content from the old site to the new one for a while, but I'm sure it's worth it!
-12
written-contents/test.dj
··· 1 - hey you, yes you. *I Love you*. _The misile is lost_. 2 - 3 - - _Not really, no_ 4 - - Well did you? 5 - 6 - {.bg-warning .text-warning-content} 7 - ::: Warning 8 - This is a warning. 9 - Here is a word in [français]{lang=fr}. 10 - ::: 11 - 12 - This is {=highlighted text=}.