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 ;
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 (
{showSymbol && symbol}
{(showCurrency || showMarketPrice) && showSymbol && (
)}
{showCurrency && currencySymbol}
{showMarketPrice && marketPrice}
{(showMarketChange || showMarketPercent) && }
{showMarketChange && formatPriceChange(marketChange)}
{showMarketPercent &&
showMarketChange &&
` (${formatPriceChange(marketPercentChange)}%)`}
{showMarketPercent &&
!showMarketChange &&
`${formatPriceChange(marketPercentChange)}%`}
);
});
});
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;
}