diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 1f0afc3..3b0d0c4 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -25,6 +25,7 @@ pub type Model { loading: Bool, view_cache: Dict(String, Element(Msg)), route: router.Route, + is_mobile: Bool, trendings: Option(List(Package)), submitted_input: String, keep_functions: Bool, @@ -37,6 +38,9 @@ pub type Model { ) } +@external(javascript, "../gloogle.ffi.mjs", "isMobile") +fn is_mobile() -> Bool + pub fn init() { let search_results = search_result.Start let index = compute_index(search_results) @@ -47,6 +51,7 @@ pub fn init() { loading: False, view_cache: dict.new(), route: router.Home, + is_mobile: is_mobile(), trendings: option.None, submitted_input: "", keep_functions: False, @@ -67,6 +72,10 @@ pub fn update_submitted_input(model: Model) { Model(..model, submitted_input: model.input) } +pub fn update_is_mobile(model: Model, is_mobile: Bool) { + Model(..model, is_mobile: is_mobile) +} + pub fn update_trendings(model: Model, trendings: List(Package)) { model.trendings |> option.unwrap([]) @@ -295,6 +304,7 @@ pub fn reset(model: Model) { loading: False, view_cache: model.view_cache, route: router.Home, + is_mobile: is_mobile(), trendings: model.trendings, submitted_input: "", keep_functions: False, diff --git a/apps/frontend/src/data/msg.gleam b/apps/frontend/src/data/msg.gleam index 9062e5d..81401a4 100644 --- a/apps/frontend/src/data/msg.gleam +++ b/apps/frontend/src/data/msg.gleam @@ -15,12 +15,15 @@ pub type Filter { pub type Msg { None + OnSearchFocus SubmitSearch + UpdateIsMobile(is_mobile: Bool) SearchResults(input: String, result: Result(SearchResults, http.HttpError)) Trendings(result: Result(List(package.Package), http.HttpError)) UpdateInput(String) Reset ScrollTo(String) + OnEscape OnRouteChange(router.Route) OnCheckFilter(Filter, Bool) } diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index b8490c4..d8ae206 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -4,6 +4,7 @@ import data/package import data/search_result import frontend/config import frontend/errors +import frontend/ffi import frontend/router import frontend/view import frontend/view/body/search_result as sr @@ -26,15 +27,29 @@ import sketch/lustre as sketch import sketch/options as sketch_options import toast/error as toast_error -@external(javascript, "./config.ffi.mjs", "scrollTo") -fn scroll_to_element(id: String) -> fn(dispatch) -> Nil +fn focus(on id: String) { + use _ <- effect.from() + ffi.focus(on: id) +} -@external(javascript, "./config.ffi.mjs", "subscribeFocus") -fn do_subscribe_focus() -> Nil +fn unfocus() { + use _ <- effect.from() + ffi.unfocus() +} fn subscribe_focus() { - use _ <- effect.from() - do_subscribe_focus() + use dispatch <- effect.from() + use key <- ffi.subscribe_focus() + case key { + "Escape" -> dispatch(msg.OnEscape) + _ -> dispatch(msg.OnSearchFocus) + } +} + +fn subscribe_is_mobile() { + use dispatch <- effect.from() + use is_mobile <- ffi.suscribe_is_mobile() + dispatch(msg.UpdateIsMobile(is_mobile)) } pub fn main() { @@ -63,6 +78,7 @@ fn init(_) { |> update.add_effect(modem.init(on_url_change)) |> update.add_effect(router.update_page_title({ initial.0 }.route)) |> update.add_effect(subscribe_focus()) + |> update.add_effect(subscribe_is_mobile()) |> update.add_effect( http.expect_json(dynamic.list(package.decoder), msg.Trendings) |> http.get(config.api_endpoint() <> "/trendings", _), @@ -83,6 +99,12 @@ fn update(model: Model, msg: Msg) { msg.ScrollTo(id) -> scroll_to(model, id) msg.OnRouteChange(route) -> handle_route_change(model, route) msg.Trendings(trendings) -> handle_trendings(model, trendings) + msg.OnSearchFocus -> update.effect(model, focus(on: "search-input")) + msg.OnEscape -> update.effect(model, unfocus()) + msg.UpdateIsMobile(is_mobile) -> + model + |> model.update_is_mobile(is_mobile) + |> update.none msg.SearchResults(input, search_results) -> handle_search_results(model, input, search_results) msg.OnCheckFilter(filter, value) -> @@ -141,7 +163,7 @@ fn submit_search(model: Model) { } fn scroll_to(model: Model, id: String) { - scroll_to_element(id) + ffi.scroll_to(element: id) |> effect.from |> update.effect(model, _) } diff --git a/apps/frontend/src/frontend/errors.gleam b/apps/frontend/src/frontend/errors.gleam index 37d9611..5220981 100644 --- a/apps/frontend/src/frontend/errors.gleam +++ b/apps/frontend/src/frontend/errors.gleam @@ -1,2 +1,2 @@ -@external(javascript, "../config.ffi.mjs", "captureMessage") +@external(javascript, "../gloogle.ffi.mjs", "captureMessage") pub fn capture_message(content: String) -> String diff --git a/apps/frontend/src/frontend/ffi.gleam b/apps/frontend/src/frontend/ffi.gleam new file mode 100644 index 0000000..44ff2a1 --- /dev/null +++ b/apps/frontend/src/frontend/ffi.gleam @@ -0,0 +1,20 @@ +@external(javascript, "../gloogle.ffi.mjs", "scrollTo") +pub fn scroll_to(element id: String) -> fn(dispatch) -> Nil + +@external(javascript, "../gloogle.ffi.mjs", "subscribeIsMobile") +pub fn suscribe_is_mobile(callback: fn(Bool) -> Nil) -> Nil + +@external(javascript, "../gloogle.ffi.mjs", "subscribeFocus") +pub fn subscribe_focus(callback: fn(String) -> Nil) -> Nil + +@external(javascript, "../gloogle.ffi.mjs", "focus") +pub fn focus(on id: String) -> Nil + +@external(javascript, "../gloogle.ffi.mjs", "unfocus") +pub fn unfocus() -> Nil + +@external(javascript, "../gloogle.ffi.mjs", "isMac") +pub fn is_mac() -> Bool + +@external(javascript, "../gloogle.ffi.mjs", "updateTitle") +pub fn update_title(title: String) -> Nil diff --git a/apps/frontend/src/frontend/router.gleam b/apps/frontend/src/frontend/router.gleam index 5930d8e..00080fd 100644 --- a/apps/frontend/src/frontend/router.gleam +++ b/apps/frontend/src/frontend/router.gleam @@ -1,12 +1,10 @@ +import frontend/ffi import gleam/list import gleam/option import gleam/result import gleam/uri.{type Uri} import lustre/effect -@external(javascript, "../config.ffi.mjs", "updateTitle") -fn update_title(title: String) -> Nil - pub type Route { Home Search(query: String) @@ -37,8 +35,8 @@ fn handle_search_path(uri: Uri) { pub fn update_page_title(route: Route) { use _ <- effect.from() case route { - Home -> update_title("Gloogle") - Search(q) -> update_title("Gloogle — Search " <> q) - Trending -> update_title("Gloogle — Trending") + Home -> ffi.update_title("Gloogle") + Search(q) -> ffi.update_title("Gloogle — Search " <> q) + Trending -> ffi.update_title("Gloogle — Trending") } } diff --git a/apps/frontend/src/frontend/view/search_input/search_input.gleam b/apps/frontend/src/frontend/view/search_input/search_input.gleam index 9b2af75..df3bdf6 100644 --- a/apps/frontend/src/frontend/view/search_input/search_input.gleam +++ b/apps/frontend/src/frontend/view/search_input/search_input.gleam @@ -1,14 +1,12 @@ import data/msg +import frontend/ffi import frontend/view/search_input/styles as s import lustre/attribute as a import lustre/element as el import lustre/event as e -@external(javascript, "../../../config.ffi.mjs", "isMac") -fn is_mac() -> Bool - pub fn view(loading loading: Bool, input input: String, small small: Bool) { - let modifier = case is_mac() { + let modifier = case ffi.is_mac() { True -> "Cmd" False -> "Ctrl" } diff --git a/apps/frontend/src/frontend/view/search_input/styles.gleam b/apps/frontend/src/frontend/view/search_input/styles.gleam index aed4d8c..a739886 100644 --- a/apps/frontend/src/frontend/view/search_input/styles.gleam +++ b/apps/frontend/src/frontend/view/search_input/styles.gleam @@ -81,5 +81,6 @@ pub fn shortcut_hint(attrs, children) { s.padding_("3px 6px"), s.border_radius(px(6)), s.opacity(0.4), + s.media(media.max_width(px(700)), [s.display("none")]), ]) } diff --git a/apps/frontend/src/gleam/coerce.gleam b/apps/frontend/src/gleam/coerce.gleam index 198f193..3f1da9e 100644 --- a/apps/frontend/src/gleam/coerce.gleam +++ b/apps/frontend/src/gleam/coerce.gleam @@ -1,2 +1,2 @@ -@external(javascript, "../config.ffi.mjs", "coerce") +@external(javascript, "../gloogle.ffi.mjs", "coerce") pub fn coerce(value: a) -> b diff --git a/apps/frontend/src/config.ffi.mjs b/apps/frontend/src/gloogle.ffi.mjs similarity index 67% rename from apps/frontend/src/config.ffi.mjs rename to apps/frontend/src/gloogle.ffi.mjs index 706abe6..4397ebe 100644 --- a/apps/frontend/src/config.ffi.mjs +++ b/apps/frontend/src/gloogle.ffi.mjs @@ -44,19 +44,45 @@ export function coerceEvent(a) { return a.detail } -export function subscribeFocus() { +export function subscribeFocus(callback) { document.addEventListener('keydown', event => { + if (event.key === 'Escape') return callback(event.key) if ((!event.metaKey && !event.ctrlKey) || event.key !== 'k') return - const element = document.getElementById('search-input') - if (element) { - element.focus() - element.select() - } + callback(event.key) }) } +export function focus(id) { + const element = document.getElementById(id) + if (element) { + element.focus() + element.select() + } +} + +export function unfocus() { + const element = document.activeElement + if (element) { + element.blur() + } +} + export function isMac() { return ( navigator.platform.indexOf('Mac') === 0 || navigator.platform === 'iPhone' ) } + +export function isMobile() { + return window.matchMedia('(max-width: 700px)').matches +} + +export function subscribeIsMobile(callback) { + window.matchMedia('(max-width: 700px)').addEventListener('change', event => { + if (event.matches) { + callback(true) + } else { + callback(false) + } + }) +} diff --git a/apps/frontend/src/lustre/lazy.gleam b/apps/frontend/src/lustre/lazy.gleam index f1d0130..53462d8 100644 --- a/apps/frontend/src/lustre/lazy.gleam +++ b/apps/frontend/src/lustre/lazy.gleam @@ -6,7 +6,7 @@ import lustre/effect import lustre/element.{type Element} import lustre/event -@external(javascript, "../config.ffi.mjs", "coerceEvent") +@external(javascript, "../gloogle.ffi.mjs", "coerceEvent") fn coerce_event(a: a) -> b const tag_name = "lazy-node"