Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tailwind setup #7

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
12bb822
websocket auth pattern
daveminer Nov 2, 2023
2a69ac3
ws logger infos
daveminer Nov 2, 2023
4044fe0
receiving msgs
daveminer Nov 2, 2023
1282f17
ticker search
daveminer Nov 4, 2023
996dc6b
working ticker list
daveminer Nov 4, 2023
88de6f6
cache handling on the search input
daveminer Nov 4, 2023
79af568
add ticker to liveview list
daveminer Nov 4, 2023
f2e52eb
remove ticker
daveminer Nov 4, 2023
7b94292
send async ws subscription
daveminer Nov 5, 2023
c5ef122
handle ws bar messages
daveminer Nov 6, 2023
74719d2
add pubsub for bars updates
daveminer Nov 7, 2023
150794e
cell value change indication
daveminer Nov 9, 2023
b245565
websocket cell updates
daveminer Nov 9, 2023
65eba6d
update background color
daveminer Nov 9, 2023
77ab061
layout elements
daveminer Nov 10, 2023
593ab2a
remove unused dep from lockfile
daveminer Nov 10, 2023
c23115d
navbar
daveminer Nov 11, 2023
57e16ca
formatting
daveminer Nov 11, 2023
072ba56
remove unused dark mode component param
daveminer Nov 11, 2023
7e98030
remove unused flex class
daveminer Nov 11, 2023
4f3fe80
search box placement
daveminer Nov 11, 2023
7179e8e
lint
daveminer Nov 11, 2023
3ff0780
fix component render and attach darkmode hook to button
daveminer Nov 11, 2023
f0ffa82
lint
daveminer Nov 11, 2023
0fe80e9
credo suggestion changes
daveminer Nov 11, 2023
68e6b46
change inspect to logger error
daveminer Nov 11, 2023
edebe59
remove unneeded map check
daveminer Nov 11, 2023
48b7353
fix import order
daveminer Nov 11, 2023
9dccb25
fix remaining metadata logger issues
daveminer Nov 11, 2023
e7ea9b3
fix capitalization bug
daveminer Nov 11, 2023
f519029
fix cap error again
daveminer Nov 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Hooks from "./_hooks"
import { toggleDarkMode } from "./darkMode"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})
Expand All @@ -31,6 +32,8 @@ topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

window.addEventListener("toggle-darkmode", _e => toggleDarkMode())

// connect if there are any LiveViews on the page
liveSocket.connect()

Expand All @@ -39,4 +42,3 @@ liveSocket.connect()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

18 changes: 18 additions & 0 deletions assets/js/darkMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

function darkExpected() {
return localStorage.theme === 'dark' || (!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
}

function setMode() {
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (darkExpected()) document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
}

export function toggleDarkMode() {
console.log("TOGGLEDARKMODE")
if (darkExpected()) localStorage.theme = 'light';
else localStorage.theme = 'dark';
setMode();
}
18 changes: 16 additions & 2 deletions assets/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration

const colors = require('tailwindcss/colors')
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
Expand All @@ -11,11 +12,24 @@ module.exports = {
"../lib/basket_web.ex",
"../lib/basket_web/**/*.*ex"
],
darkMode: "class",
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
primary: {
light: colors.emerald-800,
dark: colors.emerald-800
},
secondary: '#7a869a',
accent: '#ff5630',
background: {
light: colors.white,
dark: colors.emerald-800
},
surface: '#2c313a',
onSurface: '#ffffff',
error: '#de350b',
},
},
},
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ config :esbuild,

# Configure tailwind (the version is required)
config :tailwind,
version: "3.3.2",
version: "3.3.5",
default: [
args: ~w(
--config=tailwind.config.js
Expand Down
10 changes: 8 additions & 2 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,6 +67,13 @@ config :basket, BasketWeb.Endpoint,
# Enable dev routes for dashboard and mailbox
config :basket, dev_routes: true

config :basket, :alpaca,
api_key: System.get_env("ALPACA_API_KEY"),
api_secret: System.get_env("ALPACA_API_SECRET"),
data_http_url: "https://data.alpaca.markets",
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"

Expand Down
54 changes: 54 additions & 0 deletions lib/basket/alpaca/http/client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
defmodule Basket.Alpaca.HttpClient do
@moduledoc """
HTTP client for Alpaca API.
"""

use HTTPoison.Base

require Logger

@assets_resource "/v2/assets"
@latest_quotes_resource "/v2/stocks/bars/latest"

def process_request_headers(headers) do
headers ++ [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}]
end

@spec latest_quote(String.t()) :: {:error, any()} | {:ok, map()}
def latest_quote(ticker) do
case get("#{data_url()}#{@latest_quotes_resource}", [],
params: %{feed: "iex", symbols: ticker}
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, body}

{:error, error} ->
{:error, error}
end
end

@spec list_assets() :: {:error, any()} | {:ok, list(map())}
def list_assets do
case get("#{market_url()}#{@assets_resource}", [],
params: %{status: "active", asset_class: "us_equity"}
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, body}

{:error, error} ->
{:error, error}
end
end

def process_response_body(body) do
Jason.decode!(body)
end

defp data_url, do: Application.fetch_env!(:basket, :alpaca)[:data_http_url]

defp market_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
113 changes: 113 additions & 0 deletions lib/basket/alpaca/ws/client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
defmodule Basket.Alpaca.Websocket.Client do
@moduledoc """
Instantiates an Alpaca websocket connection and processes the incoming messages.
Currently only handles "bars" messages.
"""

use WebSockex
require Logger

alias Basket.Alpaca.Websocket.Message

@auth_success ~s([{\"T\":\"success\",\"msg\":\"authenticated\"}])
@connection_success ~s([{\"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 """
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.
"""
@spec handle_frame(WebSockex.Frame.frame(), bitstring()) :: {:ok, bitstring()}
def handle_frame({_type, @connection_success}, state) do
Logger.info("Connection message received.")

{:ok, state}
end

def handle_frame({_type, @auth_success}, state) do
Logger.info("Alpaca websocket authenticated.")

{:ok, state}
end

def handle_frame({_tpe, msg}, state) do
case Jason.decode(msg) do
{:ok, message} ->
Message.process(message)

{:error, error} ->
Logger.error("Error decoding message: #{inspect(error)}")
end

{:ok, state}
end

@spec subscribe_to_market_data(Message.subscription_fields()) :: :ok
def subscribe_to_market_data(tickers) do
case Message.market_data_subscription(tickers) do
{:ok, message} ->
WebSockex.send_frame(client_pid(), {:text, message})
Logger.debug("Subscription message sent: #{inspect(message)}")

{:error, error} ->
Logger.error("Error sending subscription message: #{inspect(error)}")
end
end

@spec unsubscribe_to_market_data(Message.subscription_fields()) :: :ok
def unsubscribe_to_market_data(tickers) do
case Message.market_data_remove_subscription(tickers) do
{:ok, message} ->
Logger.info("Sending subscription removal message: #{inspect(message)}")

WebSockex.send_frame(client_pid(), {:text, message})
Logger.info("Subscription removal message sent: #{inspect(message)}")

{:error, error} ->
Logger.error("Error sending subscription removal message: #{inspect(error)}")
end
end

defp auth_headers, do: [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}]

defp iex_feed, do: "#{url()}/iex"

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]

defp client_pid,
do:
Supervisor.which_children(Basket.Supervisor)
|> Enum.find(fn c ->
case c do
{Basket.Alpaca.Websocket.Client, _pid, :worker, [Basket.Alpaca.Websocket.Client]} ->
true

_ ->
false
end
end)
|> elem(1)
end
Loading
Loading