Skip to content

Commit

Permalink
Merge pull request #15 from daveminer/server-ticker-state
Browse files Browse the repository at this point in the history
Server ticker state
  • Loading branch information
daveminer authored Apr 3, 2024
2 parents 1fe74f8 + c0b2691 commit 12809d9
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 179 deletions.
96 changes: 50 additions & 46 deletions assets/js/_hooks/CellValueStore.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,62 @@
export const CellValueStore = {
mounted() {
this.prevValues = {}; // Initialize an empty object to store previous values
this.updatePrevValues(); // Set initial previous values on mount

this.handleEvent("run-updated", () => {
// Your updated() function code here
this.flashUpdatedCells(); // Flash cells with updated values
this.updatePrevValues(); // Update previous values after flashing
});
},
// updated() {
// this.flashUpdatedCells(); // Flash cells with updated values
// this.updatePrevValues(); // Update previous values after flashing
// },
updatePrevValues() {
// Loop through each cell and update the store with its current value
this.el.querySelectorAll('td[data-key]').forEach((cell) => {
const key = cell.getAttribute('data-key'); // Assume each cell has a unique data-key attribute
this.prevValues[key] = cell.textContent || cell.innerText;
this.handleEvent("ticker-update-received", (values) => {
const id = values["S"];
// select the table row with id equal to id
const row = document.querySelector(`tr[id="${id}"]`);
this.highlightChanges(row, values);
});
},
flashUpdatedCells() {
let selector = this.el.querySelectorAll('td[data-key]')
console.log(selector, "SELECTOR")
this.el.querySelectorAll('td[data-key]').forEach((cell) => {
const key = cell.getAttribute('data-key');
console.log(key, "KEY")
const currentValue = cell.textContent || cell.innerText;
const prevValue = this.prevValues[key];

if (!prevValue || !isFinite(Number(currentValue))) return; // Skip if no previous value or not a number
highlightChanges(row, values) {
const cells = row.querySelectorAll('td[data-key]')
// Loop through each cell in the row and update the text content
cells.forEach((cell) => {
const dataKey = cell.getAttribute('data-key');
const [_ticker, key] = dataKey.split('_');

const prevNum = parseFloat(prevValue);
const currentNum = parseFloat(currentValue);
let value;
switch (key) {
case 'open':
value = values['o'];
break;
case 'high':
value = values['h'];
break;
case 'low':
value = values['l'];
break;
case 'close':
value = values['c'];
break;
case 'volume':
value = values['v'];
break;
default:
break;
}

if (isFinite(prevNum) && isFinite(currentNum)) {
if (prevNum < currentNum) {
this.applyFlash(cell, 'increase');
} else if (prevNum > currentNum) {
this.applyFlash(cell, 'decrease');
if (isFinite(value)) {
// Multiple content slots
const content = cell.querySelectorAll('[id$=content-slot]')
const oldValue = content[0].textContent;
content[0].textContent = value;
if (oldValue && isFinite(oldValue)) {
const oldNum = parseFloat(oldValue);
const newNum = parseFloat(value);
if (oldNum < newNum) {
cell.classList.add('bg-emerald-300');
setTimeout(() => {
cell.classList.remove('bg-emerald-300');
}, 3000);
} else if (oldNum > newNum) {
cell.classList.add('bg-rose-300');
setTimeout(() => {
cell.classList.remove('bg-rose-300');
}, 3000);
}
}
}
});
},
applyFlash(cell, changeType) {
console.log("APPLYING FLASH")
// Apply classes based on whether the value increased or decreased
const flashClass = changeType === 'increase' ? 'bg-emerald-300' : 'bg-rose-300';
cell.classList.add(flashClass);

setTimeout(() => {
console.log("TIMEOUT RUN")
cell.classList.remove(flashClass)
}, 3000
);
}
};
45 changes: 0 additions & 45 deletions assets/js/_hooks/TickerHandler.js

This file was deleted.

2 changes: 0 additions & 2 deletions assets/js/_hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { CellValueStore } from './CellValueStore'
import { TickerHandler } from './TickerHandler'

const Hooks = {}
Hooks.CellValueStore = CellValueStore
Hooks.TickerHandler = TickerHandler

export default Hooks
1 change: 1 addition & 0 deletions lib/basket/http/alpaca/bars.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Basket.Http.Alpaca.Bars do
Typed instance of a stock chart bars.
"""

@derive Jason.Encoder
defstruct [:id, :ticker, :close, :open, :high, :low, :volume, :timestamp, :count, :vwap]

@type t :: %__MODULE__{
Expand Down
9 changes: 6 additions & 3 deletions lib/basket_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ defmodule BasketWeb.CoreComponents do

~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table id="ticker-table" phx-hook="TickerHandler" class="w-[40rem] mt-11 sm:w-full">
<table id="ticker-table" class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 text-center font-normal">
Expand All @@ -526,7 +526,7 @@ defmodule BasketWeb.CoreComponents do
<tbody
id={@id}
phx-hook="CellValueStore"
phx-update="append"
phx-update="replace"
class="relative divide-y divide-zinc-100 border-t border-zinc-200 leading-6"
>
<tr :for={row <- @rows} id={row.id} class="group hover:bg-zinc-50 text-sm text-zinc-700">
Expand All @@ -542,7 +542,10 @@ defmodule BasketWeb.CoreComponents do
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<span
id={"#{row.id}-#{col[:key]}-content-slot"}
class={["relative", i == 0 && "font-semibold text-zinc-900"]}
>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
Expand Down
79 changes: 37 additions & 42 deletions lib/basket_web/live/overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,45 @@ defmodule BasketWeb.Live.Overview do

alias Basket.Ticker
alias BasketWeb.Components.NavRow
alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBarTable, TickerRow}
alias BasketWeb.Live.Overview.{Search, TickerAdd, TickerBarTable}
alias BasketWeb.Presence

on_mount {BasketWeb.Live.UserLiveAuth, :user}

@initial_temp_assigns [basket: []]

def mount(_, _, socket) do
socket = assign(socket, :basket, [])

if connected?(socket) do
tickers = load_user_tickers(socket.assigns.user)

if tickers != [] do
result = TickerAdd.call(tickers, socket.assigns.user.id)
new_socket = handle_ticker_add_result(result, socket)
{:ok, new_socket, temporary_assigns: @initial_temp_assigns}
socket =
if connected?(socket) do
tickers = load_user_tickers(socket.assigns.user)

if tickers != [] do
add_ticker(socket, tickers)
else
socket
end
else
{:ok, socket, temporary_assigns: @initial_temp_assigns}
socket
end
else
{:ok, socket, temporary_assigns: @initial_temp_assigns}
end

{:ok, socket}
end

def handle_info({"ticker-add", %{"ticker" => ticker}}, socket) do
if ticker in tickers(socket) or String.trim(ticker) == "" do
{:noreply, socket}
else
Ticker.add(socket.assigns.user, ticker)
result = TickerAdd.call(ticker, socket.assigns.user.id)
new_socket = handle_ticker_add_result(result, socket, add_method: :append)
{:noreply, new_socket}

{:noreply, add_ticker(socket, ticker)}
end
end

def handle_info(
%Phoenix.Socket.Broadcast{topic: _topic, event: "ticker-update", payload: payload},
socket
) do
updated_socket = update(socket, :basket, fn _basket -> [TickerRow.new(payload)] end)
updated_socket = push_event(socket, "ticker-update-received", payload)

{:noreply, updated_socket}
end
Expand All @@ -67,7 +65,12 @@ defmodule BasketWeb.Live.Overview do
Ticker.remove(socket.assigns.user, ticker)
Presence.untrack(self(), "bars-#{ticker}", socket.assigns.user.id)

socket = push_event(socket, "ticker-removed", %{ticker: ticker})
socket =
assign(
socket,
:basket,
Enum.reject(socket.assigns.basket, fn row -> row.ticker == ticker end)
)

{:noreply, socket}
end
Expand All @@ -84,6 +87,17 @@ defmodule BasketWeb.Live.Overview do
"""
end

defp add_ticker(socket, tickers) do
case TickerAdd.call(tickers, socket.assigns.user.id) do
{:error, error} ->
Logger.error("Could not add ticker: #{error}")
socket

{:ok, %{bars: bars}} ->
handle_ticker_add_result(bars, socket)
end
end

defp load_user_tickers(user) do
case Ticker.for_user(user) do
[] ->
Expand All @@ -94,31 +108,12 @@ defmodule BasketWeb.Live.Overview do
end
end

# Define the default value for opts here, in the header.
defp handle_ticker_add_result(result, socket, opts \\ [])

defp handle_ticker_add_result(
{:ok, %{bars: bar_rows, tickers_not_found: tickers_not_found}},
socket,
_opts
bar_rows,
socket
) do
socket =
if tickers_not_found != [] do
put_flash(
socket,
:info,
"No data for tickers: #{Enum.join(tickers_not_found, ", ")}"
)
else
socket
end

update(socket, :basket, fn _basket -> bar_rows end)
end

defp handle_ticker_add_result({:error, error}, socket, _opts) do
Logger.error("Could not subscribe to ticker: #{error}")
socket
new_rows = Enum.sort(socket.assigns.basket ++ bar_rows, fn a, b -> a.ticker < b.ticker end)
assign(socket, :basket, new_rows)
end

defp tickers(socket), do: Enum.map(socket.assigns.basket, fn row -> row.ticker end)
Expand Down
41 changes: 0 additions & 41 deletions test/basket_web/live/overview_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -99,46 +99,5 @@ defmodule BasketWeb.Live.OverviewTest do
{:ok, _empty_view, _html} = live(conn, "/")
assert render(view) =~ "<input id=\"ticker-input\""
end

test "updates a TickerRow when a message with new bars is received", %{
conn: conn
} do
Basket.Http.MockAlpaca
|> expect(:latest_quote, fn _ -> {:ok, %{"bars" => build(:bars_payload)}} end)

Basket.Websocket.MockClient |> expect(:send_frame, 2, fn _, _ -> :ok end)

{:ok, view, _html} = live(conn, "/")

assert render(view) =~
"<span class=\"relative \">\n 43025\n </span>"

# Send update
send(
view.pid,
%Phoenix.Socket.Broadcast{
topic: "bars-ALPHA",
event: "ticker-update",
payload: %{
"S" => "XYZ",
"T" => "b",
"c" => 191.285,
"h" => 191.37,
"l" => 191.23,
"n" => 50,
"o" => 191.23,
"t" => "2023-11-20T16:24:00Z",
"v" => 5433,
"vw" => 191.328043
}
}
)

# Remove the last ticker so the Presence topic is left before the mock is gone.
render_hook(view, "ticker-remove", %{"ticker" => "ALPHA"})

# Check for update
assert render(view) =~ "<span class=\"relative \">\n 5433\n </span>"
end
end
end

0 comments on commit 12809d9

Please sign in to comment.