From 12bb822e5965be5107de389f08cf62b1e6109fb5 Mon Sep 17 00:00:00 2001 From: daveminer Date: Wed, 1 Nov 2023 21:38:56 -0400 Subject: [PATCH 01/31] websocket auth pattern --- config/dev.exs | 8 +++- lib/basket/alpaca/ws_client.ex | 75 ++++++++++++++++++++++++++++++++++ lib/basket/application.ex | 3 +- mix.exs | 3 +- mix.lock | 1 + 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 lib/basket/alpaca/ws_client.ex diff --git a/config/dev.exs b/config/dev.exs index 79f7150..2f8d3f5 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -26,8 +26,7 @@ config :basket, BasketWeb.Endpoint, secret_key_base: "gRiKl5rvtc1Mgj3hhDzBzjHgmPE+PiG47uS8Pxk8KwjZhaaR3WxJ/I6czbmXr0j9", watchers: [ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, - tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}, - esbuild: {Esbuild, :install_and_run, [:catalogue, ~w(--sourcemap=inline --watch)]} + tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} ] # ## SSL Support @@ -68,6 +67,11 @@ config :basket, BasketWeb.Endpoint, # Enable dev routes for dashboard and mailbox config :basket, dev_routes: true +config :basket, :alpaca, + api_key: "api-key-here", + api_secret: "secret-here", + ws_server_url: "wss://stream.data.alpaca.markets/v2" + # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/lib/basket/alpaca/ws_client.ex b/lib/basket/alpaca/ws_client.ex new file mode 100644 index 0000000..bcffbb5 --- /dev/null +++ b/lib/basket/alpaca/ws_client.ex @@ -0,0 +1,75 @@ +defmodule Basket.Alpaca.WsClient do + use WebSockex + require Logger + + def start_link(state) do + IO.puts("Starting WS Client: #{iex_feed()}") + + auth_headers = [ + {"APCA-API-KEY-ID", api_key()}, + {"APCA-API-SECRET-KEY", api_secret()} + ] + + # WebSockex.start_link(iex_feed(), __MODULE__, state, extra_headers: auth_headers) + WebSockex.start_link(iex_feed(), __MODULE__, state) + end + + def handle_connect(conn, state) do + Logger.info("Connected! #{inspect(conn)} ::: #{state}") + {:ok, state} + end + + def handle_disconnect(disconnect_map, state) do + Logger.info("Local close") + # Logger.info("Local close with reason: #{inspect(disconnect_map)}") + super(disconnect_map, state) + end + + def handle_cast({:send, {type, msg} = frame}, state) do + IO.puts("Cast state: #{state}") + IO.puts("Sending #{type} frame with payload: #{msg}") + {:reply, frame, state} + end + + @doc """ + Receive a connection success message and respond with an authentication attempt. + """ + def handle_frame({type, "[{\"T\":\"success\",\"msg\":\"connected\"}]"}, state) do + Logger.info("Connection Message Received.") + + # Logger.info("API KEY: #{api_key()}") + # Logger.info("API SECRET: #{api_secret()}") + + msg = Jason.encode!(%{"action" => "auth", "key" => api_key(), "secret" => api_secret()}) + IO.inspect("MSG: #{msg}") + # IO.inspect("REsult: #{result}, Sending: #{msg}") + # msg = "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ + # \"bars\":[\"*\"]}" + {:reply, {type, msg}, state} + # {:ok, state} + end + + def handle_frame({type, "[{\"T\":\"success\",\"msg\":\"authenticated\"}]"}, state) do + Logger.info("Authenticated!") + + msg = "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ + \"bars\":[\"*\"]}" + {:reply, {type, msg}, state} + # {:ok, state} + end + + def handle_frame({_type, msg}, state) do + Logger.info("Received Message: #{msg}") + + Logger.info("State: #{state}") + {:reply, {:text, msg}, :fake_state} + end + + defp iex_feed, do: "#{ws_server_url()}/iex" + + defp ws_server_url, do: Application.fetch_env!(:basket, :alpaca)[:ws_server_url] + + defp api_key, do: Application.fetch_env!(:basket, :alpaca)[:api_key] + + defp api_secret, do: Application.fetch_env!(:basket, :alpaca)[:api_secret] +end diff --git a/lib/basket/application.ex b/lib/basket/application.ex index daa9e68..2e64d05 100644 --- a/lib/basket/application.ex +++ b/lib/basket/application.ex @@ -17,7 +17,8 @@ defmodule Basket.Application do # Start a worker by calling: Basket.Worker.start_link(arg) # {Basket.Worker, arg}, # Start to serve requests, typically the last entry - BasketWeb.Endpoint + BasketWeb.Endpoint, + Basket.Alpaca.WsClient ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/mix.exs b/mix.exs index b7acabf..d12e8c4 100644 --- a/mix.exs +++ b/mix.exs @@ -74,7 +74,8 @@ defmodule Basket.MixProject do {:credo, "~> 1.7.1", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4.2", runtime: false}, {:mix_audit, "~> 2.1.1", runtime: false}, - {:pow, "~> 1.0.34"} + {:pow, "~> 1.0.34"}, + {:websockex, "~> 0.4.3"} ] end diff --git a/mix.lock b/mix.lock index bf9cce3..52b2845 100644 --- a/mix.lock +++ b/mix.lock @@ -56,6 +56,7 @@ "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, + "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } From 2a69ac3e6a7ec86ed80e1d4907bf1cbdb1339681 Mon Sep 17 00:00:00 2001 From: daveminer Date: Wed, 1 Nov 2023 22:44:37 -0400 Subject: [PATCH 02/31] ws logger infos --- lib/basket/alpaca/ws_client.ex | 50 ++++++++++------------------------ 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/lib/basket/alpaca/ws_client.ex b/lib/basket/alpaca/ws_client.ex index bcffbb5..09659d6 100644 --- a/lib/basket/alpaca/ws_client.ex +++ b/lib/basket/alpaca/ws_client.ex @@ -2,69 +2,47 @@ defmodule Basket.Alpaca.WsClient do use WebSockex require Logger + @default_subscription_msg "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ + \"bars\":[\"*\"]}" def start_link(state) do - IO.puts("Starting WS Client: #{iex_feed()}") + Logger.info("Starting Alpaca websocket client.") - auth_headers = [ - {"APCA-API-KEY-ID", api_key()}, - {"APCA-API-SECRET-KEY", api_secret()} - ] - - # WebSockex.start_link(iex_feed(), __MODULE__, state, extra_headers: auth_headers) - WebSockex.start_link(iex_feed(), __MODULE__, state) + WebSockex.start_link(iex_feed(), __MODULE__, state, extra_headers: auth_headers()) end - def handle_connect(conn, state) do - Logger.info("Connected! #{inspect(conn)} ::: #{state}") + def handle_connect(_conn, state) do + Logger.info("Alpaca websocket connected.") {:ok, state} end def handle_disconnect(disconnect_map, state) do - Logger.info("Local close") - # Logger.info("Local close with reason: #{inspect(disconnect_map)}") + Logger.info("Alpaca websocket disconnected.") super(disconnect_map, state) end - def handle_cast({:send, {type, msg} = frame}, state) do - IO.puts("Cast state: #{state}") - IO.puts("Sending #{type} frame with payload: #{msg}") - {:reply, frame, state} - end - @doc """ Receive a connection success message and respond with an authentication attempt. """ - def handle_frame({type, "[{\"T\":\"success\",\"msg\":\"connected\"}]"}, state) do + def handle_frame({_type, "[{\"T\":\"success\",\"msg\":\"connected\"}]"}, state) do Logger.info("Connection Message Received.") - # Logger.info("API KEY: #{api_key()}") - # Logger.info("API SECRET: #{api_secret()}") - - msg = Jason.encode!(%{"action" => "auth", "key" => api_key(), "secret" => api_secret()}) - IO.inspect("MSG: #{msg}") - # IO.inspect("REsult: #{result}, Sending: #{msg}") - # msg = "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ - # \"bars\":[\"*\"]}" - {:reply, {type, msg}, state} - # {:ok, state} + {:ok, state} end def handle_frame({type, "[{\"T\":\"success\",\"msg\":\"authenticated\"}]"}, state) do - Logger.info("Authenticated!") + Logger.info("Alpaca websocket authenticated.") - msg = "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ - \"bars\":[\"*\"]}" - {:reply, {type, msg}, state} - # {:ok, state} + {:reply, {type, @default_subscription_msg}, state} end def handle_frame({_type, msg}, state) do Logger.info("Received Message: #{msg}") - Logger.info("State: #{state}") - {:reply, {:text, msg}, :fake_state} + {:ok, state} end + defp auth_headers, do: [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}] + defp iex_feed, do: "#{ws_server_url()}/iex" defp ws_server_url, do: Application.fetch_env!(:basket, :alpaca)[:ws_server_url] From 4044fe07308b8ea9bd583430e7fbfcbfcd500ed5 Mon Sep 17 00:00:00 2001 From: daveminer Date: Thu, 2 Nov 2023 13:56:59 -0400 Subject: [PATCH 03/31] receiving msgs --- lib/basket/alpaca/ws_client.ex | 30 +++++++++++++++++++++++++----- mix.exs | 2 +- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/basket/alpaca/ws_client.ex b/lib/basket/alpaca/ws_client.ex index 09659d6..f036f6a 100644 --- a/lib/basket/alpaca/ws_client.ex +++ b/lib/basket/alpaca/ws_client.ex @@ -4,43 +4,63 @@ defmodule Basket.Alpaca.WsClient do @default_subscription_msg "{\"action\":\"subscribe\",\"trades\":[\"AAPL\"],\"quotes\":[\"AMD\",\"CLDR\"],\ \"bars\":[\"*\"]}" + @auth_success_msg "[{\"T\":\"success\",\"msg\":\"authenticated\"}]" + @connection_success_msg "[{\"T\":\"success\",\"msg\":\"connected\"}]" + + @spec start_link(bitstring()) :: {:error, any()} | {:ok, pid()} def start_link(state) do Logger.info("Starting Alpaca websocket client.") WebSockex.start_link(iex_feed(), __MODULE__, state, extra_headers: auth_headers()) end + @spec handle_connect(WebsockEx.Conn.t(), bitstring()) :: {:ok, bitstring()} def handle_connect(_conn, state) do Logger.info("Alpaca websocket connected.") {:ok, state} end + @spec handle_disconnect(map(), bitstring()) :: {:ok, bitstring()} def handle_disconnect(disconnect_map, state) do Logger.info("Alpaca websocket disconnected.") super(disconnect_map, state) end @doc """ - Receive a connection success message and respond with an authentication attempt. + Handles the messages sent by the Alpaca websocket server, responding if necessary. + Besides processing messages as they arrive, this function will also set up the initial + subscription once the authorization acknowledgement method is received. """ - def handle_frame({_type, "[{\"T\":\"success\",\"msg\":\"connected\"}]"}, state) do + @spec handle_frame(WebSockex.Frame.frame(), bitstring()) :: {:ok, bitstring()} + def handle_frame({_type, @connection_success_msg}, state) do Logger.info("Connection Message Received.") {:ok, state} end - def handle_frame({type, "[{\"T\":\"success\",\"msg\":\"authenticated\"}]"}, state) do + def handle_frame({type, @auth_success_msg}, state) do Logger.info("Alpaca websocket authenticated.") {:reply, {type, @default_subscription_msg}, state} end def handle_frame({_type, msg}, state) do - Logger.info("Received Message: #{msg}") + message = + case Jason.decode(msg) do + {:ok, message} -> message + {:error, reason} -> Logger.error("Error decoding message.", reason: reason) + end + + if message["T"] == "q", do: handle_quote_message(message), + else: Logger.info("Message received: #{message}") {:ok, state} end + defp handle_quote_message(message) do + Logger.info("Quote Message: #{message}") + end + defp auth_headers, do: [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}] defp iex_feed, do: "#{ws_server_url()}/iex" @@ -50,4 +70,4 @@ defmodule Basket.Alpaca.WsClient do defp api_key, do: Application.fetch_env!(:basket, :alpaca)[:api_key] defp api_secret, do: Application.fetch_env!(:basket, :alpaca)[:api_secret] -end +end() diff --git a/mix.exs b/mix.exs index d12e8c4..1aa3092 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule Basket.MixProject do {:surface, "~> 0.11.0"}, # for surface.init; possible to remove. {:sourceror, "~> 0.12.0"}, - {:surface_catalogue, "~> 0.6.0"}, + {:surface_catalogue, "~> 0.6.1"}, {:excoveralls, "~> 0.18", only: :test}, {:sobelow, "~> 0.13.0", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7.1", only: [:dev, :test], runtime: false}, From 1282f17ddc10a6a5b7c74e1be9f68ab195d3debe Mon Sep 17 00:00:00 2001 From: daveminer Date: Sat, 4 Nov 2023 09:52:11 -0400 Subject: [PATCH 04/31] ticker search --- config/dev.exs | 7 +-- lib/basket/alpaca/http_client.ex | 55 +++++++++++++++++++++++ lib/basket/alpaca/ws_client.ex | 31 ++++++++----- lib/basket/application.ex | 3 +- lib/basket_web/components/overview.ex | 31 +++++++++++++ lib/basket_web/components/search_input.ex | 22 +++++++++ lib/basket_web/live/overview.ex | 52 +++++++++++++++++++++ lib/basket_web/router.ex | 3 +- mix.exs | 4 +- mix.lock | 15 +++++++ 10 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 lib/basket/alpaca/http_client.ex create mode 100644 lib/basket_web/components/overview.ex create mode 100644 lib/basket_web/components/search_input.ex create mode 100644 lib/basket_web/live/overview.ex diff --git a/config/dev.exs b/config/dev.exs index 2f8d3f5..c018c2f 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -68,9 +68,10 @@ config :basket, BasketWeb.Endpoint, config :basket, dev_routes: true config :basket, :alpaca, - api_key: "api-key-here", - api_secret: "secret-here", - ws_server_url: "wss://stream.data.alpaca.markets/v2" + api_key: "AKI55BR1BTU9PWT3LWBT", + api_secret: "TdBGXubqV9HwWG6YZmF8GxiCEBHrp8pLaCPultdc", + market_http_url: "https://api.alpaca.markets", + market_ws_url: "wss://stream.data.alpaca.markets/v2" # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/lib/basket/alpaca/http_client.ex b/lib/basket/alpaca/http_client.ex new file mode 100644 index 0000000..7266a7f --- /dev/null +++ b/lib/basket/alpaca/http_client.ex @@ -0,0 +1,55 @@ +defmodule Basket.Alpaca.HttpClient do + alias Mint.HTTPError + use HTTPoison.Base + + require Logger + + @resource "/v2/assets" + + def process_request_headers(headers) do + headers ++ [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}] + end + + @spec list_assets() :: {:error, any()} | {:ok, list(map())} + def list_assets() do + case get(@resource) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, body} + + {:error, error} -> + {:error, error} + end + end + + @spec list_tickers() :: list(String.t()) | {:error, any()} + def list_tickers() do + case list_assets() do + {:ok, body} -> + Enum.map(body, fn asset -> + asset["symbol"] + end) + + {:error, error} -> + {:error, error} + end + end + + def process_request_params(params) do + Map.put(params, :status, "active") + |> Map.put(:asset_class, "us_equity") + end + + def process_request_url(resource) do + "#{url()}#{resource}" + end + + def process_response_body(body) do + Jason.decode!(body) + end + + defp url, do: Application.fetch_env!(:basket, :alpaca)[:market_http_url] + + defp api_key, do: Application.fetch_env!(:basket, :alpaca)[:api_key] + + defp api_secret, do: Application.fetch_env!(:basket, :alpaca)[:api_secret] +end diff --git a/lib/basket/alpaca/ws_client.ex b/lib/basket/alpaca/ws_client.ex index f036f6a..3a36389 100644 --- a/lib/basket/alpaca/ws_client.ex +++ b/lib/basket/alpaca/ws_client.ex @@ -33,7 +33,7 @@ defmodule Basket.Alpaca.WsClient do """ @spec handle_frame(WebSockex.Frame.frame(), bitstring()) :: {:ok, bitstring()} def handle_frame({_type, @connection_success_msg}, state) do - Logger.info("Connection Message Received.") + Logger.info("Connection message received.") {:ok, state} end @@ -45,29 +45,36 @@ defmodule Basket.Alpaca.WsClient do end def handle_frame({_type, msg}, state) do - message = - case Jason.decode(msg) do - {:ok, message} -> message - {:error, reason} -> Logger.error("Error decoding message.", reason: reason) - end + IO.inspect("MSGG: #{msg}") - if message["T"] == "q", do: handle_quote_message(message), - else: Logger.info("Message received: #{message}") + case Jason.decode(msg) do + {:ok, message} -> + Logger.info("MESSS: #{inspect(message)}") + + if Map.get(List.first(message), "T") == "q" do + handle_quote_message(List.first(message)) + else + Logger.info("Message received: #{inspect(message)}") + end + + {:error, reason} -> + Logger.error("Error decoding message.", reason: reason) + end {:ok, state} end defp handle_quote_message(message) do - Logger.info("Quote Message: #{message}") + Logger.info("Quote message: #{inspect(message)}") end defp auth_headers, do: [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}] - defp iex_feed, do: "#{ws_server_url()}/iex" + defp iex_feed, do: "#{url()}/iex" - defp ws_server_url, do: Application.fetch_env!(:basket, :alpaca)[:ws_server_url] + defp url, do: Application.fetch_env!(:basket, :alpaca)[:market_ws_url] defp api_key, do: Application.fetch_env!(:basket, :alpaca)[:api_key] defp api_secret, do: Application.fetch_env!(:basket, :alpaca)[:api_secret] -end() +end diff --git a/lib/basket/application.ex b/lib/basket/application.ex index 2e64d05..cae5b0f 100644 --- a/lib/basket/application.ex +++ b/lib/basket/application.ex @@ -18,7 +18,8 @@ defmodule Basket.Application do # {Basket.Worker, arg}, # Start to serve requests, typically the last entry BasketWeb.Endpoint, - Basket.Alpaca.WsClient + Basket.Alpaca.WsClient, + {Cachex, name: :assets} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/basket_web/components/overview.ex b/lib/basket_web/components/overview.ex new file mode 100644 index 0000000..b924af9 --- /dev/null +++ b/lib/basket_web/components/overview.ex @@ -0,0 +1,31 @@ +defmodule BasketWeb.Components.Overview do + use Surface.LiveComponent + + import BasketWeb.CoreComponents + + alias BasketWeb.Components.SearchInput + + prop rows, :list, + default: [ + %{id: 1, username: "Johnnn"}, + %{id: 2, username: "Jane"} + ] + + prop tickers, :list, default: [] + + def mount(_, _, socket) do + {:ok, assign(socket, tickers: [%{id: 1, username: "Johnnn"}, %{id: 2, username: "Jane"}])} + end + + def render(assigns) do + ~F""" +
+ + + <.table id="ticker-list" rows={@tickers}> + <:col :let={ticker} label="id">{ticker} + +
+ """ + end +end diff --git a/lib/basket_web/components/search_input.ex b/lib/basket_web/components/search_input.ex new file mode 100644 index 0000000..e26301f --- /dev/null +++ b/lib/basket_web/components/search_input.ex @@ -0,0 +1,22 @@ +defmodule BasketWeb.Components.SearchInput do + use Phoenix.Component + + import Phoenix.HTML.Form + + attr :id, :string, required: true + attr :class, :string, default: nil + attr :placeholder, :string, default: "" + attr :text, :string, default: "" + + def render(assigns) do + ~H""" +
+ <%= text_input(:search_field, :query, + placeholder: @placeholder, + autofocus: true, + "phx-debounce": "300" + ) %> +
+ """ + end +end diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex new file mode 100644 index 0000000..6ba0b69 --- /dev/null +++ b/lib/basket_web/live/overview.ex @@ -0,0 +1,52 @@ +defmodule BasketWeb.Overview do + use Surface.LiveView + + require Logger + + alias BasketWeb.Components.Overview + + prop tickers, :list, default: [] + + def mount(%{"id" => id}, _, socket) do + {:ok, + socket + |> assign(:org, AsyncResult.loading()) + |> start_async(:fetch_tickers, fn -> fetch_org!(id) end)} + end + + def handle_event("ticker-search", %{"search_field" => %{"query" => query}}, socket) do + # IO.inspect("HANDLE EVENT: #{query}") + + {_status, tickers} = + Cachex.fetch(:assets, "all", fn _key -> + IO.inspect("REACTIVE") + + case Basket.Alpaca.HttpClient.list_assets() do + {:ok, result} -> + IO.inspect("OK") + + tickers = + Enum.map(result, fn asset -> + asset["symbol"] + end) + + {:commit, tickers} + + {:error, error} -> + IO.inspect("ERR: #{inspect(error)}") + Logger.error("Could not fetch tickers", error: error.reason) + {:ignore, []} + end + end) + + IO.inspect("HERE: #{inspect(tickers)}") + + {:noreply, assign(socket, :tickers, tickers)} + end + + def render(assigns) do + ~F""" + /> + """ + end +end diff --git a/lib/basket_web/router.ex b/lib/basket_web/router.ex index bc9fec1..3e5f20c 100644 --- a/lib/basket_web/router.ex +++ b/lib/basket_web/router.ex @@ -34,7 +34,8 @@ defmodule BasketWeb.Router do scope "/", BasketWeb do pipe_through [:browser, :authenticated] - get "/", PageController, :home + # get "/", PageController, :home + live "/", Overview live "/demo", Demo end diff --git a/mix.exs b/mix.exs index 1aa3092..8e91e0d 100644 --- a/mix.exs +++ b/mix.exs @@ -75,7 +75,9 @@ defmodule Basket.MixProject do {:dialyxir, "~> 1.4.2", runtime: false}, {:mix_audit, "~> 2.1.1", runtime: false}, {:pow, "~> 1.0.34"}, - {:websockex, "~> 0.4.3"} + {:websockex, "~> 0.4.3"}, + {:httpoison, "~> 2.1.0"}, + {:cachex, "~> 3.6"} ] end diff --git a/mix.lock b/mix.lock index 52b2845..e899c36 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, @@ -14,23 +16,31 @@ "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"}, + "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, + "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "mix_audit": {:hex, :mix_audit, "2.1.1", "653aa6d8f291fc4b017aa82bdb79a4017903902ebba57960ef199cbbc8c008a1", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "541990c3ab3a7bb8c4aaa2ce2732a4ae160ad6237e5dcd5ad1564f4f85354db1"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.9", "9a2b873e2cb3955efdd18ad050f1818af097fa3f5fc3a6aaba666da36bdd3f02", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83e32da028272b4bfd076c61a964e6d2b9d988378df2f1276a0ed21b13b5e997"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, @@ -42,11 +52,14 @@ "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, "pow": {:hex, :pow, "1.0.34", "51999e624475a4c75d9e5d04fcf7e38b3c5a1f8d09f37c1311d7bef43962aafa", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0 and < 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and < 4.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "63a5e3b5197a39ac0320224526fb555b2b009852d878d29efc4362537393080b"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sourceror": {:hex, :sourceror, "0.12.3", "a2ad3a1a4554b486d8a113ae7adad5646f938cad99bf8bfcef26dc0c88e8fade", [:mix], [], "hexpm", "4d4e78010ca046524e8194ffc4683422f34a96f6b82901abbb45acc79ace0316"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "surface": {:hex, :surface, "0.11.0", "95fd9a61318cd19659d69f6a5be59ae149ebe2b22569783bc0b35e4c6da4e3bd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.11", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e7530cbfdbf48263e595224cad7ac62190d1086afbc2a0794c75bf659e36fa8d"}, "surface_catalogue": {:hex, :surface_catalogue, "0.6.1", "25f9232cd8d623b040a84620390a28433d34af23747d18525c820f2d0e52a25e", [:mix], [{:earmark, "~> 1.4.21", [hex: :earmark, repo: "hexpm", optional: false]}, {:esbuild, "~> 0.2", [hex: :esbuild, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.4", [hex: :html_entities, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:surface, "~> 0.10", [hex: :surface, repo: "hexpm", optional: false]}], "hexpm", "79d25e094add1ce14a7c0480e77a8105da127a7f5f1b2f711233013497bab9bc"}, "swoosh": {:hex, :swoosh, "1.14.0", "710e363e114dedb4080b737e0307f5410887ffc9a239f818231e5618b6b84e1b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dccfc986ac99c18345ab3e1a8b934b2d817fd6d59a2494f0af78502184c71025"}, @@ -54,6 +67,8 @@ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, From 996dc6b4532b3788513844bf4dce77663cd542f3 Mon Sep 17 00:00:00 2001 From: daveminer Date: Sat, 4 Nov 2023 12:43:13 -0400 Subject: [PATCH 05/31] working ticker list --- lib/basket_web/components/overview.ex | 8 +++----- lib/basket_web/components/search_input.ex | 23 +++++++++++++++++------ lib/basket_web/live/overview.ex | 21 ++++++++++++--------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/lib/basket_web/components/overview.ex b/lib/basket_web/components/overview.ex index b924af9..0ebecef 100644 --- a/lib/basket_web/components/overview.ex +++ b/lib/basket_web/components/overview.ex @@ -20,12 +20,10 @@ defmodule BasketWeb.Components.Overview do def render(assigns) do ~F"""
- - - <.table id="ticker-list" rows={@tickers}> - <:col :let={ticker} label="id">{ticker} -
""" end end + +# +# diff --git a/lib/basket_web/components/search_input.ex b/lib/basket_web/components/search_input.ex index e26301f..a794ba8 100644 --- a/lib/basket_web/components/search_input.ex +++ b/lib/basket_web/components/search_input.ex @@ -1,21 +1,32 @@ defmodule BasketWeb.Components.SearchInput do - use Phoenix.Component + use Surface.LiveComponent import Phoenix.HTML.Form + prop tickers, :list, default: [] + attr :id, :string, required: true attr :class, :string, default: nil - attr :placeholder, :string, default: "" + attr :text, :string, default: "" + def mount(socket) do + {:ok, assign(socket, tickers: [])} + end + def render(assigns) do - ~H""" + ~F"""
- <%= text_input(:search_field, :query, - placeholder: @placeholder, + {text_input(:search_field, :query, autofocus: true, + list: "tickers", "phx-debounce": "300" - ) %> + )} + + {#for ticker <- @tickers} + + {/for} +
""" end diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 6ba0b69..99927f9 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -3,18 +3,19 @@ defmodule BasketWeb.Overview do require Logger - alias BasketWeb.Components.Overview + alias BasketWeb.Components.SearchInput + + # alias BasketWeb.Components.Overview prop tickers, :list, default: [] - def mount(%{"id" => id}, _, socket) do - {:ok, - socket - |> assign(:org, AsyncResult.loading()) - |> start_async(:fetch_tickers, fn -> fetch_org!(id) end)} + def mount(_, _, socket) do + {:ok, assign(socket, tickers: [])} + # |> assign(:org, AsyncResult.loading()) + # |> start_async(:fetch_tickers, fn -> fetch_org!(id) end)} end - def handle_event("ticker-search", %{"search_field" => %{"query" => query}}, socket) do + def handle_event("ticker-search", %{"search_field" => %{"query" => _query}}, socket) do # IO.inspect("HANDLE EVENT: #{query}") {_status, tickers} = @@ -41,12 +42,14 @@ defmodule BasketWeb.Overview do IO.inspect("HERE: #{inspect(tickers)}") - {:noreply, assign(socket, :tickers, tickers)} + {:reply, %{}, assign(socket, :tickers, tickers)} end def render(assigns) do ~F""" - /> + <.live_component module={SearchInput} id="stock-search-input" tickers={@tickers} /> """ end end + +# From 88de6f65155f0af46fcfcf2a7cf01f5262bc45e6 Mon Sep 17 00:00:00 2001 From: daveminer Date: Sat, 4 Nov 2023 13:33:47 -0400 Subject: [PATCH 06/31] cache handling on the search input --- lib/basket_web/components/overview.ex | 29 ------------- lib/basket_web/components/search_input.ex | 49 +++++++++++++++------- lib/basket_web/live/overview.ex | 50 +++++++++++------------ 3 files changed, 59 insertions(+), 69 deletions(-) delete mode 100644 lib/basket_web/components/overview.ex diff --git a/lib/basket_web/components/overview.ex b/lib/basket_web/components/overview.ex deleted file mode 100644 index 0ebecef..0000000 --- a/lib/basket_web/components/overview.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule BasketWeb.Components.Overview do - use Surface.LiveComponent - - import BasketWeb.CoreComponents - - alias BasketWeb.Components.SearchInput - - prop rows, :list, - default: [ - %{id: 1, username: "Johnnn"}, - %{id: 2, username: "Jane"} - ] - - prop tickers, :list, default: [] - - def mount(_, _, socket) do - {:ok, assign(socket, tickers: [%{id: 1, username: "Johnnn"}, %{id: 2, username: "Jane"}])} - end - - def render(assigns) do - ~F""" -
-
- """ - end -end - -# -# diff --git a/lib/basket_web/components/search_input.ex b/lib/basket_web/components/search_input.ex index a794ba8..a381789 100644 --- a/lib/basket_web/components/search_input.ex +++ b/lib/basket_web/components/search_input.ex @@ -1,33 +1,52 @@ defmodule BasketWeb.Components.SearchInput do use Surface.LiveComponent - import Phoenix.HTML.Form + import BasketWeb.CoreComponents prop tickers, :list, default: [] attr :id, :string, required: true attr :class, :string, default: nil - attr :text, :string, default: "" + prop ticker_search_form, :string, default: "" def mount(socket) do - {:ok, assign(socket, tickers: [])} + form = to_form(%{"ticker_search_field" => ""}) + socket = assign(socket, :ticker_search_form, form) + socket = assign(socket, :tickers, []) + + {:ok, socket} end def render(assigns) do ~F""" -
- {text_input(:search_field, :query, - autofocus: true, - list: "tickers", - "phx-debounce": "300" - )} - - {#for ticker <- @tickers} - - {/for} - -
+
+ <.simple_form for={@ticker_search_form} phx-change="ticker-search"> + <.input + name="selected-ticker" + value="" + field={@ticker_search_form["ticker_search_field"]} + list="tickers" + phx-debounce="500" + errors={["TODO"]} + /> + + + {#for ticker <- @tickers} + + {/for} + + + <.button> + Add ticker + +
""" end end + +# {text_input(:search_field, :query, +# autofocus: true, +# list: "tickers", +# "phx-debounce": "300" +# )} diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index 99927f9..c236bdf 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -5,8 +5,6 @@ defmodule BasketWeb.Overview do alias BasketWeb.Components.SearchInput - # alias BasketWeb.Components.Overview - prop tickers, :list, default: [] def mount(_, _, socket) do @@ -15,34 +13,38 @@ defmodule BasketWeb.Overview do # |> start_async(:fetch_tickers, fn -> fetch_org!(id) end)} end - def handle_event("ticker-search", %{"search_field" => %{"query" => _query}}, socket) do - # IO.inspect("HANDLE EVENT: #{query}") + def handle_event("ticker-search", %{"selected-ticker" => _query}, socket) do + # IO.inspect("SOCKET: #{inspect(socket.assigns)}") - {_status, tickers} = - Cachex.fetch(:assets, "all", fn _key -> - IO.inspect("REACTIVE") + if length(socket.assigns.tickers) > 0 do + {:noreply, socket} + else + {_status, tickers} = + Cachex.fetch(:assets, "all", fn _key -> + IO.inspect("REACTIVE") - case Basket.Alpaca.HttpClient.list_assets() do - {:ok, result} -> - IO.inspect("OK") + case Basket.Alpaca.HttpClient.list_assets() do + {:ok, result} -> + IO.inspect("OK") - tickers = - Enum.map(result, fn asset -> - asset["symbol"] - end) + tickers = + Enum.map(result, fn asset -> + asset["symbol"] + end) - {:commit, tickers} + {:commit, tickers} - {:error, error} -> - IO.inspect("ERR: #{inspect(error)}") - Logger.error("Could not fetch tickers", error: error.reason) - {:ignore, []} - end - end) + {:error, error} -> + IO.inspect("ERR: #{inspect(error)}") + Logger.error("Could not fetch tickers", error: error.reason) + {:ignore, []} + end + end) - IO.inspect("HERE: #{inspect(tickers)}") + IO.inspect("HERE: #{inspect(tickers)}") - {:reply, %{}, assign(socket, :tickers, tickers)} + {:reply, %{}, assign(socket, :tickers, tickers)} + end end def render(assigns) do @@ -51,5 +53,3 @@ defmodule BasketWeb.Overview do """ end end - -# From 79af5680fa5314dcc5fb23e206d62af38fe82011 Mon Sep 17 00:00:00 2001 From: daveminer Date: Sat, 4 Nov 2023 15:21:53 -0400 Subject: [PATCH 07/31] add ticker to liveview list --- lib/basket_web/components/search_input.ex | 11 +++++++---- lib/basket_web/live/overview.ex | 14 +++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/basket_web/components/search_input.ex b/lib/basket_web/components/search_input.ex index a381789..4b6fb3e 100644 --- a/lib/basket_web/components/search_input.ex +++ b/lib/basket_web/components/search_input.ex @@ -11,6 +11,7 @@ defmodule BasketWeb.Components.SearchInput do prop ticker_search_form, :string, default: "" def mount(socket) do + # TODO: make ticker call async form = to_form(%{"ticker_search_field" => ""}) socket = assign(socket, :ticker_search_form, form) socket = assign(socket, :tickers, []) @@ -21,7 +22,7 @@ defmodule BasketWeb.Components.SearchInput do def render(assigns) do ~F"""
- <.simple_form for={@ticker_search_form} phx-change="ticker-search"> + <.simple_form for={@ticker_search_form} phx-change="ticker-search" phx-submit="ticker-add"> <.input name="selected-ticker" value="" @@ -36,10 +37,12 @@ defmodule BasketWeb.Components.SearchInput do {/for} + <:actions> + <.button> + Add ticker + + - <.button> - Add ticker -
""" end diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex index c236bdf..11a5c6d 100644 --- a/lib/basket_web/live/overview.ex +++ b/lib/basket_web/live/overview.ex @@ -1,6 +1,8 @@ defmodule BasketWeb.Overview do use Surface.LiveView + import BasketWeb.CoreComponents + require Logger alias BasketWeb.Components.SearchInput @@ -8,7 +10,9 @@ defmodule BasketWeb.Overview do prop tickers, :list, default: [] def mount(_, _, socket) do - {:ok, assign(socket, tickers: [])} + socket = assign(socket, tickers: []) + socket = assign(socket, basket: []) + {:ok, socket} # |> assign(:org, AsyncResult.loading()) # |> start_async(:fetch_tickers, fn -> fetch_org!(id) end)} end @@ -47,9 +51,17 @@ defmodule BasketWeb.Overview do end end + def handle_event("ticker-add", %{"selected-ticker" => ticker}, socket) do + IO.inspect("TICKER: #{ticker}") + {:reply, %{}, assign(socket, :basket, socket.assigns.basket ++ [ticker])} + end + def render(assigns) do ~F""" <.live_component module={SearchInput} id="stock-search-input" tickers={@tickers} /> + <.table id="ticker-list" rows={@basket}> + <:col :let={ticker} label="ticker">{ticker} + """ end end From f2e52ebf0b553a5709ef19c673a649921b7f6d5f Mon Sep 17 00:00:00 2001 From: daveminer Date: Sat, 4 Nov 2023 15:43:49 -0400 Subject: [PATCH 08/31] remove ticker --- lib/basket_web/components/layouts/root.html.heex | 4 ++-- lib/basket_web/live/overview.ex | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/basket_web/components/layouts/root.html.heex b/lib/basket_web/components/layouts/root.html.heex index 46e00c5..18852b1 100644 --- a/lib/basket_web/components/layouts/root.html.heex +++ b/lib/basket_web/components/layouts/root.html.heex @@ -4,8 +4,8 @@ - <.live_title suffix=" ยท Phoenix Framework"> - <%= assigns[:page_title] || "Basket" %> + <.live_title> + <%= assigns[:page_title] || "Stock Basket" %>