From 3b6c3ad9405701c81888b006e2b7588813eb53dd Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 17 Nov 2023 15:09:23 -0500 Subject: [PATCH 01/17] create TickerBar module for handling bar updates --- lib/basket/websocket/alpaca.ex | 4 +- lib/basket_web/components/core_components.ex | 17 +++-- lib/basket_web/live/overview.ex | 46 ++++++-------- test/basket_web/live/overview_test.exs | 66 +++++++++++--------- 4 files changed, 66 insertions(+), 67 deletions(-) diff --git a/lib/basket/websocket/alpaca.ex b/lib/basket/websocket/alpaca.ex index 9c67e1c..b1bb8de 100644 --- a/lib/basket/websocket/alpaca.ex +++ b/lib/basket/websocket/alpaca.ex @@ -63,9 +63,7 @@ defmodule Basket.Websocket.Alpaca do def handle_frame({_tpe, msg}, state) do case Jason.decode(msg) do {:ok, decoded_message} -> - Enum.each(decoded_message, fn message -> - process_message(message) - end) + Enum.each(decoded_message, &process_message/1) {:error, error} -> Logger.error("Error decoding websocket message: #{inspect(error)}") diff --git a/lib/basket_web/components/core_components.ex b/lib/basket_web/components/core_components.ex index a349cff..e8190aa 100644 --- a/lib/basket_web/components/core_components.ex +++ b/lib/basket_web/components/core_components.ex @@ -268,7 +268,7 @@ defmodule BasketWeb.CoreComponents do + + + """ + end + + defp load_tickers do + case Http.Alpaca.list_assets() do + {:ok, result} -> + tickers = + Enum.map(result, fn asset -> + asset["symbol"] + end) + + {:commit, tickers} + + {:error, error} -> + Logger.error("Could not fetch tickers: #{error}") + + {:ignore, []} + end + end +end + +# <.inline_form for={assigns.form} phx-submit="ticker-add"> +# <.input +# name="selected_ticker" +# field="search_value" +# value={assigns.form["search_value"].value} +# list="tickers" +# phx-change="ticker-search" +# phx-debounce="500" +# phx-target={@myself} +# errors={[]} +# /> + +# +# {#for ticker <- assigns.tickers} +# +# {/for} +# +# <:actions> +# <.button class="bg-green-600 whitespace-nowrap w-12"> +# + +# +# +# diff --git a/lib/basket_web/live/overview/bars.ex b/lib/basket_web/live/overview/ticker_bar.ex similarity index 90% rename from lib/basket_web/live/overview/bars.ex rename to lib/basket_web/live/overview/ticker_bar.ex index 090afab..2e8419e 100644 --- a/lib/basket_web/live/overview/bars.ex +++ b/lib/basket_web/live/overview/ticker_bar.ex @@ -1,6 +1,6 @@ -defmodule BasketWeb.Overview.TickerBar do +defmodule BasketWeb.Live.Overview.TickerBar do @moduledoc """ - Stateful representation of a cell on the ticker bar table. + Cell on the TickerBarTable. """ alias __MODULE__ diff --git a/lib/basket_web/router.ex b/lib/basket_web/router.ex index 3e5f20c..d3ca71b 100644 --- a/lib/basket_web/router.ex +++ b/lib/basket_web/router.ex @@ -35,7 +35,7 @@ defmodule BasketWeb.Router do pipe_through [:browser, :authenticated] # get "/", PageController, :home - live "/", Overview + live "/", OverviewLive live "/demo", Demo end diff --git a/mix.exs b/mix.exs index 5e4ef3e..a900563 100644 --- a/mix.exs +++ b/mix.exs @@ -65,7 +65,7 @@ defmodule Basket.MixProject do {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, {:plug_cowboy, "~> 2.5"}, - {:surface, "~> 0.11.0"}, + {:surface, "~> 0.11.1"}, # for surface.init; possible to remove. {:sourceror, "~> 0.12.0"}, {:surface_catalogue, "~> 0.6.1"}, diff --git a/test/basket_web/live/overview_test.exs b/test/basket_web/live/overview_test.exs index 89f7fef..9e981ba 100644 --- a/test/basket_web/live/overview_test.exs +++ b/test/basket_web/live/overview_test.exs @@ -5,8 +5,8 @@ defmodule BasketWeb.OverviewTest do import Mox - alias BasketWeb.Overview - alias BasketWeb.Overview.TickerBar + alias BasketWeb.Live.Overview + alias BasketWeb.Live.Overview.TickerBar @assigns_map %{__changed__: %{__context__: true}} From 4b559c37c381af2578c0c040b4c61e46836b056e Mon Sep 17 00:00:00 2001 From: daveminer Date: Thu, 23 Nov 2023 08:47:47 -0500 Subject: [PATCH 06/17] use basic form tag --- lib/basket_web/live/overview.ex | 13 +---- lib/basket_web/live/overview/search.ex | 55 ++++++------------- .../live/overview/search.hooks.jsOLD | 48 ++++++++++++++++ 3 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 lib/basket_web/live/overview/search.hooks.jsOLD diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index ae314ec..90a7f99 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -19,8 +19,8 @@ defmodule BasketWeb.OverviewLive do {:ok, socket} end - # def handle_event("ticker-add", %{"search_value" => ticker}, socket) do - def handle_info({"ticker-add", %{"selected_ticker" => ticker}}, socket) do + def handle_info({"ticker-add", %{"search_value" => ticker}}, socket) do + # def handle_info({"ticker-add", %{"selected_ticker" => ticker}}, socket) do IO.inspect("SOCKET: #{inspect(socket.assigns)}") basket_tickers = tickers(socket) @@ -34,6 +34,7 @@ defmodule BasketWeb.OverviewLive do {:ok, response} -> %{"bars" => ticker_bars} = response new_ticker_bars = Map.to_list(ticker_bars) |> List.first() + # TODO: nil not a tuple - AKUMQ initial_bars = build_ticker_bars(elem(new_ticker_bars, 1)) assign( @@ -48,14 +49,6 @@ defmodule BasketWeb.OverviewLive do # socket end - # send_update( - # self(), - # SearchInput, - # id: "stock-search-input", - # ticker_search_value: "" - # ) - - # {:reply, %{}, socket} {:noreply, socket} end end diff --git a/lib/basket_web/live/overview/search.ex b/lib/basket_web/live/overview/search.ex index 35570d0..6295877 100644 --- a/lib/basket_web/live/overview/search.ex +++ b/lib/basket_web/live/overview/search.ex @@ -41,41 +41,16 @@ defmodule BasketWeb.Live.Overview.Search do # {:ok, socket} # end - def handle_event("ticker-add", %{"search_value" => _ticker}, socket) do - IO.inspect("COMPSOCKET: #{inspect(socket.assigns)}") - # basket_tickers = tickers(socket) - - # if ticker in basket_tickers or String.trim(ticker) == "" do - # {:noreply, socket} - # else - # :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []}) - - # socket = - # case Http.Alpaca.latest_quote(ticker) do - # {:ok, response} -> - # %{"bars" => ticker_bars} = response - # new_ticker_bars = Map.to_list(ticker_bars) |> List.first() - # initial_bars = build_ticker_bars(elem(new_ticker_bars, 1)) - - # assign( - # socket, - # :basket, - # socket.assigns.basket ++ - # [Map.merge(initial_bars, %{"S" => %TickerBar{value: ticker}})] - # ) - - # {:error, error} -> - # Logger.error("Could not subscribe to ticker: #{error}") - # socket - # end - IO.inspect(socket.assigns, label: "SOCKASN") + def handle_event("ticker-add", %{"search_value" => ticker}, socket) do + IO.inspect(ticker, label: "TICKER ADD") send( self(), - {"ticker-add", %{"selected_ticker" => ""}} + {"ticker-add", %{"search_value" => ticker}} ) - {:noreply, assign(socket, :form, %{"search_value" => ""})} + IO.inspect(socket, label: "SOOOOOOO") + {:reply, %{}, socket} end def handle_event("ticker-search", %{"search_value" => _query}, socket) do @@ -94,29 +69,35 @@ defmodule BasketWeb.Live.Overview.Search do ~F"""
{#for ticker <- assigns.tickers} {/for} + - diff --git a/lib/basket_web/live/overview/search.hooks.jsOLD b/lib/basket_web/live/overview/search.hooks.jsOLD new file mode 100644 index 0000000..30c2430 --- /dev/null +++ b/lib/basket_web/live/overview/search.hooks.jsOLD @@ -0,0 +1,48 @@ +export default { + // mounted() { + // this.el.addEventListener("submit", e => { + + // let ele = document.getElementById('ticker-input'); + // console.log(ele, "EL") + // ele.value = ""; + // // const input = this.el.querySelector("input"); + // // console.log(input, "INPUT") + // // if (input) { + // // input.value = ''; + // // } + // }); + // } + // updated() { + // console.log(this.el, "THISEL") + // // this.el.addEventListener("submit", e => { + // //document.getElementById('ticker-input').value = ""; + // let ele = document.getElementById('ticker-input'); + // console.log(ele, "EL") + // ele.value = ""; + // // // const input = this.el.querySelector("input"); + // // // console.log(input, "INPUT") + // // // if (input) { + // // // input.value = ''; + // // // } + // // }); + // } +}; + +let Clear = { + updated() { + console.log(this.el, "THISEL") + // this.el.addEventListener("submit", e => { + //document.getElementById('ticker-input').value = ""; + let ele = document.getElementById('ticker-input'); + console.log(ele, "EL") + ele.value = ""; + // // const input = this.el.querySelector("input"); + // // console.log(input, "INPUT") + // // if (input) { + // // input.value = ''; + // // } + // }); + } +} + +export { Clear} \ No newline at end of file From d7e20ee9ea3de34a84cfdf124282b927c5537791 Mon Sep 17 00:00:00 2001 From: daveminer Date: Thu, 23 Nov 2023 11:41:55 -0500 Subject: [PATCH 07/17] replace surface form --- lib/basket_web/live/overview.ex | 2 +- lib/basket_web/live/overview/search.ex | 39 +++++++++++++++----------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 90a7f99..0fb27bc 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -19,7 +19,7 @@ defmodule BasketWeb.OverviewLive do {:ok, socket} end - def handle_info({"ticker-add", %{"search_value" => ticker}}, socket) do + def handle_info({"ticker-add", %{"ticker" => ticker}}, socket) do # def handle_info({"ticker-add", %{"selected_ticker" => ticker}}, socket) do IO.inspect("SOCKET: #{inspect(socket.assigns)}") basket_tickers = tickers(socket) diff --git a/lib/basket_web/live/overview/search.ex b/lib/basket_web/live/overview/search.ex index 6295877..e2366ec 100644 --- a/lib/basket_web/live/overview/search.ex +++ b/lib/basket_web/live/overview/search.ex @@ -9,16 +9,15 @@ defmodule BasketWeb.Live.Overview.Search do require Logger alias Basket.Http - alias Surface.Components.Form attr :id, :string, required: true attr :class, :string, default: nil - data form, :map, default: %{"search_value" => ""} + data form, :map, default: %{"ticker" => ""} data tickers, :list, default: [] def mount(socket) do - socket = assign(socket, :form, %{"search_value" => ""}) + socket = assign(socket, :form, %{"ticker" => ""}) socket = assign(socket, :tickers, []) {:ok, socket} @@ -41,19 +40,22 @@ defmodule BasketWeb.Live.Overview.Search do # {:ok, socket} # end - def handle_event("ticker-add", %{"search_value" => ticker}, socket) do + def handle_event("ticker-add", %{"ticker" => ticker}, socket) do IO.inspect(ticker, label: "TICKER ADD") send( self(), - {"ticker-add", %{"search_value" => ticker}} + {"ticker-add", %{"ticker" => ticker}} ) IO.inspect(socket, label: "SOOOOOOO") + socket = assign(socket, :ticker, "") {:reply, %{}, socket} end - def handle_event("ticker-search", %{"search_value" => _query}, socket) do + def handle_event("ticker-search", %{"ticker" => ticker}, socket) do + socket = assign(socket, :form, %{"ticker" => ticker}) + if length(socket.assigns.tickers) > 0 do {:noreply, socket} else @@ -68,19 +70,23 @@ defmodule BasketWeb.Live.Overview.Search do ~F"""
-
- @@ -89,7 +95,6 @@ defmodule BasketWeb.Live.Overview.Search do {/for} - - +
""" end From f1947c1ba3dd97964c6c35305d86f9c8c63ead39 Mon Sep 17 00:00:00 2001 From: daveminer Date: Thu, 23 Nov 2023 11:50:40 -0500 Subject: [PATCH 08/17] remove unused hooks --- lib/basket_web/live/overview.ex | 16 ------- lib/basket_web/live/overview/search.ex | 48 +------------------ .../live/overview/search.hooks.jsOLD | 48 ------------------- 3 files changed, 1 insertion(+), 111 deletions(-) delete mode 100644 lib/basket_web/live/overview/search.hooks.jsOLD diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 0fb27bc..5003fbb 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -20,8 +20,6 @@ defmodule BasketWeb.OverviewLive do end def handle_info({"ticker-add", %{"ticker" => ticker}}, socket) do - # def handle_info({"ticker-add", %{"selected_ticker" => ticker}}, socket) do - IO.inspect("SOCKET: #{inspect(socket.assigns)}") basket_tickers = tickers(socket) if ticker in basket_tickers or String.trim(ticker) == "" do @@ -57,8 +55,6 @@ defmodule BasketWeb.OverviewLive do %Phoenix.Socket.Broadcast{topic: "bars", event: "ticker-update", payload: bars}, socket ) do - IO.inspect("BASKET: #{inspect(socket.assigns.basket)}") - IO.inspect("BARS: #{inspect(bars)}") ticker = bars["S"] new_basket = @@ -68,8 +64,6 @@ defmodule BasketWeb.OverviewLive do else: row end) - IO.inspect("NB: #{inspect(new_basket)}") - {:noreply, assign( socket, @@ -95,16 +89,6 @@ defmodule BasketWeb.OverviewLive do end end - # def handle_event("ticker-search", %{"search_value" => _query}, socket) do - # if length(socket.assigns.tickers) > 0 do - # {:noreply, socket} - # else - # {_status, tickers} = Cachex.fetch(:assets, "all", fn _key -> load_tickers() end) - - # {:reply, %{}, assign(socket, :tickers, tickers)} - # end - # end - def render(assigns) do ~F"""
diff --git a/lib/basket_web/live/overview/search.ex b/lib/basket_web/live/overview/search.ex index e2366ec..099948a 100644 --- a/lib/basket_web/live/overview/search.ex +++ b/lib/basket_web/live/overview/search.ex @@ -23,33 +23,13 @@ defmodule BasketWeb.Live.Overview.Search do {:ok, socket} end - # def update(_assigns, socket) do - # IO.inspect("SOCK BEF: #{inspect(socket)}") - - # socket = - # assign( - # socket, - # :form, - # to_form(%{ - # "search_value" => "" - # }) - # ) - - # IO.inspect("SOCK: #{inspect(socket)}") - - # {:ok, socket} - # end - def handle_event("ticker-add", %{"ticker" => ticker}, socket) do - IO.inspect(ticker, label: "TICKER ADD") - send( self(), {"ticker-add", %{"ticker" => ticker}} ) - IO.inspect(socket, label: "SOOOOOOO") - socket = assign(socket, :ticker, "") + socket = assign(socket, :form, %{"ticker" => ""}) {:reply, %{}, socket} end @@ -66,8 +46,6 @@ defmodule BasketWeb.Live.Overview.Search do end def render(assigns) do - IO.inspect("TICK: #{inspect(assigns)}") - ~F"""
<.form @@ -127,27 +105,3 @@ defmodule BasketWeb.Live.Overview.Search do end end end - -# <.inline_form for={assigns.form} phx-submit="ticker-add"> -# <.input -# name="selected_ticker" -# field="search_value" -# value={assigns.form["search_value"].value} -# list="tickers" -# phx-change="ticker-search" -# phx-debounce="500" -# phx-target={@myself} -# errors={[]} -# /> - -# -# {#for ticker <- assigns.tickers} -# -# {/for} -# -# <:actions> -# <.button class="bg-green-600 whitespace-nowrap w-12"> -# + -# -# -# diff --git a/lib/basket_web/live/overview/search.hooks.jsOLD b/lib/basket_web/live/overview/search.hooks.jsOLD deleted file mode 100644 index 30c2430..0000000 --- a/lib/basket_web/live/overview/search.hooks.jsOLD +++ /dev/null @@ -1,48 +0,0 @@ -export default { - // mounted() { - // this.el.addEventListener("submit", e => { - - // let ele = document.getElementById('ticker-input'); - // console.log(ele, "EL") - // ele.value = ""; - // // const input = this.el.querySelector("input"); - // // console.log(input, "INPUT") - // // if (input) { - // // input.value = ''; - // // } - // }); - // } - // updated() { - // console.log(this.el, "THISEL") - // // this.el.addEventListener("submit", e => { - // //document.getElementById('ticker-input').value = ""; - // let ele = document.getElementById('ticker-input'); - // console.log(ele, "EL") - // ele.value = ""; - // // // const input = this.el.querySelector("input"); - // // // console.log(input, "INPUT") - // // // if (input) { - // // // input.value = ''; - // // // } - // // }); - // } -}; - -let Clear = { - updated() { - console.log(this.el, "THISEL") - // this.el.addEventListener("submit", e => { - //document.getElementById('ticker-input').value = ""; - let ele = document.getElementById('ticker-input'); - console.log(ele, "EL") - ele.value = ""; - // // const input = this.el.querySelector("input"); - // // console.log(input, "INPUT") - // // if (input) { - // // input.value = ''; - // // } - // }); - } -} - -export { Clear} \ No newline at end of file From 577531044abb6828d3d16c85f3b2420ca18fc514 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 09:19:55 -0500 Subject: [PATCH 09/17] fix overview tests --- lib/basket_web/live/demo.ex | 35 ------ lib/basket_web/live/overview.ex | 11 +- lib/basket_web/live/overview/ticker_bar.ex | 2 +- .../overview}/ticker_bar_table.ex | 12 +- lib/basket_web/router.ex | 2 - test/basket_web/live/overview_test.exs | 107 ++++++++---------- 6 files changed, 52 insertions(+), 117 deletions(-) delete mode 100644 lib/basket_web/live/demo.ex rename lib/basket_web/{components => live/overview}/ticker_bar_table.ex (88%) diff --git a/lib/basket_web/live/demo.ex b/lib/basket_web/live/demo.ex deleted file mode 100644 index 9059d6f..0000000 --- a/lib/basket_web/live/demo.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule BasketWeb.Demo do - @moduledoc false - use BasketWeb, :surface_live_view - - alias BasketWeb.Components.Card - - def render(assigns) do - ~F""" - - -
- - <:header> - Surface UI - - - Start building rich interactive user-interfaces, writing minimal custom Javascript. - Built on top of Phoenix LiveView, Surface leverages the amazing - Phoenix Framework to provide a fast and productive solution to - build modern web applications. - - <:footer> - #surface - #phoenix - #tailwindcss - - -
- """ - end -end diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 5003fbb..ba1832b 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -20,13 +20,9 @@ defmodule BasketWeb.OverviewLive do end def handle_info({"ticker-add", %{"ticker" => ticker}}, socket) do - basket_tickers = tickers(socket) - - if ticker in basket_tickers or String.trim(ticker) == "" do + if ticker in tickers(socket) or String.trim(ticker) == "" do {:noreply, socket} else - :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []}) - socket = case Http.Alpaca.latest_quote(ticker) do {:ok, response} -> @@ -44,9 +40,10 @@ defmodule BasketWeb.OverviewLive do {:error, error} -> Logger.error("Could not subscribe to ticker: #{error}") - # socket end + :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []}) + {:noreply, socket} end end @@ -96,7 +93,7 @@ defmodule BasketWeb.OverviewLive do
<.live_component module={Search} id="stock-search-input" />
- <.live_component module={TickerBarTable} id="ticker-bar-table" rows={@basket} /> +
""" end diff --git a/lib/basket_web/live/overview/ticker_bar.ex b/lib/basket_web/live/overview/ticker_bar.ex index 2e8419e..958fd27 100644 --- a/lib/basket_web/live/overview/ticker_bar.ex +++ b/lib/basket_web/live/overview/ticker_bar.ex @@ -7,7 +7,7 @@ defmodule BasketWeb.Live.Overview.TickerBar do defstruct value: nil, prev_value: nil @typedoc """ - This module is responsible for taking the data from an external call and updating the state of the cell. + This module takes the data from an external call and updates the state of the cell. """ @type t(value, prev_value) :: %TickerBar{ value: value, diff --git a/lib/basket_web/components/ticker_bar_table.ex b/lib/basket_web/live/overview/ticker_bar_table.ex similarity index 88% rename from lib/basket_web/components/ticker_bar_table.ex rename to lib/basket_web/live/overview/ticker_bar_table.ex index 91718a7..d882228 100644 --- a/lib/basket_web/components/ticker_bar_table.ex +++ b/lib/basket_web/live/overview/ticker_bar_table.ex @@ -4,21 +4,13 @@ defmodule BasketWeb.Components.TickerBarTable do if the ticker list is not populated, otherwise it will pull the list from the cache. """ - use Surface.LiveComponent + use Surface.Component import BasketWeb.CoreComponents + prop id, :string prop rows, :list, default: [] - attr :id, :string, required: true - attr :class, :string, default: nil - - def mount(socket) do - socket = assign(socket, basket: []) - - {:ok, socket} - end - def render(assigns) do ~F"""
diff --git a/lib/basket_web/router.ex b/lib/basket_web/router.ex index d3ca71b..9f578d3 100644 --- a/lib/basket_web/router.ex +++ b/lib/basket_web/router.ex @@ -34,9 +34,7 @@ defmodule BasketWeb.Router do scope "/", BasketWeb do pipe_through [:browser, :authenticated] - # get "/", PageController, :home live "/", OverviewLive - live "/demo", Demo end # Other scopes may use custom stacks. diff --git a/test/basket_web/live/overview_test.exs b/test/basket_web/live/overview_test.exs index 9e981ba..2cc5797 100644 --- a/test/basket_web/live/overview_test.exs +++ b/test/basket_web/live/overview_test.exs @@ -1,12 +1,12 @@ -defmodule BasketWeb.OverviewTest do +defmodule BasketWeb.OverviewLiveTest do use BasketWeb.ConnCase, async: false require Phoenix.LiveViewTest import Mox - alias BasketWeb.Live.Overview - alias BasketWeb.Live.Overview.TickerBar + alias BasketWeb.OverviewLive + alias BasketWeb.Live.Overview.{Search, TickerBar} @assigns_map %{__changed__: %{__context__: true}} @@ -49,14 +49,13 @@ defmodule BasketWeb.OverviewTest do test "assigns empty lists to keys" do Basket.Websocket.MockAlpaca |> expect(:start_link, fn _state -> {:ok, 1} end) - assert({:ok, socket} = Overview.mount([], %{}, @assigns_map)) + assert({:ok, socket} = OverviewLive.mount([], %{}, @assigns_map)) assert( socket == %{ - __changed__: %{__context__: true, basket: true, tickers: true}, + __changed__: %{__context__: true, basket: true}, __context__: %{}, - basket: [], - tickers: [] + basket: [] } ) end @@ -65,9 +64,9 @@ defmodule BasketWeb.OverviewTest do describe "handle_event/3 search" do test "ticker search does nothing without search criteria" do assert {:noreply, _socket} = - Overview.handle_event( + Search.handle_event( "ticker-search", - %{"selected-ticker" => "ABC"}, + %{"ticker" => "ABC"}, Map.merge(@assigns_map, %{assigns: %{tickers: ["ABC", "XYZ"]}}) ) end @@ -78,9 +77,9 @@ defmodule BasketWeb.OverviewTest do __changed__: %{__context__: true}, assigns: %{tickers: ["ABC", "XYZ"]} }} = - Overview.handle_event( + Search.handle_event( "ticker-search", - %{"selected-ticker" => "XYZ"}, + %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{assigns: %{tickers: ["ABC", "XYZ"]}}) ) end @@ -128,9 +127,9 @@ defmodule BasketWeb.OverviewTest do __changed__: %{__context__: true, tickers: true}, assigns: %{tickers: []} }} = - Overview.handle_event( + Search.handle_event( "ticker-search", - %{"selected-ticker" => "XYZ"}, + %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{assigns: %{tickers: []}}) ) end @@ -143,39 +142,40 @@ defmodule BasketWeb.OverviewTest do assert {:reply, %{}, %{ - __changed__: %{__context__: true, basket: true}, - assigns: %{tickers: [], basket: []}, - basket: [_bars] + __changed__: %{__context__: true, form: true}, + assigns: %{tickers: [], form: []}, + form: %{"ticker" => ""} }} = - Overview.handle_event( + Search.handle_event( "ticker-add", - %{"selected-ticker" => "XYZ"}, - Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}}) + %{"ticker" => "XYZ"}, + Map.merge(@assigns_map, %{assigns: %{tickers: [], form: []}}) ) end test "does nothing if no ticker is provided" do - assert {:noreply, + assert {:reply, %{}, %{ - __changed__: %{__context__: true}, - assigns: %{tickers: [], basket: []} + __changed__: %{__context__: true, form: true}, + assigns: %{basket: [], tickers: []}, + form: %{"ticker" => ""} }} = - Overview.handle_event( + Search.handle_event( "ticker-add", - %{"selected-ticker" => ""}, + %{"ticker" => ""}, Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}}) ) end test "does nothing if the ticker is already present", %{basket_with_row: basket_with_row} do - assert {:noreply, + assert {:reply, %{}, %{ __changed__: %{__context__: true}, assigns: %{tickers: [], basket: [_bars]} }} = - Overview.handle_event( + Search.handle_event( "ticker-add", - %{"selected-ticker" => "XYZ"}, + %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{ assigns: %{ tickers: [], @@ -200,7 +200,7 @@ defmodule BasketWeb.OverviewTest do assigns: %{tickers: [], basket: ^basket_with_row}, basket: [] }} = - Overview.handle_event( + OverviewLive.handle_event( "ticker-remove", %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: basket_with_row}}) @@ -213,7 +213,7 @@ defmodule BasketWeb.OverviewTest do __changed__: %{__context__: true}, assigns: %{tickers: [], basket: []} }} = - Overview.handle_event( + OverviewLive.handle_event( "ticker-remove", %{"ticker" => ""}, Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}}) @@ -226,7 +226,7 @@ defmodule BasketWeb.OverviewTest do __changed__: %{__context__: true}, assigns: %{tickers: [], basket: []} }} = - Overview.handle_event( + OverviewLive.handle_event( "ticker-remove", %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{ @@ -269,42 +269,25 @@ defmodule BasketWeb.OverviewTest do ] } } = - Overview.handle_info( + OverviewLive.handle_info( %Phoenix.Socket.Broadcast{ topic: "bars", event: "ticker-update", - payload: [ - %{ - "S" => %BasketWeb.Overview.TickerBar{value: "TSLA", prev_value: nil}, - "T" => %BasketWeb.Overview.TickerBar{value: "b", prev_value: nil}, - "c" => %BasketWeb.Overview.TickerBar{value: 234.32, prev_value: nil}, - "h" => %BasketWeb.Overview.TickerBar{value: 234.52, prev_value: nil}, - "l" => %BasketWeb.Overview.TickerBar{value: 234.3, prev_value: nil}, - "n" => %BasketWeb.Overview.TickerBar{value: 12, prev_value: nil}, - "o" => %BasketWeb.Overview.TickerBar{value: 234.52, prev_value: nil}, - "t" => %BasketWeb.Overview.TickerBar{ - value: "2023-11-20T16:24:00Z", - prev_value: nil - }, - "v" => %BasketWeb.Overview.TickerBar{value: 856, prev_value: nil}, - "vw" => %BasketWeb.Overview.TickerBar{value: 234.43257, prev_value: nil} + payload: %{ + "S" => %TickerBar{value: "AAPL", prev_value: nil}, + "T" => %TickerBar{value: "b", prev_value: nil}, + "c" => %TickerBar{value: 191.285, prev_value: nil}, + "h" => %TickerBar{value: 191.37, prev_value: nil}, + "l" => %TickerBar{value: 191.23, prev_value: nil}, + "n" => %TickerBar{value: 50, prev_value: nil}, + "o" => %TickerBar{value: 191.23, prev_value: nil}, + "t" => %TickerBar{ + value: "2023-11-20T16:24:00Z", + prev_value: nil }, - %{ - "S" => %BasketWeb.Overview.TickerBar{value: "AAPL", prev_value: nil}, - "T" => %BasketWeb.Overview.TickerBar{value: "b", prev_value: nil}, - "c" => %BasketWeb.Overview.TickerBar{value: 191.285, prev_value: nil}, - "h" => %BasketWeb.Overview.TickerBar{value: 191.37, prev_value: nil}, - "l" => %BasketWeb.Overview.TickerBar{value: 191.23, prev_value: nil}, - "n" => %BasketWeb.Overview.TickerBar{value: 50, prev_value: nil}, - "o" => %BasketWeb.Overview.TickerBar{value: 191.23, prev_value: nil}, - "t" => %BasketWeb.Overview.TickerBar{ - value: "2023-11-20T16:24:00Z", - prev_value: nil - }, - "v" => %BasketWeb.Overview.TickerBar{value: 5433, prev_value: nil}, - "vw" => %BasketWeb.Overview.TickerBar{value: 191.328043, prev_value: nil} - } - ] + "v" => %TickerBar{value: 5433, prev_value: nil}, + "vw" => %TickerBar{value: 191.328043, prev_value: nil} + } }, Map.merge(@assigns_map, %{ assigns: %{tickers: [], basket: basket_with_row}, From 67928837564fa16049ae0f75387a50d85d53b371 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 11:20:39 -0500 Subject: [PATCH 10/17] refactor ticker add --- lib/basket_web/components/core_components.ex | 32 ++++------ lib/basket_web/components/dark_mode_toggle.ex | 26 --------- lib/basket_web/components/nav_row.ex | 2 - lib/basket_web/live/overview.ex | 50 ++++++---------- lib/basket_web/live/overview/ticker_add.ex | 58 +++++++++++++++++++ 5 files changed, 89 insertions(+), 79 deletions(-) delete mode 100644 lib/basket_web/components/dark_mode_toggle.ex create mode 100644 lib/basket_web/live/overview/ticker_add.ex diff --git a/lib/basket_web/components/core_components.ex b/lib/basket_web/components/core_components.ex index 91f05dc..8b7cd21 100644 --- a/lib/basket_web/components/core_components.ex +++ b/lib/basket_web/components/core_components.ex @@ -330,8 +330,6 @@ defmodule BasketWeb.CoreComponents do slot :inner_block def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do - IO.inspect("ASS: #{assigns}") - assigns |> assign(field: nil, id: assigns.id || field.id) |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) @@ -407,8 +405,6 @@ defmodule BasketWeb.CoreComponents do # All other inputs text, datetime-local, url, password, etc. are handled here... def input(assigns) do - IO.inspect("DEF: #{inspect(assigns)}") - ~H"""
<.label for={@id}><%= @label %> @@ -541,7 +537,7 @@ defmodule BasketWeb.CoreComponents do "relative p-0", "text-center", @row_click && "hover:cursor-pointer", - diff_color(col, row) + diff_cell_color(col, row) ]} >
@@ -722,23 +718,19 @@ defmodule BasketWeb.CoreComponents do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end - defp diff_color(col, row) do - key = Map.get(col, :key) + defp diff_cell_color(%{key: nil}, _row), do: "" - if is_nil(key) do - "" - else - field = row[key] - - if field != nil && is_number(field.value) && is_number(field.prev_value) do - case field.value - field.prev_value do - x when x > 0 -> "bg-emerald-300 text-emerald-900" - x when x < 0 -> "bg-rose-300 text-rose-900" - _ -> "" - end - else - "" + defp diff_cell_color(%{key: key} = _col, row) do + field = row[key] + + if field != nil && is_number(field.value) && is_number(field.prev_value) do + case field.value - field.prev_value do + x when x > 0 -> "bg-emerald-300 text-emerald-900" + x when x < 0 -> "bg-rose-300 text-rose-900" + _ -> "" end + else + "" end end end diff --git a/lib/basket_web/components/dark_mode_toggle.ex b/lib/basket_web/components/dark_mode_toggle.ex deleted file mode 100644 index c772b7c..0000000 --- a/lib/basket_web/components/dark_mode_toggle.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule BasketWeb.Components.DarkModeToggle do - @moduledoc """ - A sample component generated by `mix surface.init`. - """ - use Surface.Component - - import BasketWeb.CoreComponents - - alias Phoenix.LiveView.JS - - def render(assigns) do - ~F""" - """ - - # ~F""" - #
- # - # - #
- # """ - end -end diff --git a/lib/basket_web/components/nav_row.ex b/lib/basket_web/components/nav_row.ex index 6bdfbc0..9862a30 100644 --- a/lib/basket_web/components/nav_row.ex +++ b/lib/basket_web/components/nav_row.ex @@ -2,14 +2,12 @@ defmodule BasketWeb.Components.NavRow do @moduledoc """ The header for the home page. """ - alias BasketWeb.Components.DarkModeToggle use Surface.Component def render(assigns) do ~F"""
- <.link href="/session" method="delete">Sign out
""" diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index ba1832b..74b12e5 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -6,10 +6,9 @@ defmodule BasketWeb.OverviewLive do require Logger - alias Basket.{Http, Websocket} + alias Basket.Websocket alias BasketWeb.Components.{NavRow, TickerBarTable} - alias BasketWeb.Live.Overview.Search - alias BasketWeb.Live.Overview.TickerBar + alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBar} def mount(_, _, socket) do BasketWeb.Endpoint.subscribe(Websocket.Alpaca.bars_topic()) @@ -24,25 +23,18 @@ defmodule BasketWeb.OverviewLive do {:noreply, socket} else socket = - case Http.Alpaca.latest_quote(ticker) do - {:ok, response} -> - %{"bars" => ticker_bars} = response - new_ticker_bars = Map.to_list(ticker_bars) |> List.first() - # TODO: nil not a tuple - AKUMQ - initial_bars = build_ticker_bars(elem(new_ticker_bars, 1)) - - assign( - socket, - :basket, - socket.assigns.basket ++ - [Map.merge(initial_bars, %{"S" => %TickerBar{value: ticker}})] - ) - - {:error, error} -> - Logger.error("Could not subscribe to ticker: #{error}") - end + case TickerAdd.call(ticker) do + row when is_map(row) -> + :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []}) + assign(socket, :basket, (socket.assigns.basket ++ [row]) |> sort_by_ticker()) + + :market_closed -> + # TODO: add market closed row + socket - :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []}) + _ -> + socket + end {:noreply, socket} end @@ -98,16 +90,6 @@ defmodule BasketWeb.OverviewLive do """ end - defp build_ticker_bars(ticker_bars) do - if ticker_bars == %{} do - %{"t" => "Market Closed"} - else - Enum.reduce(ticker_bars, %{}, fn {k, v}, acc -> - Map.put(acc, k, %TickerBar{value: v}) - end) - end - end - defp new_ticker_row(row, bars) do Enum.reduce(row, %{}, fn {k, v}, acc -> new_value = Map.get(bars, k) @@ -115,5 +97,11 @@ defmodule BasketWeb.OverviewLive do end) end + defp sort_by_ticker(bars), + do: + Enum.sort(bars, fn a, b -> + a["S"].value < b["S"].value + end) + defp tickers(socket), do: Enum.map(socket.assigns.basket, &Map.get(&1, "S").value) end diff --git a/lib/basket_web/live/overview/ticker_add.ex b/lib/basket_web/live/overview/ticker_add.ex new file mode 100644 index 0000000..f3bce66 --- /dev/null +++ b/lib/basket_web/live/overview/ticker_add.ex @@ -0,0 +1,58 @@ +defmodule BasketWeb.Live.Overview.TickerAdd do + @moduledoc """ + Creates a new ticker in the table. + """ + + alias Basket.Http + alias BasketWeb.Live.Overview.TickerBar + + require Logger + + @doc """ + Creates a row to be added to the ticker bar table. + """ + def call(ticker) do + case Http.Alpaca.latest_quote(ticker) do + {:ok, response} -> + case build_ticker_bars(response) do + :no_data -> + # TODO: info flash + :no_data + + :market_closed -> + :market_closed + + bars -> + bars + end + + {:error, error} -> + # TODO: error flash + Logger.error("Could not subscribe to ticker: #{error}") + end + end + + defp build_ticker_bars(%{"bars" => nil}) do + end + + defp build_ticker_bars(%{"bars" => ticker_bars}) do + # TODO: check first + new_ticker_bars = Map.to_list(ticker_bars) |> List.first() + + case new_ticker_bars do + nil -> + :no_data + + %{} -> + :market_closed + + bars -> + Enum.reduce(bars, %{}, fn {k, v}, acc -> + Map.put(acc, k, %TickerBar{value: v}) + end) + + # TODO: check + # Map.merge(new_bars, %{"S" => %TickerBar{value: ticker}}) + end + end +end From 1b4b5745bcc80561c4cea5a30be6f486bc15ec7d Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 12:58:53 -0500 Subject: [PATCH 11/17] add credo config and ticker_add refactor --- .credo.exs | 216 +++++++++++++++++++ lib/basket_web/components/core_components.ex | 2 + lib/basket_web/live/overview.ex | 8 +- lib/basket_web/live/overview/ticker_add.ex | 33 +-- 4 files changed, 235 insertions(+), 24 deletions(-) create mode 100644 .credo.exs diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..539d213 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,216 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/lib/basket_web/components/core_components.ex b/lib/basket_web/components/core_components.ex index 8b7cd21..92d0ac0 100644 --- a/lib/basket_web/components/core_components.ex +++ b/lib/basket_web/components/core_components.ex @@ -733,4 +733,6 @@ defmodule BasketWeb.CoreComponents do "" end end + + defp diff_cell_color(_col, _row), do: "" end diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 74b12e5..9f7ae27 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -32,8 +32,12 @@ defmodule BasketWeb.OverviewLive do # TODO: add market closed row socket - _ -> - socket + :no_data -> + put_flash(socket, :info, "No data for ticker: #{ticker}") + + {:error, error} -> + Logger.error("Could not subscribe to ticker: #{error}") + put_flash(socket, :error, "Something when wrong.") end {:noreply, socket} diff --git a/lib/basket_web/live/overview/ticker_add.ex b/lib/basket_web/live/overview/ticker_add.ex index f3bce66..4caa9fd 100644 --- a/lib/basket_web/live/overview/ticker_add.ex +++ b/lib/basket_web/live/overview/ticker_add.ex @@ -11,29 +11,18 @@ defmodule BasketWeb.Live.Overview.TickerAdd do @doc """ Creates a row to be added to the ticker bar table. """ + @spec call(ticker :: String.t()) :: :no_data | :market_closed | Map.t() def call(ticker) do case Http.Alpaca.latest_quote(ticker) do {:ok, response} -> - case build_ticker_bars(response) do - :no_data -> - # TODO: info flash - :no_data - - :market_closed -> - :market_closed - - bars -> - bars - end + build_ticker_bars(response) {:error, error} -> - # TODO: error flash - Logger.error("Could not subscribe to ticker: #{error}") + {:error, error} end end - defp build_ticker_bars(%{"bars" => nil}) do - end + defp build_ticker_bars(%{"bars" => nil}), do: :no_data defp build_ticker_bars(%{"bars" => ticker_bars}) do # TODO: check first @@ -43,16 +32,16 @@ defmodule BasketWeb.Live.Overview.TickerAdd do nil -> :no_data - %{} -> + map when map_size(map) == 0 -> :market_closed - bars -> - Enum.reduce(bars, %{}, fn {k, v}, acc -> - Map.put(acc, k, %TickerBar{value: v}) - end) + {ticker, bars} -> + new_bars = + Enum.reduce(bars, %{}, fn {k, v}, acc -> + Map.put(acc, k, %TickerBar{value: v}) + end) - # TODO: check - # Map.merge(new_bars, %{"S" => %TickerBar{value: ticker}}) + Map.merge(new_bars, %{"S" => %TickerBar{value: ticker}}) end end end From 5511e2dd31ea247b8e0fb72e99bc6f2cb3c88b85 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 13:01:19 -0500 Subject: [PATCH 12/17] credo ignore for market close --- lib/basket_web/live/overview.ex | 1 + lib/basket_web/live/overview/ticker_add.ex | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 9f7ae27..308a9f8 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -29,6 +29,7 @@ defmodule BasketWeb.OverviewLive do assign(socket, :basket, (socket.assigns.basket ++ [row]) |> sort_by_ticker()) :market_closed -> + # credo:disable-for-next-line # TODO: add market closed row socket diff --git a/lib/basket_web/live/overview/ticker_add.ex b/lib/basket_web/live/overview/ticker_add.ex index 4caa9fd..846320e 100644 --- a/lib/basket_web/live/overview/ticker_add.ex +++ b/lib/basket_web/live/overview/ticker_add.ex @@ -25,7 +25,6 @@ defmodule BasketWeb.Live.Overview.TickerAdd do defp build_ticker_bars(%{"bars" => nil}), do: :no_data defp build_ticker_bars(%{"bars" => ticker_bars}) do - # TODO: check first new_ticker_bars = Map.to_list(ticker_bars) |> List.first() case new_ticker_bars do From fb1c76b3b3a0fe0dd5eed56506247e5a6a9744d0 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 13:34:56 -0500 Subject: [PATCH 13/17] change module path to fix surface build --- .../{live/overview => components}/ticker_bar_table.ex | 0 lib/basket_web/live/overview.ex | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/basket_web/{live/overview => components}/ticker_bar_table.ex (100%) diff --git a/lib/basket_web/live/overview/ticker_bar_table.ex b/lib/basket_web/components/ticker_bar_table.ex similarity index 100% rename from lib/basket_web/live/overview/ticker_bar_table.ex rename to lib/basket_web/components/ticker_bar_table.ex diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 308a9f8..94aff69 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -7,7 +7,7 @@ defmodule BasketWeb.OverviewLive do require Logger alias Basket.Websocket - alias BasketWeb.Components.{NavRow, TickerBarTable} + alias BasketWeb.Components.NavRow alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBar} def mount(_, _, socket) do @@ -90,7 +90,7 @@ defmodule BasketWeb.OverviewLive do
<.live_component module={Search} id="stock-search-input" />
- +
""" end From e0e908cf4a9b68f8db67921e74ca5a04c264e6b0 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 20:39:24 -0500 Subject: [PATCH 14/17] change corecomponent import to alias --- lib/basket_web/live/overview.ex | 6 +++--- .../overview}/ticker_bar_table.ex | 12 ++++++------ lib/basket_web/router.ex | 2 +- test/basket_web/live/overview_test.exs | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) rename lib/basket_web/{components => live/overview}/ticker_bar_table.ex (85%) diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 94aff69..c4a4637 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -1,4 +1,4 @@ -defmodule BasketWeb.OverviewLive do +defmodule BasketWeb.Live.Overview do @moduledoc """ Home page shows a list of assets and updates them in realtime via websockets. """ @@ -8,7 +8,7 @@ defmodule BasketWeb.OverviewLive do alias Basket.Websocket alias BasketWeb.Components.NavRow - alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBar} + alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBar, TickerBarTable} def mount(_, _, socket) do BasketWeb.Endpoint.subscribe(Websocket.Alpaca.bars_topic()) @@ -90,7 +90,7 @@ defmodule BasketWeb.OverviewLive do
<.live_component module={Search} id="stock-search-input" />
- +
""" end diff --git a/lib/basket_web/components/ticker_bar_table.ex b/lib/basket_web/live/overview/ticker_bar_table.ex similarity index 85% rename from lib/basket_web/components/ticker_bar_table.ex rename to lib/basket_web/live/overview/ticker_bar_table.ex index d882228..aa1e41a 100644 --- a/lib/basket_web/components/ticker_bar_table.ex +++ b/lib/basket_web/live/overview/ticker_bar_table.ex @@ -1,4 +1,4 @@ -defmodule BasketWeb.Components.TickerBarTable do +defmodule BasketWeb.Live.Overview.TickerBarTable do @moduledoc """ Allows the user to search for and add a ticker to the table. Will make an HTTP call if the ticker list is not populated, otherwise it will pull the list from the cache. @@ -6,7 +6,7 @@ defmodule BasketWeb.Components.TickerBarTable do use Surface.Component - import BasketWeb.CoreComponents + alias BasketWeb.CoreComponents prop id, :string prop rows, :list, default: [] @@ -14,7 +14,7 @@ defmodule BasketWeb.Components.TickerBarTable do def render(assigns) do ~F"""
- <.table id="ticker-list" rows={@rows}> + <:col :let={ticker} key="S" label="ticker">{value_from_ticker_bar(ticker["S"])} <:col :let={ticker} key="o" label="open">{value_from_ticker_bar(ticker["o"])} <:col :let={ticker} key="h" label="high">{value_from_ticker_bar(ticker["h"])} @@ -23,15 +23,15 @@ defmodule BasketWeb.Components.TickerBarTable do <:col :let={ticker} key="v" label="volume">{value_from_ticker_bar(ticker["v"])} <:col :let={ticker} key="t" label="timestamp">{value_from_ticker_bar(ticker["t"])} <:col :let={ticker} label="remove"> - <.button + X - + " - +
""" end diff --git a/lib/basket_web/router.ex b/lib/basket_web/router.ex index 9f578d3..6e6ea36 100644 --- a/lib/basket_web/router.ex +++ b/lib/basket_web/router.ex @@ -34,7 +34,7 @@ defmodule BasketWeb.Router do scope "/", BasketWeb do pipe_through [:browser, :authenticated] - live "/", OverviewLive + live "/", Live.Overview end # Other scopes may use custom stacks. diff --git a/test/basket_web/live/overview_test.exs b/test/basket_web/live/overview_test.exs index 2cc5797..ffb291c 100644 --- a/test/basket_web/live/overview_test.exs +++ b/test/basket_web/live/overview_test.exs @@ -1,11 +1,11 @@ -defmodule BasketWeb.OverviewLiveTest do +defmodule BasketWeb.Live.OverviewTest do use BasketWeb.ConnCase, async: false require Phoenix.LiveViewTest import Mox - alias BasketWeb.OverviewLive + alias BasketWeb.Live.Overview alias BasketWeb.Live.Overview.{Search, TickerBar} @assigns_map %{__changed__: %{__context__: true}} @@ -49,7 +49,7 @@ defmodule BasketWeb.OverviewLiveTest do test "assigns empty lists to keys" do Basket.Websocket.MockAlpaca |> expect(:start_link, fn _state -> {:ok, 1} end) - assert({:ok, socket} = OverviewLive.mount([], %{}, @assigns_map)) + assert({:ok, socket} = Overview.mount([], %{}, @assigns_map)) assert( socket == %{ @@ -200,7 +200,7 @@ defmodule BasketWeb.OverviewLiveTest do assigns: %{tickers: [], basket: ^basket_with_row}, basket: [] }} = - OverviewLive.handle_event( + Overview.handle_event( "ticker-remove", %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: basket_with_row}}) @@ -213,7 +213,7 @@ defmodule BasketWeb.OverviewLiveTest do __changed__: %{__context__: true}, assigns: %{tickers: [], basket: []} }} = - OverviewLive.handle_event( + Overview.handle_event( "ticker-remove", %{"ticker" => ""}, Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}}) @@ -226,7 +226,7 @@ defmodule BasketWeb.OverviewLiveTest do __changed__: %{__context__: true}, assigns: %{tickers: [], basket: []} }} = - OverviewLive.handle_event( + Overview.handle_event( "ticker-remove", %{"ticker" => "XYZ"}, Map.merge(@assigns_map, %{ @@ -269,7 +269,7 @@ defmodule BasketWeb.OverviewLiveTest do ] } } = - OverviewLive.handle_info( + Overview.handle_info( %Phoenix.Socket.Broadcast{ topic: "bars", event: "ticker-update", From f5d229123227f48406b9fbcec0de2224cfabbf0d Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 20:44:18 -0500 Subject: [PATCH 15/17] fix map type in spec --- lib/basket_web/live/overview/ticker_add.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/basket_web/live/overview/ticker_add.ex b/lib/basket_web/live/overview/ticker_add.ex index 846320e..712b2e5 100644 --- a/lib/basket_web/live/overview/ticker_add.ex +++ b/lib/basket_web/live/overview/ticker_add.ex @@ -11,7 +11,7 @@ defmodule BasketWeb.Live.Overview.TickerAdd do @doc """ Creates a row to be added to the ticker bar table. """ - @spec call(ticker :: String.t()) :: :no_data | :market_closed | Map.t() + @spec call(ticker :: String.t()) :: :no_data | :market_closed | map() def call(ticker) do case Http.Alpaca.latest_quote(ticker) do {:ok, response} -> From 491537bdf9c7337be7bc2f6a8be13480d9c015c6 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 21:34:36 -0500 Subject: [PATCH 16/17] ticker_add tests --- lib/basket_web/live/overview/ticker_bar.ex | 35 +++++++++++++++++++ .../live/overview/ticker_bar_test.exs | 8 +++++ 2 files changed, 43 insertions(+) create mode 100644 test/basket_web/live/overview/ticker_bar_test.exs diff --git a/lib/basket_web/live/overview/ticker_bar.ex b/lib/basket_web/live/overview/ticker_bar.ex index 958fd27..d440b90 100644 --- a/lib/basket_web/live/overview/ticker_bar.ex +++ b/lib/basket_web/live/overview/ticker_bar.ex @@ -14,11 +14,32 @@ defmodule BasketWeb.Live.Overview.TickerBar do prev_value: prev_value } + @doc ~S""" + Sets a new value on a TickerBar. The prev_value is set to the current value for + computing change direction. + + ## Example + iex> set_value(%TickerBar{value: 1, prev_value: 0}, 2) + %TickerBar{value: 2, prev_value: 1} + """ @spec set_value(TickerBar.t(any(), any()), any()) :: TickerBar.t(any(), any()) def set_value(ticker_bar, value) do %TickerBar{ticker_bar | value: value, prev_value: ticker_bar.value} end + @doc ~S""" + Determines the change direction of the TickerBar based on the difference + between the current value and the previous value. + + ## Example + iex> change_direction(%TickerBar{value: 1, prev_value: 0}) + 1 + iex> change_direction(%TickerBar{value: 0, prev_value: 0}) + 0 + iex> change_direction(%TickerBar{value: 0, prev_value: 1}) + -1 + """ + @spec change_direction(%{prev_value: any(), value: any()}) :: -1 | 0 | 1 def change_direction(ticker_bar) do case change_value(ticker_bar) do x when x > 0 -> 1 @@ -27,6 +48,20 @@ defmodule BasketWeb.Live.Overview.TickerBar do end end + @doc ~S""" + Return the value of the change between the current value and the previous value. + Defaults to 0 if the value cannot be computed. + + ## Example + iex> change_value(%TickerBar{value: 10, prev_value: 4}) + 6 + iex> change_value(%TickerBar{value: 10, prev_value: 10}) + 0 + iex> change_value(%TickerBar{value: 10, prev_value: nil}) + 0 + iex> change_value(%TickerBar{value: "123", prev_value: 100}) + 0 + """ @spec change_value(%{:prev_value => any() | nil, :value => any()}) :: integer() def change_value(%{value: value, prev_value: prev_value}) do if prev_value == nil or not (is_number(value) and is_number(prev_value)) do diff --git a/test/basket_web/live/overview/ticker_bar_test.exs b/test/basket_web/live/overview/ticker_bar_test.exs new file mode 100644 index 0000000..771b58a --- /dev/null +++ b/test/basket_web/live/overview/ticker_bar_test.exs @@ -0,0 +1,8 @@ +defmodule TickerBarTest do + use ExUnit.Case, async: true + + import BasketWeb.Live.Overview.TickerBar + alias BasketWeb.Live.Overview.TickerBar + + doctest BasketWeb.Live.Overview.TickerBar, import: true +end From 925cc93435173947f79bc9166b56631670f95ff5 Mon Sep 17 00:00:00 2001 From: daveminer Date: Fri, 24 Nov 2023 23:35:20 -0500 Subject: [PATCH 17/17] add missing type to AddTicker.call/1 spec --- lib/basket_web/live/overview/ticker_add.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/basket_web/live/overview/ticker_add.ex b/lib/basket_web/live/overview/ticker_add.ex index 712b2e5..787f6da 100644 --- a/lib/basket_web/live/overview/ticker_add.ex +++ b/lib/basket_web/live/overview/ticker_add.ex @@ -11,7 +11,7 @@ defmodule BasketWeb.Live.Overview.TickerAdd do @doc """ Creates a row to be added to the ticker bar table. """ - @spec call(ticker :: String.t()) :: :no_data | :market_closed | map() + @spec call(ticker :: String.t()) :: :no_data | :market_closed | map() | {:error, String.t()} def call(ticker) do case Http.Alpaca.latest_quote(ticker) do {:ok, response} ->