Skip to content

Commit

Permalink
wip: implement trending page
Browse files Browse the repository at this point in the history
Signed-off-by: Guillaume Hivert <[email protected]>
  • Loading branch information
ghivert committed May 21, 2024
1 parent fb3e18e commit 1a143d4
Show file tree
Hide file tree
Showing 14 changed files with 298 additions and 17 deletions.
19 changes: 17 additions & 2 deletions apps/backend/src/backend/config.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import gleam/result
import wisp
import wisp/logger

pub type Environment {
Development
Production
}

pub type Context {
Context(db: pgo.Connection, hex_api_key: String, github_token: String)
Context(
db: pgo.Connection,
hex_api_key: String,
github_token: String,
env: Environment,
)
}

pub type Config {
Expand All @@ -16,21 +26,26 @@ pub type Config {
port: Int,
level: logger.Level,
github_token: String,
env: Environment,
)
}

pub fn read_config() {
let assert Ok(database_url) = os.get_env("DATABASE_URL")
let assert Ok(hex_api_key) = os.get_env("HEX_API_KEY")
let assert Ok(github_token) = os.get_env("GITHUB_TOKEN")
let env = case result.unwrap(os.get_env("GLEAM_ENV"), "") {
"development" -> Development
_ -> Production
}
let assert Ok(port) =
os.get_env("PORT")
|> result.try(int.parse)
let level =
os.get_env("LOG_LEVEL")
|> result.try(logger.parse)
|> result.unwrap(logger.Info)
Config(database_url, hex_api_key, port, level, github_token)
Config(database_url, hex_api_key, port, level, github_token, env)
}

pub fn get_secret_key_base() {
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/backend/postgres/postgres.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub fn connect(cnf: Config) {
db: db,
hex_api_key: cnf.hex_api_key,
github_token: cnf.github_token,
env: cnf.env,
)
}
}
Expand Down
46 changes: 46 additions & 0 deletions apps/backend/src/backend/postgres/queries.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,49 @@ pub fn update_package_popularity(
|> pgo.execute(db, [pgo.text(url), popularity], dynamic.dynamic)
|> result.map_error(error.DatabaseError)
}

pub fn select_package_by_popularity(db: pgo.Connection, page: Int) {
let offset = 40 * page
"SELECT
name,
repository,
documentation,
hex_url,
licenses,
description,
rank,
popularity
FROM package
WHERE popularity -> 'github' IS NOT NULL
ORDER BY popularity -> 'github' DESC
LIMIT 40
OFFSET $1"
|> pgo.execute(
db,
[pgo.int(offset)],
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)
}
15 changes: 15 additions & 0 deletions apps/backend/src/backend/router.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import backend/web
import cors_builder as cors
import gleam/bool
import gleam/http
import gleam/int
import gleam/json
import gleam/list
import gleam/pair
Expand Down Expand Up @@ -108,6 +109,20 @@ fn search(query: String, ctx: Context) {
pub fn handle_get(req: Request, ctx: Context) {
case wisp.path_segments(req) {
["healthcheck"] -> wisp.ok()
["trendings"] ->
wisp.get_query(req)
|> list.find(fn(item) { item.0 == "page" })
|> result.try(fn(item) { int.parse(item.1) })
|> result.try_recover(fn(_) { Ok(0) })
|> result.unwrap(0)
|> queries.select_package_by_popularity(ctx.db, _)
|> result.map(fn(content) {
content
|> json.preprocessed_array()
|> json.to_string_builder()
|> wisp.json_response(200)
})
|> result.unwrap(wisp.internal_server_error())
["search"] -> {
wisp.get_query(req)
|> list.find(fn(item) { item.0 == "q" })
Expand Down
11 changes: 8 additions & 3 deletions apps/backend/src/tasks/popularity.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import gleam/result
import wisp

pub fn compute_popularity(ctx: Context) {
wisp.log_info("Syncing popularity")
do_compute_popularity(ctx, offset: 0)
|> function.tap(fn(_) { wisp.log_info("Syncing package ranks finished!") })
case ctx.env {
config.Development -> Ok(Nil)
config.Production -> {
wisp.log_info("Syncing popularity")
do_compute_popularity(ctx, offset: 0)
|> function.tap(fn(_) { wisp.log_info("Syncing popularity finished!") })
}
}
}

fn do_compute_popularity(ctx: Context, offset offset: Int) {
Expand Down
15 changes: 14 additions & 1 deletion apps/frontend/src/data/model.gleam
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import data/msg.{type Msg}
import data/package.{type Package}
import data/search_result.{type SearchResult, type SearchResults}
import frontend/router
import frontend/view/body/cache
import gleam/list
import gleam/option.{type Option}
import gleam/pair
import gleam/result
import lustre/element.{type Element}
Expand All @@ -18,6 +20,7 @@ pub type Model {
loading: Bool,
view_cache: Element(Msg),
route: router.Route,
trendings: Option(List(Package)),
)
}

Expand All @@ -31,13 +34,22 @@ pub fn init() {
loading: False,
view_cache: element.none(),
route: router.Home,
trendings: option.None,
)
}

pub fn update_route(model: Model, route: router.Route) {
Model(..model, route: route)
}

pub fn update_trendings(model: Model, trendings: List(Package)) {
model.trendings
|> option.unwrap([])
|> list.append(trendings)
|> option.Some
|> fn(t) { Model(..model, trendings: t) }
}

pub fn toggle_loading(model: Model) {
Model(..model, loading: !model.loading)
}
Expand All @@ -61,14 +73,15 @@ pub fn update_search_results(model: Model, search_results: SearchResults) {
)
}

pub fn reset(_model: Model) {
pub fn reset(model: Model) {
Model(
search_results: search_result.SearchResults([], [], [], [], []),
input: "",
index: [],
loading: False,
view_cache: element.none(),
route: router.Home,
trendings: model.trendings,
)
}

Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/data/msg.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import data/package
import data/search_result.{type SearchResults}
import frontend/router
import lustre_http as http
Expand All @@ -6,6 +7,7 @@ pub type Msg {
None
SubmitSearch
SearchResults(input: String, result: Result(SearchResults, http.HttpError))
Trendings(result: Result(List(package.Package), http.HttpError))
UpdateInput(String)
Reset
ScrollTo(String)
Expand Down
41 changes: 41 additions & 0 deletions apps/frontend/src/data/package.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import gleam/dynamic
import gleam/json
import gleam/option.{type Option}
import gleam/result

pub type Package {
Package(
name: String,
repository: Option(String),
documentation: Option(String),
hex_url: Option(String),
licenses: List(String),
description: Option(String),
rank: Option(Int),
popularity: Int,
)
}

pub fn decoder(dyn) {
dynamic.decode8(
Package,
dynamic.field("name", dynamic.string),
dynamic.field("repository", dynamic.optional(dynamic.string)),
dynamic.field("documentation", dynamic.optional(dynamic.string)),
dynamic.field("hex-url", dynamic.optional(dynamic.string)),
dynamic.field("licenses", fn(dyn) {
use data <- result.try(dynamic.optional(dynamic.string)(dyn))
option.unwrap(data, "[]")
|> json.decode(using: dynamic.list(dynamic.string))
|> result.replace_error([dynamic.DecodeError("", "", [])])
}),
dynamic.field("description", dynamic.optional(dynamic.string)),
dynamic.field("rank", dynamic.optional(dynamic.int)),
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))
|> result.replace_error([dynamic.DecodeError("", "", [])])
}),
)(dyn)
}
31 changes: 26 additions & 5 deletions apps/frontend/src/frontend.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import data/model.{type Model}
import data/msg.{type Msg}
import data/package
import data/search_result
import frontend/router
import frontend/view
import gleam/bool
import gleam/dynamic
import gleam/option.{None}
import gleam/pair
import gleam/result
Expand All @@ -30,6 +32,13 @@ fn scroll_to_element(id: String) -> Nil
@external(javascript, "./config.ffi.mjs", "captureMessage")
fn capture_message(content: String) -> String

