Add repositories view

Changed files
+117 -15
src
+117 -15
src/website.gleam
··· 1 import gleam/list 2 import gleam/string 3 import gleam/uri.{type Uri} ··· 9 import lustre/event 10 import lustre/ui 11 import lustre/ui/layout/cluster 12 import modem 13 import website/common 14 import website/posts 15 import website/projects 16 - import gleam/dynamic 17 - import lustre_http 18 19 // Main 20 ··· 32 current_route: Route, 33 posts: List(posts.Post(Msg)), 34 projects: List(projects.Project(Msg)), 35 - repositories: List(Repository) 36 ) 37 } 38 ··· 62 description: String, 63 name: String, 64 stars_count: Int, 65 ) 66 } 67 68 fn get_repositories() -> Effect(Msg) { 69 let url = "https://codeberg.org/api/v1/users/naomi/repos" 70 - let decoder = dynamic.list(of: dynamic.decode4( 71 - Repository, 72 - dynamic.field("avatar_url", dynamic.string), 73 - dynamic.field("description", dynamic.string), 74 - dynamic.field("name", dynamic.string), 75 - dynamic.field("stars_count", dynamic.int) 76 - )) 77 lustre_http.get(url, lustre_http.expect_json(decoder, ApiGotRepositories)) 78 } 79 80 fn switch_theme() -> Effect(Msg) { 81 effect.from(fn(disp) { 82 do_switch_theme() ··· 101 current_route: get_route(), 102 projects: projects.all(), 103 posts: posts.all(), 104 ), 105 { 106 init_theme() 107 - modem.init(on_route_change) 108 }, 109 ) 110 } ··· 136 ) 137 ChangeDarkMode -> #(model, switch_theme()) 138 DoneChangeDarkMode -> #(model, effect.none()) 139 - ApiGotRepositories -> #(model, effect.none()) 140 } 141 } 142 ··· 219 ]) 220 } 221 222 - fn view_home(_model: Model) -> Element(Msg) { 223 html.div([], [ 224 html.div( 225 [ ··· 229 ], 230 [ 231 html.p([attr.class("mb-4")], [ 232 element.text( 233 "Hi! I'm Naomi (or Mia), a trans girl from the UK who loves to code! I mostly make Minecraft mods, but have started 234 to begin other projects like ", ··· 247 ), 248 cluster.of( 249 html.div, 250 - [attr.class("gap-4 sm:gap-20 flex flex-col sm:flex-row")], 251 [ 252 ui.stack([], [ 253 html.h3([attr.class("text-2xl underline")], [ ··· 296 "drop-shadow-md text-xl p-4 mb-4 rounded-xl bg-gray-200 dark:bg-neutral-800 dark:text-neutral-200", 297 ), 298 ], 299 - [html.h1([attr.class("text-3xl m-0 text-pink-400")],[element.text("Repositories")])], 300 ), 301 ]) 302 }
··· 1 + import birl 2 + import gleam/dynamic 3 + import gleam/int 4 import gleam/list 5 import gleam/string 6 import gleam/uri.{type Uri} ··· 12 import lustre/event 13 import lustre/ui 14 import lustre/ui/layout/cluster 15 + import lustre_http 16 import modem 17 import website/common 18 import website/posts 19 import website/projects 20 + import gleam/order 21 22 // Main 23 ··· 35 current_route: Route, 36 posts: List(posts.Post(Msg)), 37 projects: List(projects.Project(Msg)), 38 + repos: List(Repository), 39 ) 40 } 41 ··· 65 description: String, 66 name: String, 67 stars_count: Int, 68 + html_url: String, 69 + updated_at: String, 70 + ) 71 + } 72 + 73 + type RepositoryBirled { 74 + RepositoryBirled( 75 + avatar_url: String, 76 + description: String, 77 + name: String, 78 + stars_count: Int, 79 + html_url: String, 80 + updated_at: birl.Time, 81 ) 82 } 83 84 fn get_repositories() -> Effect(Msg) { 85 let url = "https://codeberg.org/api/v1/users/naomi/repos" 86 + let decoder = 87 + dynamic.list(of: dynamic.decode6( 88 + Repository, 89 + dynamic.field("avatar_url", dynamic.string), 90 + dynamic.field("description", dynamic.string), 91 + dynamic.field("name", dynamic.string), 92 + dynamic.field("stars_count", dynamic.int), 93 + dynamic.field("html_url", dynamic.string), 94 + dynamic.field("updated_at", dynamic.string), 95 + )) 96 lustre_http.get(url, lustre_http.expect_json(decoder, ApiGotRepositories)) 97 } 98 99 + fn sort_repos(repos: List(Repository)) -> List(RepositoryBirled) { 100 + let repos = 101 + repos 102 + |> list.map(fn(repo) { 103 + case repo { 104 + Repository(a, b, c, d, e, time) -> { 105 + let time = case time |> birl.parse { 106 + Ok(time) -> time 107 + Error(_) -> { 108 + birl.now() |> birl.set_day(birl.Day(1, 1, 1970)) 109 + } 110 + } 111 + RepositoryBirled(a, b, c, d, e, time) 112 + } 113 + } 114 + }) 115 + |> list.sort(fn(a, b) { 116 + let a = a.updated_at |> birl.get_day 117 + let b = b.updated_at |> birl.get_day 118 + order.break_tie(order.break_tie(int.compare(a.year, b.year), int.compare(a.month, b.month)), int.compare(a.date, b.date)) 119 + }) 120 + |> list.reverse 121 + } 122 + 123 fn switch_theme() -> Effect(Msg) { 124 effect.from(fn(disp) { 125 do_switch_theme() ··· 144 current_route: get_route(), 145 projects: projects.all(), 146 posts: posts.all(), 147 + repos: [], 148 ), 149 { 150 init_theme() 151 + effect.batch([get_repositories(), modem.init(on_route_change)]) 152 }, 153 ) 154 } ··· 180 ) 181 ChangeDarkMode -> #(model, switch_theme()) 182 DoneChangeDarkMode -> #(model, effect.none()) 183 + ApiGotRepositories(Ok(repos)) -> #( 184 + Model(..model, repos: repos), 185 + effect.none(), 186 + ) 187 + 188 + ApiGotRepositories(Error(_)) -> #(model, effect.none()) 189 } 190 } 191 ··· 268 ]) 269 } 270 271 + fn view_home(model: Model) -> Element(Msg) { 272 html.div([], [ 273 html.div( 274 [ ··· 278 ], 279 [ 280 html.p([attr.class("mb-4")], [ 281 + html.h1([attr.class("text-3xl m-0 mb-4 text-pink-400")], [ 282 + element.text("About Me"), 283 + ]), 284 element.text( 285 "Hi! I'm Naomi (or Mia), a trans girl from the UK who loves to code! I mostly make Minecraft mods, but have started 286 to begin other projects like ", ··· 299 ), 300 cluster.of( 301 html.div, 302 + [attr.class("gap-4 sm:gap-20 flex flex-col md:flex-row")], 303 [ 304 ui.stack([], [ 305 html.h3([attr.class("text-2xl underline")], [ ··· 348 "drop-shadow-md text-xl p-4 mb-4 rounded-xl bg-gray-200 dark:bg-neutral-800 dark:text-neutral-200", 349 ), 350 ], 351 + [ 352 + html.h1([attr.class("text-3xl m-0 mb-4 text-pink-400")], [ 353 + element.text("Repositories"), 354 + ]), 355 + html.div( 356 + [attr.class("grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4")], 357 + model.repos 358 + |> sort_repos 359 + |> list.map(fn(repo) { 360 + let url = case repo.avatar_url { 361 + "" -> "https://avatars.githubusercontent.com/u/95784613?v=4" 362 + a -> a 363 + } 364 + html.a( 365 + [ 366 + attr.class( 367 + " 368 + drop-shadow-lg rounded-xl p-4 369 + dark:text-neutral-200 370 + bg-gray-300 dark:bg-neutral-700 371 + hover:bg-slate-300 dark:hover:bg-slate-700 372 + ", 373 + ), 374 + attr.href(repo.html_url), 375 + ], 376 + [ 377 + html.div([attr.class("flex items-center")], [ 378 + html.img([ 379 + attr.src(url), 380 + attr.class("drop-shadow-md rounded-xl max-w-16 max-h-16"), 381 + ]), 382 + html.div([attr.class("m-2")], [ 383 + html.h2([], [element.text(repo.name)]), 384 + html.h2([], [ 385 + element.text( 386 + "⭐ " <> repo.stars_count |> int.to_string, 387 + ), 388 + ]), 389 + ]), 390 + ]), 391 + html.div([], [ 392 + case repo.description { 393 + "" -> html.p([attr.class("text-neutral-500 dark:text-neutral-400")], [element.text("No description")]) 394 + a -> element.text(a) 395 + } 396 + ]), 397 + ], 398 + ) 399 + }), 400 + ), 401 + ], 402 ), 403 ]) 404 }