From e28cef74c3e254e9675c23f04b7bc3f1f6a870d3 Mon Sep 17 00:00:00 2001 From: Guillaume Hivert Date: Wed, 16 Oct 2024 16:57:38 +0200 Subject: [PATCH] Add quick'n'dirty packages listing --- .../src/backend/postgres/queries.gleam | 42 ++++++++++++++ apps/backend/src/backend/router.gleam | 6 ++ apps/frontend/src/data/model.gleam | 3 + apps/frontend/src/data/msg.gleam | 1 + apps/frontend/src/data/package.gleam | 4 +- apps/frontend/src/frontend.gleam | 10 +++- apps/frontend/src/frontend/router.gleam | 3 + .../src/frontend/view/body/body.gleam | 57 +++++++++++++++++++ .../src/frontend/view/navbar/navbar.gleam | 7 +-- apps/frontend/src/stylesheets/all.css | 10 ++++ 10 files changed, 136 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/backend/postgres/queries.gleam b/apps/backend/src/backend/postgres/queries.gleam index 80372cd..6e2dc02 100644 --- a/apps/backend/src/backend/postgres/queries.gleam +++ b/apps/backend/src/backend/postgres/queries.gleam @@ -925,6 +925,48 @@ pub fn select_package_by_popularity(db: pgo.Connection, page: Int) { |> result.map_error(error.DatabaseError) } +pub fn select_package_by_updated_at(db: pgo.Connection) { + "SELECT + name, + repository, + documentation, + hex_url, + licenses, + description, + rank, + popularity + FROM package + ORDER BY updated_at DESC" + |> pgo.execute( + db, + [], + dynamic.decode8( + fn(a, b, c, d, e, f, g, h) { + json.object([ + #("name", json.string(a)), + #("repository", json.nullable(b, json.string)), + #("documentation", json.nullable(c, json.string)), + #("hex-url", json.nullable(d, json.string)), + #("licenses", json.string(e)), + #("description", json.nullable(f, json.string)), + #("rank", json.int(g)), + #("popularity", json.nullable(h, json.string)), + ]) + }, + dynamic.element(0, dynamic.string), + dynamic.element(1, dynamic.optional(dynamic.string)), + dynamic.element(2, dynamic.optional(dynamic.string)), + dynamic.element(3, dynamic.optional(dynamic.string)), + dynamic.element(4, dynamic.string), + dynamic.element(5, dynamic.optional(dynamic.string)), + dynamic.element(6, dynamic.int), + dynamic.element(7, dynamic.optional(dynamic.string)), + ), + ) + |> result.map(fn(r) { r.rows }) + |> result.map_error(error.DatabaseError) +} + pub fn insert_analytics( db: pgo.Connection, id: Int, diff --git a/apps/backend/src/backend/router.gleam b/apps/backend/src/backend/router.gleam index 517b421..e3fdc0b 100644 --- a/apps/backend/src/backend/router.gleam +++ b/apps/backend/src/backend/router.gleam @@ -138,6 +138,12 @@ fn encode_package(package: #(String, String, Int, option.Option(Int))) { pub fn handle_get(req: Request, ctx: Context) { case wisp.path_segments(req) { ["healthcheck"] -> wisp.ok() + ["packages"] -> + queries.select_package_by_updated_at(ctx.db) + |> result.unwrap([]) + |> json.preprocessed_array + |> json.to_string_builder + |> wisp.json_response(200) ["trendings"] -> wisp.get_query(req) |> list.find(fn(item) { item.0 == "page" }) diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 0dfb9a8..4b7e742 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -25,6 +25,7 @@ pub type Model { search_results: Dict(String, SearchResults), index: Index, loading: Bool, + packages: List(package.Package), view_cache: Dict(String, Element(Msg)), route: router.Route, is_mobile: Bool, @@ -57,6 +58,7 @@ pub fn init() { search_results: dict.new(), index: index, loading: False, + packages: [], view_cache: dict.new(), route: router.Home, is_mobile: is_mobile(), @@ -338,6 +340,7 @@ pub fn reset(model: Model) { index: [], loading: False, view_cache: model.view_cache, + packages: model.packages, route: router.Home, is_mobile: is_mobile(), trendings: model.trendings, diff --git a/apps/frontend/src/data/msg.gleam b/apps/frontend/src/data/msg.gleam index aa2818c..26229e5 100644 --- a/apps/frontend/src/data/msg.gleam +++ b/apps/frontend/src/data/msg.gleam @@ -38,6 +38,7 @@ pub type Analytics { pub type Msg { None + Packages(packages: Result(List(package.Package), http.HttpError)) OnSearchFocus(event: Event) SubmitSearch UpdateIsMobile(is_mobile: Bool) diff --git a/apps/frontend/src/data/package.gleam b/apps/frontend/src/data/package.gleam index 29f171b..7c60a96 100644 --- a/apps/frontend/src/data/package.gleam +++ b/apps/frontend/src/data/package.gleam @@ -1,4 +1,5 @@ import gleam/dynamic +import gleam/io import gleam/json import gleam/option.{type Option} import gleam/result @@ -34,7 +35,8 @@ pub fn decoder(dyn) { dynamic.field("popularity", fn(dyn) { use data <- result.try(dynamic.optional(dynamic.string)(dyn)) option.unwrap(data, "{}") - |> json.decode(using: dynamic.field("github", dynamic.int)) + |> json.decode(using: dynamic.optional_field("github", dynamic.int)) + |> result.map(option.unwrap(_, 0)) |> result.replace_error([dynamic.DecodeError("", "", [])]) }), )(dyn) diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index 1643401..7c44d5a 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -12,6 +12,7 @@ import frontend/view/body/search_result as sr import gleam/bool import gleam/dict import gleam/dynamic +import gleam/io import gleam/option.{None, Some} import gleam/pair import gleam/result @@ -99,6 +100,10 @@ fn init(_) { http.expect_json(dynamic.list(package.decoder), msg.Trendings) |> http.get(config.api_endpoint() <> "/trendings", _), ) + |> update.add_effect( + http.expect_json(dynamic.list(package.decoder), msg.Packages) + |> http.get(config.api_endpoint() <> "/packages", _), + ) |> update.add_effect( msg.OnAnalytics |> http.expect_json( @@ -135,11 +140,13 @@ fn on_url_change(uri: Uri) -> Msg { } fn update(model: Model, msg: Msg) { - case msg { + case io.debug(msg) { msg.UpdateInput(content) -> update_input(model, content) msg.SubmitSearch -> submit_search(model) msg.Reset -> reset(model) msg.None -> update.none(model) + msg.Packages(Ok(packages)) -> model.Model(..model, packages:) |> update.none + msg.Packages(_) -> update.none(model) msg.ScrollTo(id) -> scroll_to(model, id) msg.OnRouteChange(route) -> handle_route_change(model, route) msg.Trendings(trendings) -> handle_trendings(model, trendings) @@ -242,6 +249,7 @@ fn handle_route_change(model: Model, route: router.Route) { let model = model.update_route(model, route) case route { router.Home -> model.update_input(model, "") + router.Packages -> model.update_input(model, "") router.Trending -> model.update_input(model, "") router.Analytics -> model.update_input(model, "") router.Search(q) -> diff --git a/apps/frontend/src/frontend/router.gleam b/apps/frontend/src/frontend/router.gleam index 1525a0a..f5377f1 100644 --- a/apps/frontend/src/frontend/router.gleam +++ b/apps/frontend/src/frontend/router.gleam @@ -8,6 +8,7 @@ import lustre/effect pub type Route { Home Search(query: String) + Packages Trending Analytics } @@ -15,6 +16,7 @@ pub type Route { pub fn parse_uri(uri: Uri) -> Route { case uri.path_segments(uri.path) { ["search"] -> handle_search_path(uri) + ["packages"] -> Packages ["trending"] -> Trending ["analytics"] -> Analytics _ -> Home @@ -38,6 +40,7 @@ pub fn update_page_title(route: Route) { use _ <- effect.from() case route { Home -> ffi.update_title("Gloogle") + Packages -> ffi.update_title("Gloogle — Packages") Search(q) -> ffi.update_title("Gloogle — Search " <> q) Trending -> ffi.update_title("Gloogle — Trending") Analytics -> ffi.update_title("Gloogle — Analytics") diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index d06c3dc..54e3758 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -178,6 +178,7 @@ fn sidebar(model: Model) { h.div([a.class("sidebar-spacer"), disabled], []), h.div([a.class("sidebar-links")], [ sidebar_link(href: "/analytics", icon: icons.trends(), title: "Analytics"), + sidebar_link(href: "/packages", icon: icons.gift(), title: "Packages"), // s.sidebar_link_wrapper([], [ // s.sidebar_icon([], [icons.shortcuts()]), // s.sidebar_link([], [el.text("Shortcuts")]), @@ -373,9 +374,65 @@ fn view_analytics(model: Model) { ]) } +fn view_packages(model: Model) { + el.fragment([ + sidebar(model), + h.main( + [a.class("main"), a.style([#("padding", "24px 36px")])], + list.intersperse( + { + use package <- list.map(model.packages) + h.div([a.class("search-result")], [ + h.div([a.class("search-details")], [ + h.div([a.class("search-details-name")], [h.text(package.name)]), + h.div([a.class("search-details-links")], [ + case package.hex_url { + option.None -> el.none() + option.Some(url) -> + h.a([a.href(url), a.target("_blank"), a.rel("noreferrer")], [ + h.text("Show hex"), + ]) + }, + case package.documentation { + option.None -> el.none() + option.Some(url) -> + h.a([a.href(url), a.target("_blank"), a.rel("noreferrer")], [ + h.text("Show documentation"), + ]) + }, + ]), + ]), + case package.description { + option.None -> el.none() + option.Some(description) -> + h.div([a.class("search-body")], [ + h.div([a.class("signature")], [h.text(description)]), + ]) + }, + h.div([a.class("search-details-links")], [ + case package.repository { + option.None -> el.none() + option.Some(url) -> + h.a([a.href(url), a.target("_blank"), a.rel("noreferrer")], [ + h.text("Show repository"), + ]) + }, + h.div([a.class("search-details-licences")], [ + package.licenses |> string.join(" ") |> h.text, + ]), + ]), + ]) + }, + h.div([a.class("search-result-separator")], []), + ), + ), + ]) +} + pub fn body(model: Model) { case model.route { router.Home -> h.main([a.class("main")], [view_search_input(model)]) + router.Packages -> view_packages(model) router.Trending -> h.main([a.class("main")], [view_trending(model)]) router.Analytics -> view_analytics(model) router.Search(_) -> { diff --git a/apps/frontend/src/frontend/view/navbar/navbar.gleam b/apps/frontend/src/frontend/view/navbar/navbar.gleam index a506789..6b40b4d 100644 --- a/apps/frontend/src/frontend/view/navbar/navbar.gleam +++ b/apps/frontend/src/frontend/view/navbar/navbar.gleam @@ -6,10 +6,7 @@ import lustre/element/html as h fn navbar_links() { s.nav_links([], [ - s.trending([], [ - h.text("Packages"), - s.coming_soon([], [h.text(" (coming soon…)")]), - ]), + s.nav_link([a.href("/packages")], [h.text("Packages")]), s.nav_link([a.href("/analytics")], [h.text("Analytics")]), ]) } @@ -19,7 +16,7 @@ pub fn navbar(model: Model) { s.navbar(transparent, [a.class("navbar")], [ case model.route { router.Home -> navbar_links() - router.Search(_) | router.Trending | router.Analytics -> + router.Search(_) | router.Trending | router.Analytics | router.Packages -> s.navbar_search([], [ s.navbar_search_title([a.href("/")], [ s.search_lucy(40, [a.src("/images/lucy.svg")]), diff --git a/apps/frontend/src/stylesheets/all.css b/apps/frontend/src/stylesheets/all.css index 0dad7dc..36e4a1a 100644 --- a/apps/frontend/src/stylesheets/all.css +++ b/apps/frontend/src/stylesheets/all.css @@ -590,3 +590,13 @@ lazy-node:has(:not(:defined)) { overflow: hidden; padding: 12px; } + +.search-details-links { + display: flex; + gap: 12px; + font-size: 0.9rem; +} + +.search-details-links a { + color: var(--input-text-color); +}