pub fn api_endpoint() {
case is_dev() {
True -> "http://localhost:3000"
False -> "https://api.gloogle.run"
}
}

pub fn main() {
let debugger_ = case is_dev() {
False -> Error(Nil)
Expand Down Expand Up @@ -69,6 +78,10 @@ fn init(_) {
|> handle_route_change(model.init(), _)
submit_search(initial.0)
|> update.add_effect(modem.init(on_url_change))
|> update.add_effect(
http.expect_json(dynamic.list(package.decoder), msg.Trendings)
|> http.get(api_endpoint() <> "/trendings", _),
)
}

fn on_url_change(uri: Uri) -> Msg {
Expand All @@ -86,6 +99,7 @@ fn update(model: Model, msg: Msg) {
msg.OnRouteChange(route) -> handle_route_change(model, route)
msg.SearchResults(input, search_results) ->
handle_search_results(model, input, search_results)
msg.Trendings(trendings) -> handle_trendings(model, trendings)
}
}

Expand All @@ -102,17 +116,13 @@ fn reset(model: Model) {
}

fn submit_search(model: Model) {
let endpoint = case is_dev() {
True -> "http://localhost:3000"
False -> "https://api.gloogle.run"
}
use <- bool.guard(when: model.input == "", return: #(model, effect.none()))
use <- bool.guard(when: model.loading, return: #(model, effect.none()))
let new_model = model.toggle_loading(model)
http.expect_json(search_result.decode_search_results, {
msg.SearchResults(input: model.input, result: _)
})
|> http.get(endpoint <> "/search?q=" <> model.input, _)
|> http.get(api_endpoint() <> "/search?q=" <> model.input, _)
|> pair.new(new_model, _)
}

Expand Down Expand Up @@ -145,6 +155,7 @@ fn handle_route_change(model: Model, route: router.Route) {
update.none(case route {
router.Home -> model.update_input(model, "")
router.Search(q) -> model.update_input(model, q)
router.Trending -> model.update_input(model, "")
})
}

Expand All @@ -160,3 +171,13 @@ fn display_toast(
|> result.unwrap_error(option.None)
|> option.unwrap(effect.none())
}

fn handle_trendings(
model: Model,
trendings: Result(List(package.Package), http.HttpError),
) {
trendings
|> result.map(fn(trendings) { model.update_trendings(model, trendings) })
|> result.unwrap(model)
|> update.none()
}
2 changes: 2 additions & 0 deletions apps/frontend/src/frontend/router.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import gleam/uri.{type Uri}
pub type Route {
Home
Search(query: String)
Trending
}

pub fn parse_uri(uri: Uri) -> Route {
case uri.path_segments(uri.path) {
["search"] -> handle_search_path(uri)
["trending"] -> Trending
_ -> Home
}
}
Expand Down
Loading

0 comments on commit 1a143d4

Please sign in to comment.