aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx')
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx211
1 files changed, 211 insertions, 0 deletions
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx
new file mode 100755
index 0000000..4327f1c
--- /dev/null
+++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx
@@ -0,0 +1,211 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { stockStyles as styles } from "../../styles/components/data/stock";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 15 * 60 * 1000; // 15 min
15
16/**
17 * Stock widget component
18 * @returns {JSX.Element|null} The stock widget
19 */
20export const Widget = React.memo(() => {
21 const { displayIndex, settings, pushMissive } = useSimpleBarContext();
22 const { widgets, stockWidgetOptions } = settings;
23 const { stockWidget } = widgets;
24 const {
25 refreshFrequency,
26 yahooFinanceApiKey,
27 symbols,
28 showSymbol,
29 showCurrency,
30 showMarketPrice,
31 showMarketChange,
32 showMarketPercent,
33 showColor,
34 showOnDisplay,
35 showIcon,
36 } = stockWidgetOptions;
37
38 // Determine the refresh frequency for the widget
39 const refresh = React.useMemo(
40 () =>
41 Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
42 [refreshFrequency],
43 );
44
45 // Check if the widget should be visible on the current display
46 const visible =
47 Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && stockWidget;
48
49 const ref = React.useRef();
50
51 // Memoize cleanedUpSymbols to prevent recreation on every render
52 const cleanedUpSymbols = React.useMemo(
53 () => symbols.replace(/ /g, ""),
54 [symbols],
55 );
56
57 // Memoize enumeratedSymbols to prevent recreation on every render
58 const enumeratedSymbols = React.useMemo(
59 () => cleanedUpSymbols.replace(/ /g, "").split(","),
60 [cleanedUpSymbols],
61 );
62
63 const [state, setState] = React.useState();
64 const [loading, setLoading] = React.useState(visible);
65
66 /**
67 * Reset the widget state
68 */
69 const resetWidget = () => {
70 setState(undefined);
71 setLoading(false);
72 };
73
74 /**
75 * Fetch stock data from Yahoo Finance API
76 */
77 const getStocks = React.useCallback(async () => {
78 if (!visible) return;
79 const response = await fetch(
80 `https://yfapi.net/v6/finance/quote?symbols=${cleanedUpSymbols}`,
81 {
82 headers: new Headers({ "x-api-key": yahooFinanceApiKey }),
83 },
84 );
85 if (response.status === 429) {
86 // Exceeded daily quota
87 } else if (response.status === 403) {
88 Utils.notification("Invalid Yahoo Finance API key", pushMissive);
89 } else if (response.status === 200) {
90 const result = await response.json();
91 const symbolQuotes = result.quoteResponse.result;
92
93 if (symbolQuotes.length !== enumeratedSymbols.length) {
94 // One or more symbols is invalid
95 const receivedSymbols = symbolQuotes.map((s) => s.symbol.toLowerCase());
96 const invalidSymbols = enumeratedSymbols.filter(
97 (s) => !receivedSymbols.includes(s),
98 );
99 Utils.notification("Invalid stock symbol(s): " + invalidSymbols);
100 } else {
101 setState({ symbolQuotes });
102 }
103 } else {
104 // eslint-disable-next-line no-console
105 console.error("Unexpected response from Yahoo Finance API: ", response);
106 }
107
108 setLoading(false);
109 }, [
110 visible,
111 cleanedUpSymbols,
112 yahooFinanceApiKey,
113 pushMissive,
114 enumeratedSymbols,
115 ]);
116
117 // Use server socket to fetch stock data
118 useServerSocket("stock", visible, getStocks, resetWidget, setLoading);
119 // Refresh widget data periodically
120 useWidgetRefresh(visible, getStocks, refresh);
121
122 /**
123 * Refresh stock data on user interaction
124 * @param {Event} e - The event object
125 */
126 const refreshStocks = (e) => {
127 Utils.clickEffect(e);
128 setLoading(true);
129 getStocks();
130 };
131
132 if (loading) return <DataWidgetLoader.Widget className="stock" />;
133 if (!state || !state.symbolQuotes) return null;
134
135 return enumeratedSymbols.map((symbolName, i) => {
136 const symbolQuote = state.symbolQuotes[i];
137 const symbol = symbolQuote.symbol;
138 const currencySymbol = getCurrencySymbol(symbolQuote.currency);
139 const marketPrice = Number(symbolQuote.regularMarketPrice).toFixed(2);
140 const marketChange = Number(symbolQuote.regularMarketChange).toFixed(2);
141 const marketPercentChange = Number(
142 symbolQuote.regularMarketChangePercent,
143 ).toFixed(2);
144 const stockUp = !marketChange.startsWith("-");
145
146 return (
147 <DataWidget.Widget
148 key={symbolName}
149 classes="stock"
150 ref={ref}
151 Icon={showIcon ? (stockUp ? Icons.UpArrow : Icons.DownArrow) : null}
152 href={`https://finance.yahoo.com/quote/${symbolName}`}
153 onClick={openStock}
154 onRightClick={refreshStocks}
155 >
156 <span>
157 {showSymbol && symbol}
158 {(showCurrency || showMarketPrice) && showSymbol && (
159 <span>&nbsp;</span>
160 )}
161 {showCurrency && currencySymbol}
162 {showMarketPrice && marketPrice}
163 </span>
164 <span className={showColor ? (stockUp ? "stockUp" : "stockDown") : ""}>
165 {(showMarketChange || showMarketPercent) && <span>&nbsp;</span>}
166 {showMarketChange && formatPriceChange(marketChange)}
167 {showMarketPercent &&
168 showMarketChange &&
169 ` (${formatPriceChange(marketPercentChange)}%)`}
170 {showMarketPercent &&
171 !showMarketChange &&
172 `${formatPriceChange(marketPercentChange)}%`}
173 </span>
174 </DataWidget.Widget>
175 );
176 });
177});
178
179Widget.displayName = "Stock";
180
181/**
182 * Handle click event to open stock details
183 * @param {Event} e - The event object
184 */
185function openStock(e) {
186 Utils.clickEffect(e);
187}
188
189/**
190 * Get the currency symbol for a given currency code
191 * @param {string} currency - The currency code
192 * @returns {string} The currency symbol
193 */
194function getCurrencySymbol(currency) {
195 if (currency === "EUR") return "€";
196 if (currency === "USD") return "$";
197 if (currency === "GBP") return "£";
198 if (currency === "CNY") return "¥";
199 if (currency === "JPY") return "¥";
200 return `${currency} `;
201}
202
203/**
204 * Format the price change with a '+' for positive changes
205 * @param {string} priceChange - The price change
206 * @returns {string} The formatted price change
207 */
208function formatPriceChange(priceChange) {
209 // Add a '+' for positive changes
210 return (priceChange.startsWith("-") ? "" : "+") + priceChange;
211}