My personal website, in gleam+lustre!
0
fork

Configure Feed

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

What the hell just happened

+535 -137
+10 -5
gleam.toml
··· 6 6 lustre = ">= 5.6.0 and < 6.0.0" 7 7 modem = ">= 2.1.2 and < 3.0.0" 8 8 jot = ">= 10.1.1 and < 11.0.0" 9 + chilp = ">= 1.0.0 and < 2.0.0" 9 10 10 11 [dev-dependencies] 11 12 lustre_dev_tools = ">= 2.3.4 and < 3.0.0" ··· 18 19 19 20 [tools.lustre.build] 20 21 no-tailwind = true 21 - minify =true 22 + minify = true 22 23 23 24 [tools.lustre.html] 24 - links=[ 25 - {rel= "preconnect", href = "https://fontlay.com", crossorigin = "" }, 26 - {rel= "stylesheet", href = "" } 25 + links = [ 26 + { rel = "preconnect", href = "https://fontlay.com", crossorigin = "" }, 27 + { rel = "shortcut icon", href = "/strawmelonjuice.png", type = "image/x-icon" }, 28 + { rel = "stylesheet", href = "" }, 29 + ] 30 + stylesheets = [ 31 + { href = "/styles.css" }, 32 + { href = "https://fontlay.com/css2?family=Lilex&display=swap" }, 27 33 ] 28 - stylesheets = [{ href = "/styles.css" }, { href = "https://fontlay.com/css2?family=Lilex&display=swap" }] 29 34 title = "Mar's site"
+5
manifest.toml
··· 4 4 packages = [ 5 5 { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 6 { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 7 + { name = "chilp", version = "1.0.0-pre", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time", "lustre", "rsvp"], otp_app = "chilp", source = "hex", outer_checksum = "4F927BB2984ECB3F3CF5B6189A46B8A84D844926A3ECB325498C172596C246B7" }, 7 8 { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 9 { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 10 { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, ··· 12 13 { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, 13 14 { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 14 15 { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 16 + { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, 15 17 { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 16 18 { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 19 + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 17 20 { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 18 21 { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 19 22 { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, ··· 36 39 { name = "modem", version = "2.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "3F9682EBCBF4D26045F1038A7507E8C7967E49D43F9CA6BA68EF0C971B195A7F" }, 37 40 { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 38 41 { name = "polly", version = "3.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_erlang", "gleam_otp", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "51FB565D81FF6212FDF3306D44419601F2A7C4EDD1F00FC9DA5C376A00AED4FE" }, 42 + { name = "rsvp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "40F9E0E662FF258E10C7041A9591261FE802D56625FB444B91510969644F7722" }, 39 43 { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 40 44 { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, 41 45 { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, ··· 45 49 ] 46 50 47 51 [requirements] 52 + chilp = { version = ">= 1.0.0 and < 2.0.0" } 48 53 gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 49 54 gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 50 55 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
+77 -43
site.css
··· 4 4 @plugin "daisyui"; 5 5 6 6 @plugin "daisyui/theme" { 7 - name: "strawmelonjuicedotcom"; 8 - default: true; 9 - prefersdark: true; 10 - color-scheme: "dark"; 11 - --color-base-100: oklch(96% 0.059 95.617); 12 - --color-base-200: oklch(93% 0.032 17.717); 13 - --color-base-300: oklch(92% 0.12 95.746); 14 - --color-base-content: oklch(25% 0.09 281.288); 15 - --color-primary: #edbcbc; 16 - --color-primary-content: oklch(44% 0.043 257.281); 17 - --color-secondary: #ceddd9; 18 - --color-secondary-content: oklch(29% 0.066 243.157); 19 - --color-accent: oklch(71% 0.203 305.504); 20 - --color-accent-content: oklch(38% 0.063 188.416); 21 - --color-neutral: #adebb3; 22 - --color-neutral-content: oklch(28% 0.141 291.089); 23 - --color-info: oklch(96% 0.059 95.617); 24 - --color-info-content: oklch(42% 0.095 57.708); 25 - --color-success: oklch(76% 0.177 163.223); 26 - --color-success-content: oklch(37% 0.077 168.94); 27 - --color-warning: oklch(70% 0.213 47.604); 28 - --color-warning-content: oklch(0% 0 0); 29 - --color-error: oklch(63% 0.237 25.331); 30 - --color-error-content: oklch(0% 0 0); 31 - --radius-selector: 0.5rem; 32 - --radius-field: 2rem; 33 - --radius-box: 0.25rem; 34 - --size-selector: 0.25rem; 35 - --size-field: 0.1875rem; 36 - --border: 2px; 37 - --depth: 1; 38 - --noise: 1; 39 - --font-roboto: "Lilex", monospace, system-ui, sans-serif; 7 + name: "strawmelonjuicedotcom"; 8 + default: true; 9 + prefersdark: true; 10 + color-scheme: "dark"; 11 + --color-base-100: oklch(96% 0.059 95.617); 12 + --color-base-200: oklch(93% 0.032 17.717); 13 + --color-base-300: oklch(92% 0.12 95.746); 14 + --color-base-content: oklch(25% 0.09 281.288); 15 + --color-primary: #edbcbc; 16 + --color-primary-content: oklch(44% 0.043 257.281); 17 + --color-secondary: #ceddd9; 18 + --color-secondary-content: oklch(29% 0.066 243.157); 19 + --color-accent: oklch(71% 0.203 305.504); 20 + --color-accent-content: oklch(38% 0.063 188.416); 21 + --color-neutral: #adebb3; 22 + --color-neutral-content: oklch(28% 0.141 291.089); 23 + --color-info: oklch(96% 0.059 95.617); 24 + --color-info-content: oklch(42% 0.095 57.708); 25 + --color-success: oklch(76% 0.177 163.223); 26 + --color-success-content: oklch(37% 0.077 168.94); 27 + --color-warning: oklch(70% 0.213 47.604); 28 + --color-warning-content: oklch(0% 0 0); 29 + --color-error: oklch(63% 0.237 25.331); 30 + --color-error-content: oklch(0% 0 0); 31 + --radius-selector: 0.5rem; 32 + --radius-field: 2rem; 33 + --radius-box: 0.25rem; 34 + --size-selector: 0.25rem; 35 + --size-field: 0.1875rem; 36 + --border: 2px; 37 + --depth: 1; 38 + --noise: 1; 39 + --font-roboto: "Lilex", monospace, system-ui, sans-serif; 40 40 } 41 41 42 42 @layer base { 43 - html { 44 - font-family: "Lilex", monospace, system-ui, sans-serif; 45 - font-optical-sizing: auto; 46 - font-weight: 400; 47 - font-style: normal; 48 - margin: 0; 43 + html { 44 + font-family: "Lilex", monospace, system-ui, sans-serif; 45 + font-optical-sizing: auto; 46 + font-weight: 400; 47 + font-style: normal; 48 + margin: 0; 49 + } 50 + 51 + body, 52 + #app { 53 + /*background-color: #f8e4e4;*/ 54 + @apply w-screen h-screen; 55 + } 56 + 57 + footer { 58 + position: fixed; 59 + bottom: 0; 60 + -webkit-animation: seconds 1.0s forwards; 61 + -webkit-animation-iteration-count: 1; 62 + -webkit-animation-delay: 5s; 63 + animation: seconds 1.0s forwards; 64 + animation-iteration-count: 1; 65 + animation-delay: 5s; 66 + } 67 + 68 + @-webkit-keyframes seconds { 69 + 0% { 70 + opacity: 1; 49 71 } 50 72 51 - body { 52 - /*background-color: #f8e4e4;*/ 53 - @apply w-screen h-screen; 73 + 100% { 74 + opacity: 0; 75 + left: -9999px; 54 76 } 55 - } 77 + } 78 + 79 + @keyframes seconds { 80 + 0% { 81 + opacity: 1; 82 + } 83 + 84 + 100% { 85 + opacity: 0; 86 + left: -9999px; 87 + } 88 + } 89 + }
+443 -89
src/homepage.gleam
··· 5 5 Post( 6 6 id: 0, 7 7 title: "Test", 8 - summary: "Testing", 8 + summary: "This is a test post to check that everything is working.", 9 + published: "2026-03-11", 10 + revised: Some("Shortly after"), 9 11 body: File(Djot, "./written-contents/test.dj"), 10 12 aliases: ["first"], 11 - comments: CommentsDisable, 13 + comments: MastodonStatusLink("pony.social", "115911235653686237"), 14 + category: "Testing", 15 + tags: [ 16 + "test", 17 + "hello world", 18 + "gleam", 19 + "djot", 20 + "markdown", 21 + "blogging", 22 + "lifecycle", 23 + "post management", 24 + "content management system", 25 + "website development", 26 + "web development", 27 + "programming", 28 + "coding", 29 + "software engineering", 30 + "technology", 31 + "personal blog", 32 + "blog-personal", 33 + ], 12 34 ), 13 35 ] 14 36 } 15 37 38 + const highlighted_posts = [0] 39 + 16 40 // The site itself 17 41 42 + import chilp 43 + import chilp/widget 18 44 import gleam/dict.{type Dict} 19 45 import gleam/int 20 46 import gleam/list 47 + import gleam/option.{type Option, None, Some} 21 48 import gleam/pair 22 49 import gleam/result 23 50 import gleam/uri.{type Uri} ··· 32 59 import modem 33 60 34 61 pub fn main() { 62 + let assert Ok(_) = widget.register() 35 63 let app = lustre.application(init, update, view) 36 64 let assert Ok(_) = lustre.start(app, "#app", Nil) 37 65 ··· 51 79 type Post { 52 80 Post( 53 81 id: Int, 82 + category: String, 54 83 title: String, 55 84 summary: String, 85 + published: String, 86 + revised: Option(String), 56 87 body: Body, 88 + tags: List(String), 57 89 aliases: List(String), 58 90 comments: MastodonComments, 59 91 ) ··· 68 100 type NormalizedPost { 69 101 NormalizedPost( 70 102 id: Int, 103 + category: String, 71 104 title: String, 105 + published: String, 106 + revised: Option(String), 72 107 summary: String, 73 108 body: Element(Msg), 109 + tags: List(String), 74 110 comments: MastodonComments, 75 111 ) 76 112 } ··· 98 134 Me 99 135 Portfolio 100 136 NotFound(uri: Uri) 137 + AllAndEverything 138 + Category(String) 139 + Tagged(String) 101 140 } 102 141 103 142 fn post_normalize(takes: Post) -> NormalizedPost { 104 - let Post(id:, title:, summary:, body: _, aliases: _, comments:) = takes 143 + let Post( 144 + id:, 145 + title:, 146 + summary:, 147 + body: _, 148 + aliases: _, 149 + comments:, 150 + published:, 151 + revised:, 152 + category:, 153 + tags:, 154 + ) = takes 105 155 NormalizedPost( 106 156 id:, 107 157 title:, 108 158 summary:, 159 + revised:, 160 + published:, 109 161 body: post_body_normalize(takes), 110 162 comments:, 163 + category:, 164 + tags:, 111 165 ) 112 166 } 113 167 ··· 135 189 136 190 ["posts"] -> Posts 137 191 192 + ["posts", "tagged", tag] -> Tagged(tag) 193 + 194 + ["posts", "category", category] -> Category(category) 195 + 138 196 ["post", post_id] -> 139 197 result.unwrap( 140 198 result.map( ··· 147 205 ["me"] -> Me 148 206 ["me", "portfolio"] -> Portfolio 149 207 208 + ["everything"] -> AllAndEverything 209 + 150 210 _ -> NotFound(uri:) 151 211 } 152 212 } 153 213 154 - fn href(route: Route) -> Attribute(msg) { 214 + fn href(route: Route) -> Attribute(Msg) { 155 215 let url = case route { 156 216 Index -> "/" 157 217 Me -> "/me" 158 218 Posts -> "/posts" 159 219 PostById(post_id) -> "/post/" <> int.to_string(post_id) 160 220 Portfolio -> "/me/portfolio" 221 + AllAndEverything -> "/everything" 161 222 NotFound(_) -> "/404" 223 + Tagged(v) -> "/posts/tagged/" <> v 224 + Category(c) -> "/posts/category/" <> c 162 225 } 163 226 164 227 attribute.href(url) ··· 201 264 } 202 265 203 266 fn view(model: Model) -> Element(Msg) { 267 + let view_header_link = fn(target: Route, text: String) -> Element(Msg) { 268 + let current = model.route 269 + let is_active = case current, target { 270 + PostById(_), Posts -> True 271 + _, _ -> current == target 272 + } 273 + 274 + html.li( 275 + [ 276 + attribute.classes([ 277 + #("", True), 278 + #("underline", is_active), 279 + // #("border-2 border-dotted border-accent rounded-full", is_active), 280 + ]), 281 + ], 282 + [html.a([href(target)], [html.text(text)])], 283 + ) 284 + } 204 285 let header_links = 205 286 [ 206 - view_header_link(current: model.route, to: Index, label: "Home"), 207 - view_header_link(current: model.route, to: Portfolio, label: "Portfolio"), 208 - view_header_link(current: model.route, to: Me, label: "Me"), 209 - view_header_link(current: model.route, to: Posts, label: "Posts"), 287 + view_header_link(Index, "Home"), 288 + view_header_link(Portfolio, "Portfolio"), 289 + view_header_link(Me, "Me"), 290 + view_header_link(Posts, "Posts"), 210 291 ] 211 292 |> element.fragment 212 293 element.fragment([ ··· 256 337 html.ul([attribute.class("menu menu-horizontal px-1 hidden md:flex")], [ 257 338 header_links, 258 339 ]), 259 - // Search - Disabled because I am too lazy to do this rn. 260 - // html.button([attribute.class("btn btn-ghost btn-circle")], [ 261 - // svg.svg( 262 - // [ 263 - // attribute("stroke", "currentColor"), 264 - // attribute("viewBox", "0 0 24 24"), 265 - // attribute("fill", "none"), 266 - // attribute.class("h-5 w-5"), 267 - // attribute("xmlns", "http://www.w3.org/2000/svg"), 268 - // ], 269 - // [ 270 - // svg.path([ 271 - // attribute("d", "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"), 272 - // attribute("stroke-width", "2"), 273 - // attribute("stroke-linejoin", "round"), 274 - // attribute("stroke-linecap", "round"), 275 - // ]), 276 - // ], 277 - // ), 278 - // ]), 340 + // Search 341 + html.a([href(AllAndEverything)], [ 342 + html.button( 343 + [ 344 + attribute.class("btn btn-ghost btn-circle"), 345 + ], 346 + [ 347 + svg.svg( 348 + [ 349 + attribute("stroke", "currentColor"), 350 + attribute("viewBox", "0 0 24 24"), 351 + attribute("fill", "none"), 352 + attribute.class("h-5 w-5"), 353 + attribute("xmlns", "http://www.w3.org/2000/svg"), 354 + ], 355 + [ 356 + svg.path([ 357 + attribute( 358 + "d", 359 + "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z", 360 + ), 361 + attribute("stroke-width", "2"), 362 + attribute("stroke-linejoin", "round"), 363 + attribute("stroke-linecap", "round"), 364 + ]), 365 + ], 366 + ), 367 + ], 368 + ), 369 + ]), 279 370 ]), 280 371 html.div([attribute.class("navbar-center")], [ 281 372 html.a([href(Index)], [ ··· 294 385 html.main( 295 386 [ 296 387 attribute.class( 297 - "mx-auto my-14 max-w-2xl bg-primary bg-opacity-15 text-primary-content h-full md:rounded-md p-5 md:p-12 md:h-fit", 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", 298 389 ), 299 390 ], 300 391 { 301 392 case model.route { 302 - Index -> view_index() 303 - Posts -> view_posts(model) 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)) 304 397 PostById(post_id) -> view_post(model, post_id) 305 398 Me -> view_about() 306 - Portfolio -> todo 399 + Portfolio -> view_portfolio() 307 400 NotFound(_) -> view_not_found() 401 + AllAndEverything -> view_all_and_everything(model) 308 402 } 309 403 }, 310 404 ), 405 + html.footer( 406 + [ 407 + attribute.class( 408 + "footer sm:footer-horizontal footer-center bg-base-300 text-base-content p-4 text-xs", 409 + ), 410 + ], 411 + [ 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 + ), 428 + ]), 429 + ]), 430 + ], 431 + ), 311 432 ]) 312 433 } 313 434 435 + fn view_portfolio() -> List(Element(Msg)) { 436 + [element.text("wawawawawawa (portfolio coming soon)")] 437 + } 438 + 439 + fn view_all_and_everything(model) -> List(Element(Msg)) { 440 + let wrap = fn(target: Route, label: String, content: List(Element(Msg))) -> Element( 441 + Msg, 442 + ) { 443 + [ 444 + html.a( 445 + [ 446 + href(target), 447 + attribute.class("link link-accent"), 448 + ], 449 + [title(label)], 450 + ), 451 + html.div([attribute.class("my-8 bg-secondary rounded-md p-4")], content), 452 + ] 453 + |> element.fragment 454 + } 455 + 456 + [ 457 + element.text( 458 + "I've not implemented a search function yet, but you can use control+f to search through everything on this page!", 459 + ), 460 + html.hr([attribute.class("my-8")]), 461 + wrap(Index, "Homepage", view_index(model)), 462 + wrap(Posts, "All Posts", view_posts(model, AllPosts)), 463 + wrap(Me, "About Me", view_about()), 464 + wrap(Portfolio, "Portfolio", view_portfolio()), 465 + ] 466 + } 467 + 314 468 fn socials() -> Element(Msg) { 315 469 html.div([attribute.class("grid grid-flow-col gap-4")], [ 316 470 // Mastodon ================================================================================= ··· 318 472 [ 319 473 attribute.target("_blank"), 320 474 attribute.href("https://pony.social/@strawmelonjuice"), 475 + attribute.class("cursor-pointer w-[26px] h-[26px] hover:text-[#5638cc]"), 321 476 ], 322 477 [ 323 478 svg.svg( ··· 346 501 // [ 347 502 // attribute.target("_blank"), 348 503 // attribute.href("https://instagram.com/strawmelonjuice"), 504 + // attribute.class("cursor-pointer"), 349 505 // ], 350 506 // [ 351 507 // svg.svg( ··· 409 565 [ 410 566 attribute.target("_blank"), 411 567 attribute.href("https://matrix.to/#/@mar:strawmelonjuice.com"), 568 + attribute.class( 569 + "cursor-pointer w-[26px] h-[26px] hover:text-black hover:bg-white", 570 + ), 412 571 ], 413 572 [ 414 573 svg.svg( ··· 456 615 [ 457 616 attribute.target("_blank"), 458 617 attribute.href("mailto:web@strawmelonjuice.com"), 618 + attribute.class("cursor-pointer w-[26px] h-[26px] hover:text-[#cc3856]"), 459 619 ], 460 620 [ 461 621 svg.svg( ··· 498 658 attribute.href( 499 659 "https://witchsky.app/profile/did:plc:jgtfsmv25thfs4zmydtbccnn", 500 660 ), 661 + attribute.class("cursor-pointer w-[26px] h-[26px] hover:text-[#006aff]"), 501 662 ], 502 663 [ 503 664 svg.svg( ··· 613 774 ]) 614 775 } 615 776 616 - fn view_index() -> List(Element(msg)) { 777 + fn view_index(model: Model) -> List(Element(Msg)) { 617 778 [ 618 779 title("Welcome to my fruity little corner of the web"), 619 780 leading("I hope you like it!"), 620 - html.p([attribute.class("mt-14")], [ 621 - html.text("There is not much going on at the moment, but you can still "), 622 - link(Posts, "read my ramblings ->"), 623 - ]), 624 - paragraph("If you like <3"), 781 + paragraph( 782 + "This is my personal website, where I share my thoughts, projects and other things that I find interesting. I'm a software developer, but I also do some art and writing on the side. I hope to share all of that here in the future!", 783 + ), 784 + html.br([]), 785 + case highlighted_posts { 786 + [] -> 787 + html.p([attribute.class("mt-6")], [ 788 + html.text("I write things sometimes, "), 789 + link(Posts, "check them out ->"), 790 + ]) 791 + _ -> { 792 + [ 793 + subtitle("Pinned posts"), 794 + list.map(highlighted_posts, fn(id) { 795 + let assert Ok(post) = model.posts |> dict.get(id) 796 + as "A pinned post does not exist :o" 797 + view_post_preview(post) 798 + }) 799 + |> element.fragment, 800 + link(Posts, "see all posts ->"), 801 + ] 802 + |> element.fragment 803 + } 804 + }, 805 + paragraph(""), 806 + subsubtitle("Missing something?"), 807 + paragraph( 808 + "That's okay! I hope to port all my content here over time, but be sure to use the Matrix or email link on the top right to reach out and let me know what's still missing!", 809 + ), 625 810 ] 626 811 } 627 812 628 - fn view_posts(model: Model) -> List(Element(msg)) { 813 + type PostFilter { 814 + AllPosts 815 + PostsInCategory(String) 816 + PostsWithTag(String) 817 + } 818 + 819 + fn view_posts(model: Model, filtered: PostFilter) -> List(Element(Msg)) { 629 820 let posts = 630 821 model.posts 822 + |> dict.filter(fn(_id, post) { 823 + case filtered { 824 + AllPosts -> True 825 + PostsInCategory(category) -> post.category == category 826 + PostsWithTag(tag) -> list.contains(post.tags, tag) 827 + } 828 + }) 631 829 |> dict.values 632 830 |> list.sort(fn(a, b) { int.compare(a.id, b.id) }) 633 - |> list.map(fn(post) { 634 - html.article([attribute.class("mt-14")], [ 635 - html.h3([attribute.class("text-xl text-purple-600 font-light")], [ 636 - html.a([attribute.class("hover:underline"), href(PostById(post.id))], [ 637 - html.text(post.title), 831 + |> list.map(view_post_preview) 832 + 833 + [ 834 + case filtered { 835 + AllPosts -> { 836 + [ 837 + title("All posts"), 838 + html.p([attribute.class("mt-6")], [ 839 + element.text("Filter: "), 840 + html.a([href(Tagged("blog-personal"))], [ 841 + html.button([attribute.class("btn btn-sm btn-info")], [ 842 + element.text(" Blog posts "), 843 + ]), 844 + ]), 845 + html.button( 846 + [ 847 + attribute("style", "anchor-name:--anchor-1"), 848 + attribute("popovertarget", "popover-1"), 849 + attribute.class("btn btn-sm btn-info"), 850 + ], 851 + [html.text(" Creative ")], 852 + ), 853 + html.ul( 854 + [ 855 + attribute("style", "position-anchor:--anchor-1"), 856 + attribute.id("popover-1"), 857 + attribute("popover", ""), 858 + attribute.class( 859 + "dropdown menu w-52 rounded-box bg-base-100 shadow-sm", 860 + ), 861 + ], 862 + [ 863 + html.li([], [ 864 + html.a([href(Tagged("creative"))], [ 865 + html.text("Everything"), 866 + ]), 867 + ]), 868 + html.li([], [ 869 + html.a([href(Tagged("creative-art"))], [ 870 + html.text("Creative content"), 871 + ]), 872 + ]), 873 + html.li([], [ 874 + html.a([href(Tagged("creative-writings"))], [ 875 + html.text("Stories"), 876 + ]), 877 + ]), 878 + ], 879 + ), 638 880 ]), 639 - ]), 640 - html.p([attribute.class("mt-1")], [html.text(post.summary)]), 641 - ]) 642 - }) 881 + ] 882 + |> element.fragment 883 + } 884 + PostsInCategory(category) -> { 885 + [ 886 + title("Posts in category: " <> category), 887 + html.p([], [link(Posts, "<- See all posts")]), 888 + ] 889 + |> element.fragment 890 + } 891 + PostsWithTag(tag) -> 892 + [ 893 + title("Posts with tag: " <> tag), 894 + html.p([], [link(Posts, "<- See all posts instead")]), 895 + ] 896 + |> element.fragment 897 + }, 898 + ..posts 899 + ] 900 + } 643 901 644 - [title("Posts"), ..posts] 902 + fn view_post_preview(post: NormalizedPost) -> Element(Msg) { 903 + html.article([attribute.class("my-4 bg-secondary rounded-md p-4")], [ 904 + html.h4([attribute.class("text-lg text-accent font-light")], [ 905 + html.a([attribute.class("hover:underline"), href(PostById(post.id))], [ 906 + html.text(post.title), 907 + ]), 908 + ]), 909 + html.br([]), 910 + html.p( 911 + [ 912 + attribute.class( 913 + "mb-2 text-sm rounded-md bg-base-200 text-base-content", 914 + ), 915 + ], 916 + [ 917 + html.text( 918 + "Category: " 919 + <> post.category 920 + <> " • Posted: " 921 + <> post.published 922 + <> case post.revised { 923 + Some(updated) -> " (updated: " <> updated <> ")" 924 + None -> "" 925 + }, 926 + ), 927 + ], 928 + ), 929 + 930 + html.p([attribute.class("mt-1")], [ 931 + html.text(post.summary), 932 + html.hr([attribute.class("my-4")]), 933 + link(PostById(post.id), "Read more ->"), 934 + ]), 935 + ]) 645 936 } 646 937 647 938 fn view_post(model: Model, post_id: Int) -> List(Element(Msg)) { ··· 649 940 Error(_) -> view_not_found() 650 941 Ok(post) -> [ 651 942 html.article([], [ 943 + html.p( 944 + [ 945 + attribute.class( 946 + "mb-2 text-sm rounded-md p-2 bg-base-200 text-base-content", 947 + ), 948 + ], 949 + [ 950 + element.text("Category: "), 951 + link(Category(post.category), post.category), 952 + html.text( 953 + " • Posted: " 954 + <> post.published 955 + <> case post.revised { 956 + Some(updated) -> " • Updated: " <> updated 957 + None -> "" 958 + }, 959 + ), 960 + ], 961 + ), 652 962 title(post.title), 653 963 leading(post.summary), 654 - post.body, 964 + html.section([attribute.class("p-2 m-1 ")], [post.body]), 655 965 ]), 656 - html.p([attribute.class("mt-14")], [link(Posts, "<- Go back?")]), 966 + html.hr([attribute.class("my-8")]), 967 + html.p([attribute.class("mt-10")], [ 968 + link(Posts, "<- Go back to all posts"), 969 + ]), 970 + html.div( 971 + [ 972 + attribute.classes([#("hidden", post.tags == [])]), 973 + ], 974 + [ 975 + html.hr([attribute.class("my-8")]), 976 + html.blockquote([], [ 977 + html.p( 978 + [ 979 + attribute.class("mt-6"), 980 + ], 981 + [html.text("Tags: ")] 982 + |> list.append( 983 + post.tags 984 + |> list.rest() 985 + |> result.unwrap([]) 986 + |> list.map(fn(tag) { 987 + [element.text(", "), link(Tagged(tag), tag)] 988 + |> element.fragment 989 + }) 990 + |> list.append( 991 + [ 992 + { 993 + let tag = post.tags |> list.first() |> result.unwrap("") 994 + link(Tagged(tag), tag) 995 + }, 996 + ], 997 + _, 998 + ), 999 + ), 1000 + ), 1001 + ]), 1002 + case post.comments { 1003 + CommentsDisable -> 1004 + [ 1005 + html.hr([attribute.class("my-8")]), 1006 + element.text("Comments are disabled for this post."), 1007 + ] 1008 + |> element.fragment 1009 + MastodonStatusLink(instance:, id:) -> chilp.widget(instance, id) 1010 + }, 1011 + ], 1012 + ), 657 1013 ] 658 1014 } 659 1015 } 660 1016 661 - fn view_about() -> List(Element(msg)) { 1017 + fn view_about() -> List(Element(Msg)) { 662 1018 [ 1019 + html.div([attribute.class("mx-auto w-2/5")], [socials()]), 1020 + html.hr([attribute.class("my-8")]), 663 1021 title("Me"), 664 - paragraph( 665 - "I document the odd occurrences that catch my attention and rewrite my own 666 - narrative along the way. I'm fine being referred to with pronouns.", 667 - ), 668 - paragraph( 669 - "If you enjoy these glimpses into my mind, feel free to come back 670 - semi-regularly. But not too regularly, you creep.", 671 - ), 1022 + [ 1023 + "Hi! I'm Mar, Maeryn, strawmelonjuice or however you may know me.", 1024 + "Let me introduce myself a bit!", 1025 + "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 + ] 1028 + |> list.map(paragraph) 1029 + |> element.fragment, 672 1030 ] 673 1031 } 674 1032 675 - fn view_not_found() -> List(Element(msg)) { 1033 + fn view_not_found() -> List(Element(Msg)) { 676 1034 [ 677 1035 title("Not found"), 678 1036 paragraph( ··· 682 1040 ] 683 1041 } 684 1042 685 - fn title(title: String) -> Element(msg) { 1043 + // Reusable non-component components ======================================================================== 1044 + 1045 + fn title(title: String) -> Element(Msg) { 686 1046 html.h2([attribute.class("text-3xl text-accent-content font-light")], [ 687 1047 html.text(title), 688 1048 ]) 689 1049 } 690 1050 691 - fn leading(text: String) -> Element(msg) { 692 - html.p([attribute.class("mt-8 text-lg")], [html.text(text)]) 1051 + fn subtitle(subtitle: String) -> Element(Msg) { 1052 + html.h3([attribute.class("text-xl text-accent font-light")], [ 1053 + html.text(subtitle), 1054 + ]) 693 1055 } 694 1056 695 - fn paragraph(text: String) -> Element(msg) { 696 - html.p([attribute.class("mt-14")], [html.text(text)]) 1057 + fn subsubtitle(subsubtitle: String) -> Element(Msg) { 1058 + html.h4([attribute.class("text-lg text-accent font-light")], [ 1059 + html.text(subsubtitle), 1060 + ]) 697 1061 } 698 1062 699 - fn link(target: Route, title: String) -> Element(msg) { 1063 + fn leading(text: String) -> Element(Msg) { 1064 + 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 + } 1070 + 1071 + fn paragraph(text: String) -> Element(Msg) { 1072 + html.p([attribute.class("mt-6")], [html.text(text)]) 1073 + } 1074 + 1075 + fn link(target: Route, title: String) -> Element(Msg) { 700 1076 html.a( 701 1077 [ 702 1078 href(target), ··· 705 1081 [html.text(title)], 706 1082 ) 707 1083 } 708 - 709 - fn view_header_link( 710 - to target: Route, 711 - current current: Route, 712 - label text: String, 713 - ) -> Element(msg) { 714 - let is_active = case current, target { 715 - PostById(_), Posts -> True 716 - _, _ -> current == target 717 - } 718 - 719 - html.li( 720 - [ 721 - attribute.classes([ 722 - #("", True), 723 - #("underline", is_active), 724 - // #("border-2 border-dotted border-accent rounded-full", is_active), 725 - ]), 726 - ], 727 - [html.a([href(target)], [html.text(text)])], 728 - ) 729 - }