diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index 106aa7e..43451d4 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -145,122 +145,16 @@ fn sidebar(model: Model) { ]), h.div([a.class("sidebar-filter")], [el.text("Filters")]), h.div([a.class("sidebar-filters")], [ - h.label([a.class("sidebar-filter-line")], [ - el.fragment([ - h.div( - [ - a.class("sidebar-checkbox-1"), - a.style([ - #("background", case model.keep_functions { - True -> "#ffaff3" - False -> "var(--input-background)" - }), - ]), - ], - [], - ), - h.input([ - a.class("sidebar-checkbox-2"), - a.type_("checkbox"), - a.checked(model.keep_functions), - e.on_check(msg.OnCheckFilter(msg.Functions, _)), - ]), - ]), - h.div([a.class("sidebar-filter-name")], [el.text("Functions")]), - ]), - h.label([a.class("sidebar-filter-line")], [ - el.fragment([ - h.div( - [ - a.class("sidebar-checkbox-1"), - a.style([ - #("background", case model.keep_types { - True -> "#ffaff3" - False -> "var(--input-background)" - }), - ]), - ], - [], - ), - h.input([ - a.class("sidebar-checkbox-2"), - a.type_("checkbox"), - a.checked(model.keep_types), - e.on_check(msg.OnCheckFilter(msg.Types, _)), - ]), - ]), - h.div([a.class("sidebar-filter-name")], [el.text("Types")]), - ]), - h.label([a.class("sidebar-filter-line")], [ - el.fragment([ - h.div( - [ - a.class("sidebar-checkbox-1"), - a.style([ - #("background", case model.keep_aliases { - True -> "#ffaff3" - False -> "var(--input-background)" - }), - ]), - ], - [], - ), - h.input([ - a.class("sidebar-checkbox-2"), - a.type_("checkbox"), - a.checked(model.keep_aliases), - e.on_check(msg.OnCheckFilter(msg.Aliases, _)), - ]), - ]), - h.div([a.class("sidebar-filter-name")], [el.text("Aliases")]), - ]), + checkbox(model.keep_functions, msg.Functions, "Functions"), + checkbox(model.keep_types, msg.Types, "Types"), + checkbox(model.keep_aliases, msg.Aliases, "Aliases"), h.div([a.class("filter-separator")], []), - h.label([a.class("sidebar-filter-line")], [ - el.fragment([ - h.div( - [ - a.class("sidebar-checkbox-1"), - a.style([ - #("background", case model.keep_documented { - True -> "#ffaff3" - False -> "var(--input-background)" - }), - ]), - ], - [], - ), - h.input([ - a.class("sidebar-checkbox-2"), - a.type_("checkbox"), - a.checked(model.keep_documented), - e.on_check(msg.OnCheckFilter(msg.Documented, _)), - ]), - ]), - h.div([a.class("sidebar-filter-name")], [el.text("Documented")]), - ]), - h.label([a.class("sidebar-filter-line")], [ - el.fragment([ - h.div( - [ - a.class("sidebar-checkbox-1"), - a.style([ - #("background", case model.show_old_packages { - True -> "#ffaff3" - False -> "var(--input-background)" - }), - ]), - ], - [], - ), - h.input([ - a.class("sidebar-checkbox-2"), - a.type_("checkbox"), - a.checked(model.show_old_packages), - e.on_check(msg.OnCheckFilter(msg.ShowOldPackages, _)), - ]), - ]), - h.div([a.class("sidebar-filter-name")], [el.text("Show old versions")]), - ]), + checkbox(model.keep_documented, msg.Documented, "Documented"), + checkbox( + model.show_old_packages, + msg.ShowOldPackages, + "Show old versions", + ), ]), h.div([a.class("sidebar-spacer")], []), h.div([a.class("sidebar-links")], [ @@ -276,22 +170,42 @@ fn sidebar(model: Model) { // s.sidebar_icon([], [icons.gift()]), // s.sidebar_link([], [el.text("Resources")]), // ]), - h.a( - [ - a.class("sidebar-link-wrapper"), - a.href("https://github.com/sponsors/ghivert"), - a.target("_blank"), - a.rel("noreferrer noopener"), - ], - [ - h.div([a.class("sidebar-icon")], [icons.heart()]), - h.div([a.class("sidebar-link")], [el.text("Sponsor")]), - ], + sidebar_link( + href: "https://github.com/sponsors/ghivert", + title: "Sponsor", + icon: icons.heart(), ), ]), ]) } +fn checkbox(active: Bool, msg: msg.Filter, name: String) { + let bg = case active { + True -> "#ffaff3" + False -> "var(--input-background)" + } + h.label([a.class("sidebar-filter-line")], [ + el.fragment([ + h.div([a.class("sidebar-checkbox-1"), a.style([#("background", bg)])], []), + h.input([ + a.class("sidebar-checkbox-2"), + a.type_("checkbox"), + a.checked(active), + e.on_check(msg.OnCheckFilter(msg, _)), + ]), + ]), + h.div([a.class("sidebar-filter-name")], [el.text(name)]), + ]) +} + +fn sidebar_link(href href: String, title title: String, icon icon) { + let class = a.class("sidebar-link-wrapper") + h.a([class, a.href(href), a.target("_blank"), a.rel("noreferrer noopener")], [ + h.div([a.class("sidebar-icon")], [icon]), + h.div([a.class("sidebar-link")], [el.text(title)]), + ]) +} + pub fn body(model: Model) { case model.route { router.Home -> h.main([a.class("main")], [view_search_input(model)]) diff --git a/apps/frontend/src/frontend/view/body/cache.gleam b/apps/frontend/src/frontend/view/body/cache.gleam index 316d29e..af3b150 100644 --- a/apps/frontend/src/frontend/view/body/cache.gleam +++ b/apps/frontend/src/frontend/view/body/cache.gleam @@ -1,35 +1,17 @@ -import data/implementations -import data/kind import data/msg import data/search_result -import frontend/colors/palette -import frontend/icons -import frontend/strings as frontend_strings -import frontend/view/body/signature -import frontend/view/documentation +import frontend/view/body/search_result as sr import frontend/view/types as t -import gleam/bool -import gleam/coerce.{coerce} -import gleam/dict -import gleam/io import gleam/list -import gleam/option -import lustre import lustre/attribute as a -import lustre/effect as eff import lustre/element as el import lustre/element/html as h import lustre/event as e -import sketch/lustre as sketch_lustre -import sketch/options as sketch_options fn view_search_results(search_results: List(search_result.SearchResult)) { - el.fragment({ - list.map(search_results, fn(item) { - el.element("search-result", [a.property("item", item)], []) - }) - |> list.intersperse(h.div([a.class("search-result-separator")], [])) - }) + list.map(search_results, sr.view) + |> list.intersperse(h.div([a.class("search-result-separator")], [])) + |> el.fragment } fn sidebar( diff --git a/apps/frontend/src/frontend/view/body/search_result.gleam b/apps/frontend/src/frontend/view/body/search_result.gleam index 821fa3c..59ea5b9 100644 --- a/apps/frontend/src/frontend/view/body/search_result.gleam +++ b/apps/frontend/src/frontend/view/body/search_result.gleam @@ -1,11 +1,12 @@ import data/implementations -import data/search_result +import data/search_result.{type SearchResult} import frontend/colors/palette import frontend/icons import frontend/view/body/signature import frontend/view/documentation import frontend/view/types as t import gleam/bool +import gleam/coerce import gleam/dict import gleam/dynamic import gleam/list @@ -17,14 +18,52 @@ import lustre/effect import lustre/element import lustre/element/html as h import lustre/event as e +import lustre/update + +pub type Model { + Model(item: option.Option(SearchResult), opened: Bool) +} + +pub type Msg { + Received(option.Option(SearchResult)) + ToggleOpen +} + +fn on_attribute_change() { + let on_coerce = fn(dyn) { Ok(coerce.coerce(dyn)) } |> dynamic.optional + dict.from_list([#("item", fn(dyn) { on_coerce(dyn) |> result.map(Received) })]) +} pub fn setup() { let attrs = on_attribute_change() - lustre.component(init, update, view, attrs) + let init = fn(_) { #(Model(item: option.None, opened: False), effect.none()) } + lustre.component(init, update, internal_view, attrs) |> lustre.register("search-result") } -fn implementations_pill(implementations: implementations.Implementations) { +pub fn view(item: SearchResult) { + let attributes = [a.property("item", item)] + element.element("search-result", attributes, []) +} + +fn update(model, msg) { + case msg { + ToggleOpen -> Model(..model, opened: !model.opened) + Received(search_result) -> Model(item: search_result, opened: False) + } + |> update.none +} + +fn implementation_pill(item) { + let #(content, _, background, _) = item + let style = a.style([#("background", background)]) + h.div([a.class("implementations-pill-container")], [ + h.div([a.class("implementations-pill"), style], []), + h.text(content), + ]) +} + +fn implementation_pills(implementations: implementations.Implementations) { case implementations { implementations.Implementations(True, False, False) -> element.none() implementations.Implementations(gleam, erl, js) -> @@ -34,129 +73,74 @@ fn implementations_pill(implementations: implementations.Implementations) { #("JavaScript", js, palette.javascript, palette.dark.blacker), ] |> list.filter(fn(item) { item.1 }) - |> list.map(fn(item) { - let #(content, _, background, _) = item - h.div([a.class("implementations-pill-container")], [ - h.div( - [ - a.class("implementations-pill"), - a.style([#("background", background)]), - ], - [], - ), - h.text(content), - ]) - }) + |> list.map(implementation_pill) |> h.div([a.class("implementations-pill-wrapper")], _) } } -pub type Model { - Model(item: option.Option(search_result.SearchResult), opened: Bool) -} - -pub type Msg { - Received(option.Option(search_result.SearchResult)) - ToggleOpen +fn internal_view(model: Model) -> element.Element(Msg) { + use <- bool.guard(when: option.is_none(model.item), return: element.none()) + let assert option.Some(item) = model.item + let package_id = item.package_name <> "@" <> item.version + let id = package_id <> "-" <> item.module_name <> "-" <> item.name + h.div([a.class("search-result"), a.id(id)], [ + h.div([a.class("search-details")], [ + h.div([a.class("search-details-name")], [ + view_name(item), + h.div([a.class("external-icon-wrapper")], [icons.external_link()]), + ]), + view_documentation_arrow(model, item), + ]), + h.div([a.class("search-body")], [ + h.code([a.class("signature")], signature.view_signature(item)), + ]), + view_implementation_pills(model, item), + view_documentation(model, item), + ]) } -fn init(_) { - #(Model(item: option.None, opened: False), effect.none()) +fn view_name(item: SearchResult) { + let class = a.class("qualified-name") + let href = search_result.hexdocs_link(item) + h.a([class, a.target("_blank"), a.rel("noreferrer"), a.href(href)], [ + t.white(item.package_name), + t.dark_white("@" <> item.version), + t.dark_white("."), + t.keyword(item.module_name), + t.dark_white("."), + t.fun(item.name), + ]) } -fn update(model, msg) { - case msg { - ToggleOpen -> #(Model(..model, opened: !model.opened), effect.none()) - Received(search_result) -> #( - Model(..model, item: search_result, opened: False), - effect.none(), - ) - } +fn view_documentation_arrow(model: Model, item: SearchResult) { + use <- bool.guard(when: item.documentation == "", return: element.none()) + let no_implementation = option.is_none(item.metadata.implementations) + use <- bool.guard(when: no_implementation, return: element.none()) + let class = a.class("search-details-arrow-expand") + let data_opened = a.attribute("data-opened", bool.to_string(model.opened)) + h.button([class, data_opened, e.on_click(ToggleOpen)], [ + h.span([], [ + element.text( + case model.opened { + True -> "Hide" + False -> "Show" + } + <> " documentation", + ), + ]), + icons.arrow(), + ]) } -fn view(model: Model) -> element.Element(Msg) { - case model.item { - option.None -> element.text("bloup") - option.Some(item) -> { - let package_id = item.package_name <> "@" <> item.version - let id = package_id <> "-" <> item.module_name <> "-" <> item.name - h.div([a.class("search-result"), a.id(id)], [ - h.div([a.class("search-details")], [ - h.div([a.class("search-details-name")], [ - h.a( - [ - a.class("qualified-name"), - a.target("_blank"), - a.rel("noreferrer"), - a.href(search_result.hexdocs_link(item)), - ], - [ - t.white(item.package_name), - t.dark_white("@" <> item.version), - t.dark_white("."), - t.keyword(item.module_name), - t.dark_white("."), - t.fun(item.name), - ], - ), - h.div([a.class("external-icon-wrapper")], [icons.external_link()]), - ]), - case item.documentation, item.metadata.implementations { - "", option.None -> element.none() - _, _ -> - h.button( - [ - a.class("search-details-arrow-expand"), - a.attribute("data-opened", bool.to_string(model.opened)), - e.on_click(ToggleOpen), - ], - [ - h.span([], [ - element.text( - case model.opened { - True -> "Hide" - False -> "Show" - } - <> " documentation", - ), - ]), - icons.arrow(), - ], - ) - }, - ]), - h.div([a.class("search-body")], [ - h.code([a.class("signature")], signature.view_signature(item)), - ]), - case model.opened { - False -> element.none() - True -> - item.metadata.implementations - |> option.map(implementations_pill) - |> option.unwrap(element.none()) - }, - case model.opened, item.documentation { - False, _ | True, "" -> element.none() - True, _ -> - h.div([a.class("documentation")], [ - documentation.view(item.documentation), - ]) - }, - ]) - } - } +fn view_implementation_pills(model: Model, item: SearchResult) { + use <- bool.guard(when: !model.opened, return: element.none()) + item.metadata.implementations + |> option.map(implementation_pills) + |> option.unwrap(element.none()) } -pub fn on_attribute_change() { - dict.from_list([ - #("item", { - fn(dyn) { - { - fn(dyn) { Ok(dynamic.unsafe_coerce(dyn)) } - |> dynamic.optional() - }(dyn) - |> result.map(Received) - } - }), - ]) +fn view_documentation(model: Model, item: SearchResult) { + use <- bool.guard(when: item.documentation == "", return: element.none()) + use <- bool.guard(when: !model.opened, return: element.none()) + h.div([a.class("documentation")], [documentation.view(item.documentation)]) }