diff --git a/components/SearchCommand.tsx b/components/SearchCommand.tsx
index f7bc62f..82b4098 100644
--- a/components/SearchCommand.tsx
+++ b/components/SearchCommand.tsx
@@ -46,7 +46,7 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
useEffect(() => {
debouncedSearch();
- }, [searchTerm]);
+ }, [debouncedSearch, searchTerm]);
const handleSelectStock = () => {
setOpen(false);
@@ -87,7 +87,7 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
{isSearchMode ? 'Search results' : 'Popular stocks'}
{` `}({displayStocks?.length || 0})
- {displayStocks?.map((stock, i) => (
+ {displayStocks?.map((stock) => (
- {stock.symbol} | {stock.exchange } | {stock.type}
+ {[stock.symbol, stock.exchange, stock.type].filter(Boolean).join(' | ')}
@@ -114,4 +114,4 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
>
)
-}
\ No newline at end of file
+}
diff --git a/lib/actions/finnhub.actions.ts b/lib/actions/finnhub.actions.ts
index 2932bdc..18753b8 100644
--- a/lib/actions/finnhub.actions.ts
+++ b/lib/actions/finnhub.actions.ts
@@ -7,6 +7,25 @@ import { cache } from 'react';
const FINNHUB_BASE_URL = 'https://finnhub.io/api/v1';
const NEXT_PUBLIC_FINNHUB_API_KEY = process.env.NEXT_PUBLIC_FINNHUB_API_KEY ?? '';
+type FinnhubQuote = {
+ c?: number;
+ d?: number;
+ dp?: number;
+};
+
+type FinnhubCompanyProfile = {
+ currency?: string;
+ exchange?: string;
+ logo?: string;
+ marketCapitalization?: number;
+ name?: string;
+ ticker?: string;
+};
+
+type SearchStockCandidate = FinnhubSearchResult & {
+ __exchange?: string;
+};
+
async function fetchJSON(url: string, revalidateSeconds?: number): Promise {
const options: RequestInit & { next?: { revalidate?: number } } = revalidateSeconds
? { cache: 'force-cache', next: { revalidate: revalidateSeconds } }
@@ -22,12 +41,23 @@ async function fetchJSON(url: string, revalidateSeconds?: number): Promise
export { fetchJSON };
+function getExchangeLabel(symbol: string, exchange?: string) {
+ if (exchange?.trim()) {
+ return exchange;
+ }
+
+ const parts = symbol.split('.');
+ const suffix = parts.length > 1 ? parts[parts.length - 1] : '';
+
+ return suffix || 'US';
+}
+
export async function getQuote(symbol: string) {
try {
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
const url = `${FINNHUB_BASE_URL}/quote?symbol=${encodeURIComponent(symbol)}&token=${token}`;
// No caching for real-time price
- return await fetchJSON(url, 0);
+ return await fetchJSON(url, 0);
} catch (e) {
console.error('Error fetching quote for', symbol, e);
return null;
@@ -39,7 +69,7 @@ export async function getCompanyProfile(symbol: string) {
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(symbol)}&token=${token}`;
// Cache profile for 24 hours
- return await fetchJSON(url, 86400);
+ return await fetchJSON(url, 86400);
} catch (e) {
console.error('Error fetching profile for', symbol, e);
return null;
@@ -160,7 +190,7 @@ export const searchStocks = cache(async (query?: string): Promise(url, 3600);
- return { sym, profile } as { sym: string; profile: any };
+ const profile = await fetchJSON(url, 3600);
+ return { sym, profile } as { sym: string; profile: FinnhubCompanyProfile | null };
} catch (e) {
console.error('Error fetching profile2 for', sym, e);
- return { sym, profile: null } as { sym: string; profile: any };
+ return { sym, profile: null } as { sym: string; profile: FinnhubCompanyProfile | null };
}
})
);
@@ -185,19 +215,16 @@ export const searchStocks = cache(async (query?: string): Promise Boolean(x));
+ .filter((x): x is SearchStockCandidate => Boolean(x));
} else {
const url = `${FINNHUB_BASE_URL}/search?q=${encodeURIComponent(trimmed)}&token=${token}`;
const data = await fetchJSON(url, 1800);
@@ -208,9 +235,8 @@ export const searchStocks = cache(async (query?: string): Promise {
const upper = (r.symbol || '').toUpperCase();
const name = r.description || upper;
- const exchangeFromDisplay = (r.displaySymbol as string | undefined) || undefined;
- const exchangeFromProfile = (r as any).__exchange as string | undefined;
- const exchange = exchangeFromDisplay || exchangeFromProfile || 'US';
+ const exchangeFromProfile = r.__exchange;
+ const exchange = getExchangeLabel(upper, exchangeFromProfile);
const type = r.type || 'Stock';
const item: StockWithWatchlistStatus = {
symbol: upper,