diff --git a/apps/frontend/public/main.css b/apps/frontend/public/main.css index a26f7d4..de3ccca 100644 --- a/apps/frontend/public/main.css +++ b/apps/frontend/public/main.css @@ -46,7 +46,12 @@ body { } } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { margin: 0; } @@ -64,3 +69,17 @@ h1, h2, h3, h4, h5, h6 { a { color: var(--a-color); } + +@keyframes bg-spin { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 72ff14d..6215970 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -7,13 +7,22 @@ pub type Index = List(#(#(String, String), List(#(String, String)))) pub type Model { - Model(input: String, search_results: SearchResults, index: Index) + Model( + input: String, + search_results: SearchResults, + index: Index, + loading: Bool, + ) } pub fn init() { let search_results = search_result.Start let index = compute_index(search_results) - Model(input: "", search_results: search_results, index: index) + Model(input: "", search_results: search_results, index: index, loading: False) +} + +pub fn toggle_loading(model: Model) { + Model(..model, loading: !model.loading) } pub fn update_input(model: Model, content: String) { @@ -26,7 +35,12 @@ pub fn update_search_results(model: Model, search_results: SearchResults) { } pub fn reset(_model: Model) { - Model(search_results: search_result.Start, input: "", index: []) + Model( + search_results: search_result.Start, + input: "", + index: [], + loading: False, + ) } fn compute_index(search_results: SearchResults) -> Index { diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index 372fb56..8cbc1b1 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -87,9 +87,11 @@ fn submit_search(model: Model) { 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) |> http.get(endpoint <> "/search?q=" <> model.input, _) - |> pair.new(model, _) + |> pair.new(new_model, _) } fn scroll_to(model: Model, id: String) { @@ -105,6 +107,7 @@ fn handle_search_results( search_results |> result.map(model.update_search_results(model, _)) |> result.unwrap(model) + |> model.toggle_loading() |> pair.new(toast) } diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index fe79084..8585860 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -84,16 +84,23 @@ fn view_search_input(model: Model) { s.search_title_wrapper([], [ s.search_title([], [ s.search_lucy([a.src("/images/lucy.svg")]), - h.text("Gloogle"), + s.search_title_with_hint([], [ + h.text("Gloogle"), + s.pre_alpha_title([], [h.text("Pre Alpha")]), + ]), ]), h.text(frontend_strings.gloogle_description), ]), - s.search_input([ + s.search_input(model.loading, [ a.placeholder("Search for a function, or a type"), e.on_input(msg.UpdateInput), a.value(model.input), ]), - s.search_submit([a.type_("submit"), a.value("Submit")]), + s.search_submit([ + a.type_("submit"), + a.value("Submit"), + a.disabled(model.loading), + ]), ]) } diff --git a/apps/frontend/src/frontend/view/body/styles.gleam b/apps/frontend/src/frontend/view/body/styles.gleam index 795aee3..aea4c92 100644 --- a/apps/frontend/src/frontend/view/body/styles.gleam +++ b/apps/frontend/src/frontend/view/body/styles.gleam @@ -1,4 +1,6 @@ import frontend/colors/palette +import gleam/bool +import gleam/string import sketch as s import sketch/lustre/extra as l import sketch/size.{px} @@ -154,20 +156,45 @@ pub fn search_lucy(attributes) { l.memo("img", attributes, [], [s.width(px(40))]) } -pub fn search_input(attributes) { - l.memo("input", attributes, [], [ - s.grid_area("input"), - s.appearance("none"), - s.background(palette.dark.white), - s.border("none"), - s.padding(px(18)), +pub fn search_input(loading: Bool, attributes) { + let id = "search-input-wrapper-" <> bool.to_string(loading) + let id_ = "search-input-" <> bool.to_string(loading) + let content = + l.dynamic("input", attributes, [], id_, [ + s.appearance("none"), + s.border("none"), + s.padding( + px(case loading { + True -> 18 + False -> 22 + }), + ), + s.outline("none"), + s.color(palette.dark.charcoal), + s.width(size.percent(100)), + s.background(palette.dark.white), + s.border_radius(px(14)), + s.transition("padding .3s"), + ]) + + l.dynamic("div", [], [content], id, [ s.border_radius(px(18)), - s.transition("outline .3s"), - s.outline("2px solid transparent"), - s.color(palette.dark.charcoal), - s.active([s.outline("2px solid " <> palette.dark.faff_pink)]), - s.focus([s.outline("2px solid " <> palette.dark.faff_pink)]), - s.width(size.percent(100)), + s.overflow("hidden"), + s.grid_area("input"), + s.padding( + px(case loading { + True -> 4 + False -> 0 + }), + ), + s.background("linear-gradient(-45deg, #4ce7ff, #c651e5, #e3d8be, #4ce7ff)"), + s.property("background-size", "400% 400%"), + s.transition("padding .3s"), + s.animation("bg-spin 3s linear infinite"), + s.animation_play_state(case loading { + True -> "running" + False -> "paused" + }), ]) } @@ -188,6 +215,7 @@ pub fn search_submit(attributes) { s.transition("background .3s"), s.active([s.background(palette.dark.dark_faff_pink)]), s.focus([s.background(palette.dark.dark_faff_pink)]), + s.disabled([s.background(palette.dark.unexpected_aubergine)]), ]) } @@ -330,3 +358,11 @@ pub fn named_type_button(attributes, children) { s.hover([s.text_decoration("underline")]), ]) } + +pub fn search_title_with_hint(attributes, children) { + l.memo("div", attributes, children, [s.display("flex"), s.gap(px(12))]) +} + +pub fn pre_alpha_title(attributes, children) { + l.memo("div", attributes, children, [s.font_size(px(16))]) +} diff --git a/apps/frontend/src/frontend/view/navbar/navbar.gleam b/apps/frontend/src/frontend/view/navbar/navbar.gleam index 2531912..8a742df 100644 --- a/apps/frontend/src/frontend/view/navbar/navbar.gleam +++ b/apps/frontend/src/frontend/view/navbar/navbar.gleam @@ -30,7 +30,7 @@ pub fn navbar(model: Model) { h.text("Gloogle"), ]), s.search_input_wrapper([e.on_submit(msg.SubmitSearch)], [ - s.search_input([ + s.search_input(model.loading, [ a.placeholder("Search for a function, or a type"), e.on_input(msg.UpdateInput), a.value(model.input), diff --git a/apps/frontend/src/shodown-highlight.mjs b/apps/frontend/src/shodown-highlight.mjs deleted file mode 100644 index 26bcafc..0000000 --- a/apps/frontend/src/shodown-highlight.mjs +++ /dev/null @@ -1,86 +0,0 @@ -import hljs from 'highlight.js' -import { decode as decodeHtml } from 'html-encoder-decoder' -import showdown from 'showdown' - -const classAttr = 'class="' - -/** - * showdownHighlight - * Highlight the code in the showdown input. - * - * Examples: - * - * ```js - * let converter = new showdown.Converter({ - * extensions: [showdownHighlight] - * }) - * ``` - * - * Enable the classes in the `
` element: - * - * ```js - * let converter = new showdown.Converter({ - * extensions: [showdownHighlight({ pre: true })] - * }) - * ``` - * - * - * If you want to disable language [auto detection](https://highlightjs.org/usage/) - * feature of hljs, change `auto_detection` flag as `false`. With this option - * turned off, `showdown-highlight` will not process any codeblocks with no - * language specified. - * - * ```js - * let converter = new showdown.Converter({ - * extensions: [showdownHighlight({ auto_detection: false })] - * }) - * ``` - * - * @name showdownHighlight - * @function - */ -export function showdownHighlight({ pre = false, auto_detection = true } = {}) { - const filter = text => { - const params = { - left: '', - flags: 'g', - } - - const replacement = (wholeMatch, match, left, right) => { - match = decodeHtml(match) - - const lang = (left.match(/class=\"([^ \"]+)/) || [])[1] - - if (!lang && !auto_detection) { - return wholeMatch - } - - if (left.includes(classAttr)) { - const attrIndex = left.indexOf(classAttr) + classAttr.length - left = left.slice(0, attrIndex) + 'hljs ' + left.slice(attrIndex) - } else { - left = left.slice(0, -1) + ' class="hljs">' - } - - if (pre && lang) { - left = left.replace(']*>', - right: '
', ``) - } - - if (lang && hljs.getLanguage(lang)) { - return left + hljs.highlight(match, { language: lang }).value + right - } - - return left + hljs.highlightAuto(match).value + right - } - - return showdown.helper.replaceRecursiveRegExp(text, replacement, params.left, params.right, params.flags) - } - - return [ - { - type: 'output', - filter, - }, - ] -}