-
-
Notifications
You must be signed in to change notification settings - Fork 987
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: add stock market data widget
ran pre-commit hook
- Loading branch information
Showing
7 changed files
with
170 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }); | ||
} |