diff --git a/apps/frontend/public/images/loading.svg b/apps/frontend/public/images/loading.svg new file mode 100644 index 0000000..1b5dd87 --- /dev/null +++ b/apps/frontend/public/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 246ad56..e98063f 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -1,5 +1,6 @@ import data/msg.{type Msg} import data/search_result.{type SearchResult, type SearchResults} +import frontend/router import frontend/view/body/cache import gleam/list import gleam/pair @@ -16,6 +17,7 @@ pub type Model { index: Index, loading: Bool, view_cache: Element(Msg), + route: router.Route, ) } @@ -28,9 +30,14 @@ pub fn init() { index: index, loading: False, view_cache: element.none(), + route: router.Home, ) } +pub fn update_route(model: Model, route: router.Route) { + Model(..model, route: route) +} + pub fn toggle_loading(model: Model) { Model(..model, loading: !model.loading) } @@ -42,7 +49,7 @@ pub fn update_input(model: Model, content: String) { pub fn update_search_results(model: Model, search_results: SearchResults) { let index = compute_index(search_results) let view_cache = case search_results { - search_result.Start | search_result.NoSearchResults -> element.none() + search_result.Start | search_result.InternalServerError -> element.none() search_result.SearchResults(e, m, s) -> cache.cache_search_results(index, e, m, s) } @@ -56,18 +63,18 @@ pub fn update_search_results(model: Model, search_results: SearchResults) { pub fn reset(_model: Model) { Model( - search_results: search_result.Start, + search_results: search_result.SearchResults([], [], []), input: "", index: [], loading: False, view_cache: element.none(), + route: router.Home, ) } fn compute_index(search_results: SearchResults) -> Index { case search_results { - search_result.Start -> [] - search_result.NoSearchResults -> [] + search_result.Start | search_result.InternalServerError -> [] search_result.SearchResults(exact, others, searches) -> { [] |> insert_module_names(exact) diff --git a/apps/frontend/src/data/msg.gleam b/apps/frontend/src/data/msg.gleam index 3a690f2..4a180c2 100644 --- a/apps/frontend/src/data/msg.gleam +++ b/apps/frontend/src/data/msg.gleam @@ -1,11 +1,13 @@ import data/search_result.{type SearchResults} +import frontend/router import lustre_http as http pub type Msg { None SubmitSearch - SearchResults(Result(SearchResults, http.HttpError)) + SearchResults(input: String, result: Result(SearchResults, http.HttpError)) UpdateInput(String) Reset ScrollTo(String) + OnRouteChange(router.Route) } diff --git a/apps/frontend/src/data/search_result.gleam b/apps/frontend/src/data/search_result.gleam index 663e660..c020729 100644 --- a/apps/frontend/src/data/search_result.gleam +++ b/apps/frontend/src/data/search_result.gleam @@ -19,12 +19,12 @@ pub type SearchResult { pub type SearchResults { Start + InternalServerError SearchResults( exact_matches: List(SearchResult), matches: List(SearchResult), searches: List(SearchResult), ) - NoSearchResults } pub fn decode_search_result(dyn) { @@ -43,7 +43,7 @@ pub fn decode_search_result(dyn) { pub fn decode_search_results(dyn) { dynamic.any([ - dynamic.decode1(fn(_) { NoSearchResults }, { + dynamic.decode1(fn(_) { InternalServerError }, { dynamic.field("error", dynamic.string) }), dynamic.decode3( diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index 8ab2f7d..d04811e 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -1,11 +1,13 @@ import data/model.{type Model} import data/msg.{type Msg} import data/search_result +import frontend/router import frontend/view import gleam/bool -import gleam/option +import gleam/option.{None} import gleam/pair import gleam/result +import gleam/uri.{type Uri} import grille_pain import grille_pain/lustre/toast import grille_pain/options @@ -13,6 +15,7 @@ import lustre import lustre/effect import lustre/update import lustre_http as http +import modem import sketch/lustre as sketch import sketch/options as sketch_options import tardis @@ -28,8 +31,6 @@ fn scroll_to_element(id: String) -> Nil fn capture_message(content: String) -> String pub fn main() { - let init = fn(_) { #(model.init(), effect.none()) } - let debugger_ = case is_dev() { False -> Error(Nil) True -> @@ -60,6 +61,21 @@ pub fn main() { |> apply_debugger(tardis.activate) } +fn init(_) { + let initial = + modem.initial_uri() + |> result.map(router.parse_uri) + |> result.unwrap(router.Home) + |> handle_route_change(model.init(), _) + submit_search(initial.0) + |> update.add_effect(modem.init(on_url_change)) +} + +fn on_url_change(uri: Uri) -> Msg { + router.parse_uri(uri) + |> msg.OnRouteChange() +} + fn update(model: Model, msg: Msg) { case msg { msg.UpdateInput(content) -> update_input(model, content) @@ -67,8 +83,9 @@ fn update(model: Model, msg: Msg) { msg.Reset -> reset(model) msg.None -> update.none(model) msg.ScrollTo(id) -> scroll_to(model, id) - msg.SearchResults(search_results) -> - handle_search_results(model, search_results) + msg.OnRouteChange(route) -> handle_route_change(model, route) + msg.SearchResults(input, search_results) -> + handle_search_results(model, input, search_results) } } @@ -92,7 +109,9 @@ fn submit_search(model: Model) { 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) + http.expect_json(search_result.decode_search_results, { + msg.SearchResults(input: model.input, result: _) + }) |> http.get(endpoint <> "/search?q=" <> model.input, _) |> pair.new(new_model, _) } @@ -104,14 +123,29 @@ fn scroll_to(model: Model, id: String) { fn handle_search_results( model: Model, + input: String, search_results: Result(search_result.SearchResults, http.HttpError), ) { let toast = display_toast(search_results) - search_results - |> result.map(model.update_search_results(model, _)) - |> result.unwrap(model) - |> model.toggle_loading() - |> pair.new(toast) + let up = + search_results + |> result.map(model.update_search_results(model, _)) + |> result.map(model.update_route(_, router.Search(input))) + |> result.unwrap(model) + |> model.toggle_loading() + |> update.effect(toast) + uri.parse("/search?q=" <> input) + |> result.map(modem.push(_)) + |> result.map(update.add_effect(up, _)) + |> result.unwrap(up) +} + +fn handle_route_change(model: Model, route: router.Route) { + let model = model.update_route(model, route) + update.none(case route { + router.Home -> model.update_input(model, "") + router.Search(q) -> model.update_input(model, q) + }) } fn display_toast( diff --git a/apps/frontend/src/frontend/images.gleam b/apps/frontend/src/frontend/images.gleam index bce4bd9..52f75ea 100644 --- a/apps/frontend/src/frontend/images.gleam +++ b/apps/frontend/src/frontend/images.gleam @@ -1,3 +1,5 @@ pub const internal_error = "/images/internal_error.png" pub const shadow_lucy = "/images/shadow_lucy.png" + +pub const loading = "/images/loading.svg" diff --git a/apps/frontend/src/frontend/router.gleam b/apps/frontend/src/frontend/router.gleam new file mode 100644 index 0000000..1ca48c6 --- /dev/null +++ b/apps/frontend/src/frontend/router.gleam @@ -0,0 +1,29 @@ +import gleam/list +import gleam/option +import gleam/result +import gleam/uri.{type Uri} + +pub type Route { + Home + Search(query: String) +} + +pub fn parse_uri(uri: Uri) -> Route { + case uri.path_segments(uri.path) { + ["search"] -> handle_search_path(uri) + _ -> Home + } +} + +fn handle_search_path(uri: Uri) { + uri.query + |> option.map(uri.parse_query) + |> option.then(fn(query_params) { + query_params + |> result.unwrap([]) + |> list.key_find("q") + |> option.from_result() + }) + |> option.map(Search) + |> option.unwrap(Home) +} diff --git a/apps/frontend/src/frontend/strings.gleam b/apps/frontend/src/frontend/strings.gleam index 85eb4ff..f4c2143 100644 --- a/apps/frontend/src/frontend/strings.gleam +++ b/apps/frontend/src/frontend/strings.gleam @@ -9,3 +9,5 @@ pub const searches_match = "Matched from a document search in functions, constan pub const retry_query = "Retry with a different query. You can match functions, types or constants names, as well as functions types directly!" pub const internal_server_error = "Internal server error. The error should be fixed soon. Please, retry later." + +pub const loading = "Your content is loading. Please wait…" diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index 505f0cc..b56deab 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -2,6 +2,7 @@ import data/model.{type Model} import data/msg import data/search_result import frontend/images +import frontend/router import frontend/strings as frontend_strings import frontend/view/body/styles as s import lustre/attribute as a @@ -49,21 +50,30 @@ fn empty_state( pub fn body(model: Model) { s.main([], [ - case model.search_results { - search_result.Start -> view_search_input(model) - search_result.NoSearchResults -> - empty_state( - image: images.internal_error, - title: "Internal server error", - content: frontend_strings.internal_server_error, - ) - search_result.SearchResults([], [], []) -> - empty_state( - image: images.shadow_lucy, - title: "No match found!", - content: frontend_strings.retry_query, - ) - search_result.SearchResults(_, _, _) -> model.view_cache + case model.route { + router.Home -> view_search_input(model) + router.Search(_) -> + case model.search_results { + search_result.Start -> + empty_state( + image: images.loading, + title: "Loading…", + content: frontend_strings.loading, + ) + search_result.InternalServerError -> + empty_state( + image: images.internal_error, + title: "Internal server error", + content: frontend_strings.internal_server_error, + ) + search_result.SearchResults([], [], []) -> + empty_state( + image: images.shadow_lucy, + title: "No match found!", + content: frontend_strings.retry_query, + ) + search_result.SearchResults(_, _, _) -> model.view_cache + } }, ]) } diff --git a/apps/frontend/src/frontend/view/navbar/navbar.gleam b/apps/frontend/src/frontend/view/navbar/navbar.gleam index 71e438a..72a04fc 100644 --- a/apps/frontend/src/frontend/view/navbar/navbar.gleam +++ b/apps/frontend/src/frontend/view/navbar/navbar.gleam @@ -1,6 +1,6 @@ import data/model.{type Model} import data/msg -import data/search_result +import frontend/router import frontend/view/navbar/styles as s import lustre/attribute as a import lustre/element/html as h @@ -21,11 +21,11 @@ fn navbar_links() { pub fn navbar(model: Model) { s.navbar([a.class("navbar")], [ - case model.search_results { - search_result.Start -> h.div([], []) - search_result.NoSearchResults | search_result.SearchResults(_, _, _) -> + case model.route { + router.Home -> h.div([], []) + router.Search(_) -> s.navbar_search([], [ - s.navbar_search_title([e.on_click(msg.Reset)], [ + s.navbar_search_title([a.href("/")], [ s.search_lucy([a.src("/images/lucy.svg")]), h.text("Gloogle"), ]), diff --git a/apps/frontend/src/lustre/update.gleam b/apps/frontend/src/lustre/update.gleam index a275c44..27ecdb2 100644 --- a/apps/frontend/src/lustre/update.gleam +++ b/apps/frontend/src/lustre/update.gleam @@ -4,6 +4,14 @@ pub fn none(model: model) { #(model, effect.none()) } +pub fn effect(model: model, effect: Effect(msg)) { + #(model, effect) +} + +pub fn effects(model: model, effects: List(Effect(msg))) { + #(model, effect.batch(effects)) +} + pub fn add_effect(tuple: #(model, Effect(msg)), effect: Effect(msg)) { let #(model, fst_effect) = tuple #(model, effect.batch([fst_effect, effect]))