My personal website, in gleam+lustre!
0
fork

Configure Feed

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

Random works

+319 -114
+2 -2
.envrc
··· 1 1 if nix flake show &> /dev/null; then 2 - use flake 3 - fi 2 + use flake 3 + fi
+3
.gitignore
··· 10 10 #Added automatically by Lustre Dev Tools 11 11 /.lustre 12 12 /dist 13 + 14 + node_modules 15 + assets/styles.css
-3
.vscode/settings.json
··· 1 - { 2 - "git.enabled": false 3 - }
assets/.gitkeep

This is a binary file and will not be displayed.

assets/strawmelonjuice.png

This is a binary file and will not be displayed.

+17
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "devDependencies": { 7 + "daisyui": "^5.5.19", 8 + "tailwindcss": "^4.2.1", 9 + }, 10 + }, 11 + }, 12 + "packages": { 13 + "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="], 14 + 15 + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], 16 + } 17 + }
+37 -28
flake.nix
··· 1 - { 2 - inputs = { 3 - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 - utils.url = "github:numtide/flake-utils"; 5 - }; 6 - 7 - outputs = { self, nixpkgs, utils }: 8 - utils.lib.eachDefaultSystem (system: 9 - let 10 - pkgs = import nixpkgs { inherit system; }; 11 - in 12 - { 13 - devShells.default = pkgs.mkShell { 14 - buildInputs = with pkgs; [ 15 - gleam 16 - erlang_28 17 - rebar3 18 - bun 19 - just 20 - watchexec 21 - ]; 22 - 23 - shellHook = '' 24 - gleam deps download 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 + utils.url = "github:numtide/flake-utils"; 5 + }; 6 + 7 + outputs = 8 + { 9 + self, 10 + nixpkgs, 11 + utils, 12 + }: 13 + utils.lib.eachDefaultSystem ( 14 + system: 15 + let 16 + pkgs = import nixpkgs { inherit system; }; 17 + in 18 + { 19 + devShells.default = pkgs.mkShell { 20 + buildInputs = with pkgs; [ 21 + gleam 22 + erlang_28 23 + rebar3 24 + bun 25 + tailwindcss_4 26 + just 27 + watchexec 28 + ]; 29 + 30 + shellHook = '' 31 + bun i --silent --only-missing 32 + gleam deps download 25 33 just --list 26 - ''; 27 - }; 28 - }); 29 - } 34 + ''; 35 + }; 36 + } 37 + ); 38 + }
+15 -11
gleam.toml
··· 1 1 name = "homepage" 2 2 version = "1.0.0" 3 3 4 - # Fill out these fields if you intend to generate HTML documentation or publish 5 - # your project to the Hex package manager. 6 - # 7 - # description = "" 8 - # licences = ["Apache-2.0"] 9 - # repository = { type = "github", user = "", repo = "" } 10 - # links = [{ title = "Website", href = "" }] 11 - # 12 - # For a full reference of all the available options, you can have a look at 13 - # https://gleam.run/writing-gleam/gleam-toml/. 14 - 15 4 [dependencies] 16 5 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 6 lustre = ">= 5.6.0 and < 6.0.0" ··· 23 12 simplifile = ">= 2.3.2 and < 3.0.0" 24 13 gleam_erlang = ">= 1.3.0 and < 2.0.0" 25 14 gleam_json = ">= 3.1.0 and < 4.0.0" 15 + 16 + [tools.lustre] 17 + bin.bun = "system" 18 + 19 + [tools.lustre.build] 20 + no-tailwind = true 21 + minify =true 22 + 23 + [tools.lustre.html] 24 + links=[ 25 + {rel= "preconnect", href = "https://fontlay.com", crossorigin = "" }, 26 + {rel= "stylesheet", href = "" } 27 + ] 28 + stylesheets = [{ href = "/styles.css" }, { href = "https://fontlay.com/css2?family=Lilex&display=swap" }] 29 + title = "Mar's site"
+7 -2
justfile
··· 9 9 mkdir -p src/homepage/from_prebuild 10 10 cp --update=none priv/codegen-templates/data.gleam src/homepage/from_prebuild/data.gleam 11 11 gleam run -m homepage/prepare 12 + tailwindcss -i ./site.css -o ./assets/styles.css 12 13 13 14 # Produce a JavaScript bundle in dist. 14 15 build: prepare ··· 19 20 20 21 [parallel] 21 22 [private] 22 - preview-inner: prepare lustre_dev_start prepare-preview 23 + preview-inner: prepare lustre_dev_start prepare-preview tw_watch 23 24 24 25 [private] 25 26 prepare-preview: 26 - watchexec --restart -w'./written-contents' -- just prepare 27 + watchexec --restart -w'./written-contents' -w'src' --stop-timeout 0 -- just prepare 28 + 29 + [private] 30 + tw_watch: 31 + tailwindcss -i ./site.css -o ./assets/styles.css --watch 27 32 28 33 # Watches gleam files and previews 29 34 [private]
+7
package.json
··· 1 + { 2 + "dependencies": {}, 3 + "devDependencies": { 4 + "daisyui": "^5.5.19", 5 + "tailwindcss": "^4.2.1" 6 + } 7 + }
+54
site.css
··· 1 + @import "tailwindcss"; 2 + @source "./src/homepage.gleam"; 3 + @source "./src/**/*.gleam"; 4 + @plugin "daisyui"; 5 + 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: #85aaa0; 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 + } 41 + 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; 49 + } 50 + body { 51 + /*background-color: #f8e4e4;*/ 52 + @apply w-screen h-screen; 53 + } 54 + }
+174 -67
src/homepage.gleam
··· 6 6 id: 0, 7 7 title: "Test", 8 8 summary: "Testing", 9 - body: File(Plain, "./written-contents/test.dj"), 10 - aliases: [], 9 + body: File(Plain, "./written-contents/test.txt"), 10 + aliases: ["first"], 11 + comments: CommentsDisable, 11 12 ), 12 13 ] 13 14 } ··· 18 19 import gleam/int 19 20 import gleam/list 20 21 import gleam/pair 22 + import gleam/result 21 23 import gleam/uri.{type Uri} 24 + import homepage/djotparse 25 + import homepage/from_prebuild/data 22 26 import lustre 23 - import lustre/attribute.{type Attribute} 27 + import lustre/attribute.{type Attribute, attribute} 24 28 import lustre/effect.{type Effect} 25 29 import lustre/element.{type Element} 26 30 import lustre/element/html 31 + import lustre/element/svg 27 32 import modem 28 33 29 34 pub fn main() { ··· 50 55 summary: String, 51 56 body: Body, 52 57 aliases: List(String), 58 + comments: MastodonComments, 53 59 ) 54 60 } 55 61 62 + type MastodonComments { 63 + /// example https://[pony.social]/@strawmelonjuice/[115911235653686237]. 64 + MastodonStatusLink(instance: String, id: String) 65 + CommentsDisable 66 + } 67 + 56 68 type NormalizedPost { 57 - NormalizedPost(id: Int, title: String, summary: String, body: Element(Msg)) 69 + NormalizedPost( 70 + id: Int, 71 + title: String, 72 + summary: String, 73 + body: Element(Msg), 74 + comments: MastodonComments, 75 + ) 58 76 } 59 77 60 78 type Body { 61 79 Lustre(Element(Msg)) 62 80 File(markup: Markup, path: String) 63 - String(markup: Markup, string: String) 81 + Inline(markup: Markup, string: String) 64 82 } 65 83 66 84 pub type Markup { ··· 77 95 Index 78 96 Posts 79 97 PostById(id: Int) 80 - About 98 + Me 99 + Portfolio 81 100 NotFound(uri: Uri) 82 101 } 83 102 84 - import homepage/djotparse 85 - import homepage/from_prebuild/data 86 - 87 103 fn post_normalize(takes: Post) -> NormalizedPost { 88 - let Post(id:, title:, summary:, body: _, aliases: _) = takes 89 - NormalizedPost(id:, title:, summary:, body: post_body_normalize(takes)) 104 + let Post(id:, title:, summary:, body: _, aliases: _, comments:) = takes 105 + NormalizedPost( 106 + id:, 107 + title:, 108 + summary:, 109 + body: post_body_normalize(takes), 110 + comments:, 111 + ) 90 112 } 91 113 92 114 fn post_body_normalize(takes: Post) { ··· 95 117 File(markup:, path:) -> { 96 118 let assert Ok(inner) = data.files() |> dict.get(path) 97 119 as "File does not exist in data." 98 - post_body_normalize(Post(..takes, body: String(markup:, string: inner))) 120 + post_body_normalize(Post(..takes, body: Inline(markup:, string: inner))) 99 121 } 100 122 // The actual thing 101 - String(Djot, string:) -> 123 + Inline(Djot, string:) -> 102 124 djotparse.djot_to_html(string) 103 125 |> element.unsafe_raw_html("", "div", [], _) 104 - String(markup: Plain, string:) -> html.pre([], [element.text(string)]) 105 - String(markup: HTML, string:) -> 126 + Inline(markup: Plain, string:) -> html.pre([], [element.text(string)]) 127 + Inline(markup: HTML, string:) -> 106 128 element.unsafe_raw_html("", "div", [], string) 107 129 } 108 130 } 109 131 110 - fn parse_route(uri: Uri) -> Route { 132 + fn parse_route(uri: Uri, model: Model) -> Route { 111 133 case uri.path_segments(uri.path) { 112 134 [] | [""] -> Index 113 135 114 136 ["posts"] -> Posts 115 137 116 138 ["post", post_id] -> 117 - case int.parse(post_id) { 118 - Ok(post_id) -> PostById(id: post_id) 119 - Error(_) -> NotFound(uri:) 120 - } 139 + result.unwrap( 140 + result.map( 141 + result.or(dict.get(model.aliases, post_id), int.parse(post_id)), 142 + PostById, 143 + ), 144 + NotFound(uri:), 145 + ) 121 146 122 - ["about"] -> About 147 + ["me"] -> Me 148 + ["me", "portfolio"] -> Portfolio 123 149 124 150 _ -> NotFound(uri:) 125 151 } ··· 128 154 fn href(route: Route) -> Attribute(msg) { 129 155 let url = case route { 130 156 Index -> "/" 131 - About -> "/about" 157 + Me -> "/me" 132 158 Posts -> "/posts" 133 159 PostById(post_id) -> "/post/" <> int.to_string(post_id) 160 + Portfolio -> "/me/portfolio" 134 161 NotFound(_) -> "/404" 135 162 } 136 163 ··· 138 165 } 139 166 140 167 fn init(_) -> #(Model, Effect(Msg)) { 141 - let route = case modem.initial_uri() { 142 - Ok(uri) -> parse_route(uri) 143 - Error(_) -> Index 144 - } 145 - 146 168 let aliases = 147 169 list.map(posts(), fn(post) { 148 170 list.map(post.aliases, fn(alias) { #(post.id, alias) }) ··· 156 178 |> list.map(fn(post) { #(post.id, post |> post_normalize) }) 157 179 |> dict.from_list 158 180 181 + let route = case modem.initial_uri() { 182 + Ok(uri) -> parse_route(uri, Model(Index, posts:, aliases:)) 183 + Error(_) -> Index 184 + } 159 185 let model = Model(route:, posts:, aliases:) 160 186 161 187 let effect = 162 - modem.init(fn(uri) { 188 + modem.init(fn(uri: Uri) -> Msg { 163 189 uri 164 - |> parse_route 190 + |> parse_route(model) 165 191 |> UserNavigatedTo 166 192 }) 167 193 ··· 175 201 } 176 202 177 203 fn view(model: Model) -> Element(Msg) { 178 - html.div([attribute.class("mx-auto max-w-2xl px-32")], [ 179 - html.nav([attribute.class("flex justify-between items-center my-16")], [ 180 - html.h1([attribute.class("text-purple-600 font-medium text-xl")], [ 181 - html.a([href(Index)], [html.text("My little Blog")]), 204 + element.fragment([ 205 + // Header start 206 + html.nav([attribute.class("navbar bg-secondary shadow-sm")], [ 207 + html.div([attribute.class("navbar-start")], [ 208 + html.div([attribute.class("dropdown")], [ 209 + html.div( 210 + [ 211 + attribute.class("btn btn-ghost btn-circle"), 212 + attribute.role("button"), 213 + attribute("tabindex", "0"), 214 + ], 215 + [ 216 + svg.svg( 217 + [ 218 + attribute("stroke", "currentColor"), 219 + attribute("viewBox", "0 0 24 24"), 220 + attribute("fill", "none"), 221 + attribute.class("h-5 w-5"), 222 + attribute("xmlns", "http://www.w3.org/2000/svg"), 223 + ], 224 + [ 225 + svg.path([ 226 + attribute("d", "M4 6h16M4 12h16M4 18h7"), 227 + attribute("stroke-width", "2"), 228 + attribute("stroke-linejoin", "round"), 229 + attribute("stroke-linecap", "round"), 230 + ]), 231 + ], 232 + ), 233 + ], 234 + ), 235 + html.ul( 236 + [ 237 + attribute.class( 238 + "menu menu-sm dropdown-content bg-neutral rounded-box z-1 mt-3 w-52 p-2 shadow", 239 + ), 240 + attribute("tabindex", "-1"), 241 + ], 242 + [ 243 + view_header_link(current: model.route, to: Index, label: "Home"), 244 + view_header_link( 245 + current: model.route, 246 + to: Portfolio, 247 + label: "Portfolio", 248 + ), 249 + view_header_link(current: model.route, to: Me, label: "Me"), 250 + view_header_link(current: model.route, to: Posts, label: "Posts"), 251 + ], 252 + ), 253 + ]), 254 + ]), 255 + html.div([attribute.class("navbar-center")], [ 256 + html.a([href(Index)], [ 257 + html.img([ 258 + attribute.class("w-[60px]"), 259 + attribute.alt("strawmelonjuice"), 260 + attribute.src("/strawmelonjuice.png"), 261 + ]), 262 + ]), 182 263 ]), 183 - html.ul([attribute.class("flex space-x-8")], [ 184 - view_header_link(current: model.route, to: Posts, label: "Posts"), 185 - view_header_link(current: model.route, to: About, label: "About"), 264 + html.div([attribute.class("navbar-end")], [ 265 + html.button([attribute.class("btn btn-ghost btn-circle")], [ 266 + svg.svg( 267 + [ 268 + attribute("stroke", "currentColor"), 269 + attribute("viewBox", "0 0 24 24"), 270 + attribute("fill", "none"), 271 + attribute.class("h-5 w-5"), 272 + attribute("xmlns", "http://www.w3.org/2000/svg"), 273 + ], 274 + [ 275 + svg.path([ 276 + attribute("d", "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"), 277 + attribute("stroke-width", "2"), 278 + attribute("stroke-linejoin", "round"), 279 + attribute("stroke-linecap", "round"), 280 + ]), 281 + ], 282 + ), 283 + ]), 186 284 ]), 187 285 ]), 188 - html.main([attribute.class("my-16")], { 189 - case model.route { 190 - Index -> view_index() 191 - Posts -> view_posts(model) 192 - PostById(post_id) -> view_post(model, post_id) 193 - About -> view_about() 194 - NotFound(_) -> view_not_found() 195 - } 196 - }), 286 + // Header end 287 + html.main( 288 + [ 289 + attribute.class( 290 + "mx-auto my-14 max-w-2xl bg-primary bg-opacity-15 text-primary-content h-full md:rounded-md md:p-12 md:h-fit", 291 + ), 292 + ], 293 + { 294 + case model.route { 295 + Index -> view_index() 296 + Posts -> view_posts(model) 297 + PostById(post_id) -> view_post(model, post_id) 298 + Me -> view_about() 299 + Portfolio -> todo 300 + NotFound(_) -> view_not_found() 301 + } 302 + }, 303 + ), 197 304 ]) 198 - } 199 - 200 - fn view_header_link( 201 - to target: Route, 202 - current current: Route, 203 - label text: String, 204 - ) -> Element(msg) { 205 - let is_active = case current, target { 206 - PostById(_), Posts -> True 207 - _, _ -> current == target 208 - } 209 - 210 - html.li( 211 - [ 212 - attribute.classes([ 213 - #("border-transparent border-b-2 hover:border-purple-600", True), 214 - #("text-purple-600", is_active), 215 - ]), 216 - ], 217 - [html.a([href(target)], [html.text(text)])], 218 - ) 219 305 } 220 306 221 307 fn view_index() -> List(Element(msg)) { ··· 308 394 html.a( 309 395 [ 310 396 href(target), 311 - attribute.class("text-purple-600 hover:underline cursor-pointer"), 397 + attribute.class("link link-primary"), 312 398 ], 313 399 [html.text(title)], 314 400 ) 315 401 } 402 + 403 + fn view_header_link( 404 + to target: Route, 405 + current current: Route, 406 + label text: String, 407 + ) -> Element(msg) { 408 + let is_active = case current, target { 409 + PostById(_), Posts -> True 410 + _, _ -> current == target 411 + } 412 + 413 + html.li( 414 + [ 415 + attribute.classes([ 416 + #("", True), 417 + #("text-accent-content bg-accent", is_active), 418 + ]), 419 + ], 420 + [html.a([href(target)], [html.text(text)])], 421 + ) 422 + }
-1
written-contents/test.dj
··· 1 - hoi "testing", ik probeer
+3
written-contents/test.txt
··· 1 + hoi "testing", ik probeer hier 2 + iets uit 3 + wa-