Skip to content

Commit

Permalink
Feature: add stock market data widget
Browse files Browse the repository at this point in the history
ran pre-commit hook
  • Loading branch information
eldyl committed Jun 9, 2024
1 parent 5afcf44 commit 982f050
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 0 deletions.
Binary file added docs/assets/widget_stocks_demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions docs/widgets/info/stocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Stocks
description: Stocks Information Widget Configuration
---

The Stocks Widget allows you to include basic stock market data in your homepage.
The widget includes the current price of a stock, as well as the change in price
for the day.

Finnhub.io is currently the only supported provider for the stocks widget.
You can sign up for a free api key at [finnhub.io](https://finnhub.io).
You are encouraged to read finnhub.io's
[terms of service/privacy policy](https://finnhub.io/terms-of-service) before
signing up. The documentation for the endpoint that is utilized can be viewed
[here](https://finnhub.io/docs/api/quote).

You must set `finnhub` as a provider in your `settings.yaml` like below:

```yaml
providers:
finnhub: yourfinnhubapikeyhere
```

Next, configure the stocks widget in your `widgets.yaml`:

```yaml
- stocks:
provider: finnhub
cache: 1 # Optional/Stocks widget default is to cache results for 1 minute
watchlist:
- GME
- AMC
- NVDA
- AMD
```

The above configuration would result in something like this:

![Example of Stocks Widget](../../assets/widget_stocks_demo.png)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ nav:
- widgets/info/openweathermap.md
- widgets/info/resources.md
- widgets/info/search.md
- widgets/info/stocks.md
- widgets/info/unifi_controller.md
- widgets/info/weather.md
- more/troubleshooting.md
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -888,5 +888,9 @@
"auth": "With Auth",
"outdated": "Outdated",
"banned": "Banned"
},
"stocks": {
"stocks": "Stocks",
"loading": "Loading"
}
}
65 changes: 65 additions & 0 deletions src/components/widgets/stocks/stocks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import useSWR from "swr";
import { useState } from "react";
import { useTranslation } from "next-i18next";
import { FaChartLine } from "react-icons/fa6";

import Error from "../widget/error";
import Container from "../widget/container";
import PrimaryText from "../widget/primary_text";
import SecondaryText from "../widget/secondary_text";
import WidgetIcon from "../widget/widget_icon";
import Raw from "../widget/raw";

function Widget({ options }) {
const { t, i18n } = useTranslation();

const [viewingPercentChange, setViewingPercentChange] = useState(false);

const { data, error } = useSWR(
`/api/widgets/stocks?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
);

if (error || data?.error) {
return <Error options={options} />;
}

if (!data) {
return (
<Container>
<WidgetIcon icon={FaChartLine} />
<PrimaryText>{t("stocks.loading")}...</PrimaryText>
</Container>
);
}

if (data) {
const stocks = data.stocks.map((stock) => (
<span key={stock.ticker} className="flex flex-col items-center justify-center w-5 divide-y dark:divide-white/20">
<span className="text-theme-800 dark:text-theme-200 text-sm font-medium">{stock.ticker}</span>
{!viewingPercentChange ? (
<SecondaryText>{stock.currentPrice}</SecondaryText>
) : (
<SecondaryText>{stock.percentChange}%</SecondaryText>
)}
</span>
));
return (
<Container>
<Raw>
<button
type="button"
onClick={() => (viewingPercentChange ? setViewingPercentChange(false) : setViewingPercentChange(true))}
className="flex items-center justify-center w-full h-full hover:outline-none focus:outline-none"
>
<FaChartLine className="information-widget-icon flex-none mr-3 w-5 h-5 text-theme-800 dark:text-theme-200" />
<div className="flex flex-wrap items-center pl-1 gap-x-7 mr-3">{stocks}</div>
</button>
</Raw>
</Container>
);
}
}

export default function Stocks({ options }) {
return <Widget options={{ ...options }} />;
}
1 change: 1 addition & 0 deletions src/components/widgets/widget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const widgetMappings = {
openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")),
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
stocks: dynamic(() => import("components/widgets/stocks/stocks")),
};

export default function Widget({ widget, style }) {
Expand Down
60 changes: 60 additions & 0 deletions src/pages/api/widgets/stocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";

export default async function handler(req, res) {
const { watchlist, provider, cache } = req.query;

if (!provider) {
return res.status(400).json({ error: "Missing provider" });
}

if (provider !== "finnhub") {
return res.status(400).json({ error: "Missing valid provider" });
}

if (!watchlist) {
return res.status(400).json({ error: "Missing Watchlist" });
}

const providersInConfig = getSettings()?.providers;

let apiKey;
Object.entries(providersInConfig).forEach(([key, val]) => {
if (key === provider) apiKey = val;
});

if (typeof apiKey === "undefined") {
return res.status(400).json({ error: "Missing or invalid API Key for provider" });
}

const watchlistArr = watchlist.split(",") || [watchlist];

if (provider === "finnhub") {
// Finnhub allows up to 30 calls/second
// https://finnhub.io/docs/api/rate-limit
if (watchlistArr.length > 30) res.status(400).json({ error: "Max items in watchlist is 30" });

const results = await Promise.all(
watchlistArr.map(async (ticker) => {
// https://finnhub.io/docs/api/quote
const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`;

// Finnhub free accounts allow up to 60 calls/minute
// https://finnhub.io/pricing
const { c, dp } = await cachedFetch(apiUrl, cache || 1);

if (c === null || dp === null) {
return { ticker, currentPrice: "error", percentChange: "error" };
}

return { ticker, currentPrice: c.toFixed(2), percentChange: dp.toFixed(2) };
}),
);

return res.send({
stocks: results,
});
}

return res.status(400).json({ error: "Invalid configuration" });
}

0 comments on commit 982f050

Please sign in to comment.