aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx
blob: 4327f1caa255c7a77a5c6f294cecfd76927542e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import * as Uebersicht from "uebersicht";
import * as DataWidget from "./data-widget.jsx";
import * as DataWidgetLoader from "./data-widget-loader.jsx";
import * as Icons from "../icons/icons.jsx";
import useWidgetRefresh from "../../hooks/use-widget-refresh";
import useServerSocket from "../../hooks/use-server-socket";
import { useSimpleBarContext } from "../simple-bar-context.jsx";
import * as Utils from "../../utils";

export { stockStyles as styles } from "../../styles/components/data/stock";

const { React } = Uebersicht;

const DEFAULT_REFRESH_FREQUENCY = 15 * 60 * 1000; // 15 min

/**
 * Stock widget component
 * @returns {JSX.Element|null} The stock widget
 */
export const Widget = React.memo(() => {
  const { displayIndex, settings, pushMissive } = useSimpleBarContext();
  const { widgets, stockWidgetOptions } = settings;
  const { stockWidget } = widgets;
  const {
    refreshFrequency,
    yahooFinanceApiKey,
    symbols,
    showSymbol,
    showCurrency,
    showMarketPrice,
    showMarketChange,
    showMarketPercent,
    showColor,
    showOnDisplay,
    showIcon,
  } = stockWidgetOptions;

  // Determine the refresh frequency for the widget
  const refresh = React.useMemo(
    () =>
      Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
    [refreshFrequency],
  );

  // Check if the widget should be visible on the current display
  const visible =
    Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && stockWidget;

  const ref = React.useRef();

  // Memoize cleanedUpSymbols to prevent recreation on every render
  const cleanedUpSymbols = React.useMemo(
    () => symbols.replace(/ /g, ""),
    [symbols],
  );

  // Memoize enumeratedSymbols to prevent recreation on every render
  const enumeratedSymbols = React.useMemo(
    () => cleanedUpSymbols.replace(/ /g, "").split(","),
    [cleanedUpSymbols],
  );

  const [state, setState] = React.useState();
  const [loading, setLoading] = React.useState(visible);

  /**
   * Reset the widget state
   */
  const resetWidget = () => {
    setState(undefined);
    setLoading(false);
  };

  /**
   * Fetch stock data from Yahoo Finance API
   */
  const getStocks = React.useCallback(async () => {
    if (!visible) return;
    const response = await fetch(
      `https://yfapi.net/v6/finance/quote?symbols=${cleanedUpSymbols}`,
      {
        headers: new Headers({ "x-api-key": yahooFinanceApiKey }),
      },
    );
    if (response.status === 429) {
      // Exceeded daily quota
    } else if (response.status === 403) {
      Utils.notification("Invalid Yahoo Finance API key", pushMissive);
    } else if (response.status === 200) {
      const result = await response.json();
      const symbolQuotes = result.quoteResponse.result;

      if (symbolQuotes.length !== enumeratedSymbols.length) {
        // One or more symbols is invalid
        const receivedSymbols = symbolQuotes.map((s) => s.symbol.toLowerCase());
        const invalidSymbols = enumeratedSymbols.filter(
          (s) => !receivedSymbols.includes(s),
        );
        Utils.notification("Invalid stock symbol(s): " + invalidSymbols);
      } else {
        setState({ symbolQuotes });
      }
    } else {
      // eslint-disable-next-line no-console
      console.error("Unexpected response from Yahoo Finance API: ", response);
    }

    setLoading(false);
  }, [
    visible,
    cleanedUpSymbols,
    yahooFinanceApiKey,
    pushMissive,
    enumeratedSymbols,
  ]);

  // Use server socket to fetch stock data
  useServerSocket("stock", visible, getStocks, resetWidget, setLoading);
  // Refresh widget data periodically
  useWidgetRefresh(visible, getStocks, refresh);

  /**
   * Refresh stock data on user interaction
   * @param {Event} e - The event object
   */
  const refreshStocks = (e) => {
    Utils.clickEffect(e);
    setLoading(true);
    getStocks();
  };

  if (loading) return <DataWidgetLoader.Widget className="stock" />;
  if (!state || !state.symbolQuotes) return null;

  return enumeratedSymbols.map((symbolName, i) => {
    const symbolQuote = state.symbolQuotes[i];
    const symbol = symbolQuote.symbol;
    const currencySymbol = getCurrencySymbol(symbolQuote.currency);
    const marketPrice = Number(symbolQuote.regularMarketPrice).toFixed(2);
    const marketChange = Number(symbolQuote.regularMarketChange).toFixed(2);
    const marketPercentChange = Number(
      symbolQuote.regularMarketChangePercent,
    ).toFixed(2);
    const stockUp = !marketChange.startsWith("-");

    return (
      <DataWidget.Widget
        key={symbolName}
        classes="stock"
        ref={ref}
        Icon={showIcon ? (stockUp ? Icons.UpArrow : Icons.DownArrow) : null}
        href={`https://finance.yahoo.com/quote/${symbolName}`}
        onClick={openStock}
        onRightClick={refreshStocks}
      >
        <span>
          {showSymbol && symbol}
          {(showCurrency || showMarketPrice) && showSymbol && (
            <span>&nbsp;</span>
          )}
          {showCurrency && currencySymbol}
          {showMarketPrice && marketPrice}
        </span>
        <span className={showColor ? (stockUp ? "stockUp" : "stockDown") : ""}>
          {(showMarketChange || showMarketPercent) && <span>&nbsp;</span>}
          {showMarketChange && formatPriceChange(marketChange)}
          {showMarketPercent &&
            showMarketChange &&
            ` (${formatPriceChange(marketPercentChange)}%)`}
          {showMarketPercent &&
            !showMarketChange &&
            `${formatPriceChange(marketPercentChange)}%`}
        </span>
      </DataWidget.Widget>
    );
  });
});

Widget.displayName = "Stock";

/**
 * Handle click event to open stock details
 * @param {Event} e - The event object
 */
function openStock(e) {
  Utils.clickEffect(e);
}

/**
 * Get the currency symbol for a given currency code
 * @param {string} currency - The currency code
 * @returns {string} The currency symbol
 */
function getCurrencySymbol(currency) {
  if (currency === "EUR") return "€";
  if (currency === "USD") return "$";
  if (currency === "GBP") return "£";
  if (currency === "CNY") return "¥";
  if (currency === "JPY") return "¥";
  return `${currency} `;
}

/**
 * Format the price change with a '+' for positive changes
 * @param {string} priceChange - The price change
 * @returns {string} The formatted price change
 */
function formatPriceChange(priceChange) {
  // Add a '+' for positive changes
  return (priceChange.startsWith("-") ? "" : "+") + priceChange;
}