Add repositories view

Changed files
+117 -15
src
+117 -15
src/website.gleam
··· 1 + import birl 2 + import gleam/dynamic 3 + import gleam/int 1 4 import gleam/list 2 5 import gleam/string 3 6 import gleam/uri.{type Uri} ··· 9 12 import lustre/event 10 13 import lustre/ui 11 14 import lustre/ui/layout/cluster 15 + import lustre_http 12 16 import modem 13 17 import website/common 14 18 import website/posts 15 19 import website/projects 16 - import gleam/dynamic 17 - import lustre_http 20 + import gleam/order 18 21 19 22 // Main 20 23 ··· 32 35 current_route: Route, 33 36 posts: List(posts.Post(Msg)), 34 37 projects: List(projects.Project(Msg)), 35 - repositories: List(Repository) 38 + repos: List(Repository), 36 39 ) 37 40 } 38 41 ··· 62 65 description: String, 63 66 name: String, 64 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, 65 81 ) 66 82 } 67 83 68 84 fn get_repositories() -> Effect(Msg) { 69 85 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 - )) 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 + )) 77 96 lustre_http.get(url, lustre_http.expect_json(decoder, ApiGotRepositories)) 78 97 } 79 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 + 80 123 fn switch_theme() -> Effect(Msg) { 81 124 effect.from(fn(disp) { 82 125 do_switch_theme() ··· 101 144 current_route: get_route(), 102 145 projects: projects.all(), 103 146 posts: posts.all(), 147 + repos: [], 104 148 ), 105 149 { 106 150 init_theme() 107 - modem.init(on_route_change) 151 + effect.batch([get_repositories(), modem.init(on_route_change)]) 108 152 }, 109 153 ) 110 154 } ··· 136 180 ) 137 181 ChangeDarkMode -> #(model, switch_theme()) 138 182 DoneChangeDarkMode -> #(model, effect.none()) 139 - ApiGotRepositories -> #(model, effect.none()) 183 + ApiGotRepositories(Ok(repos)) -> #( 184 + Model(..model, repos: repos), 185 + effect.none(), 186 + ) 187 + 188 + ApiGotRepositories(Error(_)) -> #(model, effect.none()) 140 189 } 141 190 } 142 191 ··· 219 268 ]) 220 269 } 221 270 222 - fn view_home(_model: Model) -> Element(Msg) { 271 + fn view_home(model: Model) -> Element(Msg) { 223 272 html.div([], [ 224 273 html.div( 225 274 [ ··· 229 278 ], 230 279 [ 231 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 + ]), 232 284 element.text( 233 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 234 286 to begin other projects like ", ··· 247 299 ), 248 300 cluster.of( 249 301 html.div, 250 - [attr.class("gap-4 sm:gap-20 flex flex-col sm:flex-row")], 302 + [attr.class("gap-4 sm:gap-20 flex flex-col md:flex-row")], 251 303 [ 252 304 ui.stack([], [ 253 305 html.h3([attr.class("text-2xl underline")], [ ··· 296 348 "drop-shadow-md text-xl p-4 mb-4 rounded-xl bg-gray-200 dark:bg-neutral-800 dark:text-neutral-200", 297 349 ), 298 350 ], 299 - [html.h1([attr.class("text-3xl m-0 text-pink-400")],[element.text("Repositories")])], 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 + ], 300 402 ), 301 403 ]) 302 404 }