Merge pull request #73 from keshav-005/fix-55-search-ticker-metadata
Fix search ticker metadata
This commit is contained in:
commit
346c6bebd5
|
|
@ -46,7 +46,7 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedSearch();
|
debouncedSearch();
|
||||||
}, [searchTerm]);
|
}, [debouncedSearch, searchTerm]);
|
||||||
|
|
||||||
const handleSelectStock = () => {
|
const handleSelectStock = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
@ -87,7 +87,7 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
|
||||||
{isSearchMode ? 'Search results' : 'Popular stocks'}
|
{isSearchMode ? 'Search results' : 'Popular stocks'}
|
||||||
{` `}({displayStocks?.length || 0})
|
{` `}({displayStocks?.length || 0})
|
||||||
</div>
|
</div>
|
||||||
{displayStocks?.map((stock, i) => (
|
{displayStocks?.map((stock) => (
|
||||||
<li key={stock.symbol} className="search-item">
|
<li key={stock.symbol} className="search-item">
|
||||||
<Link
|
<Link
|
||||||
href={`/stocks/${stock.symbol}`}
|
href={`/stocks/${stock.symbol}`}
|
||||||
|
|
@ -100,7 +100,7 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
|
||||||
{stock.name}
|
{stock.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{stock.symbol} | {stock.exchange } | {stock.type}
|
{[stock.symbol, stock.exchange, stock.type].filter(Boolean).join(' | ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -114,4 +114,4 @@ export default function SearchCommand({ renderAs = 'button', label = 'Add stock'
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,32 @@ import { cache } from 'react';
|
||||||
const FINNHUB_BASE_URL = 'https://finnhub.io/api/v1';
|
const FINNHUB_BASE_URL = 'https://finnhub.io/api/v1';
|
||||||
const NEXT_PUBLIC_FINNHUB_API_KEY = process.env.NEXT_PUBLIC_FINNHUB_API_KEY ?? '';
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FINNHUB_EXCHANGE_SUFFIXES = new Set([
|
||||||
|
'AS', 'AT', 'AX', 'BA', 'BK', 'BO', 'BR', 'CO', 'DE', 'F', 'HE', 'HK',
|
||||||
|
'IL', 'IS', 'JK', 'JO', 'KL', 'KQ', 'KS', 'L', 'LS', 'MC', 'MI', 'MX',
|
||||||
|
'NS', 'NZ', 'OL', 'PA', 'PR', 'SA', 'SI', 'SS', 'ST', 'SW', 'SZ', 'T',
|
||||||
|
'TA', 'TO', 'TW', 'TWO', 'V', 'VI', 'WA',
|
||||||
|
]);
|
||||||
|
|
||||||
async function fetchJSON<T>(url: string, revalidateSeconds?: number): Promise<T> {
|
async function fetchJSON<T>(url: string, revalidateSeconds?: number): Promise<T> {
|
||||||
const options: RequestInit & { next?: { revalidate?: number } } = revalidateSeconds
|
const options: RequestInit & { next?: { revalidate?: number } } = revalidateSeconds
|
||||||
? { cache: 'force-cache', next: { revalidate: revalidateSeconds } }
|
? { cache: 'force-cache', next: { revalidate: revalidateSeconds } }
|
||||||
|
|
@ -22,12 +48,27 @@ async function fetchJSON<T>(url: string, revalidateSeconds?: number): Promise<T>
|
||||||
|
|
||||||
export { fetchJSON };
|
export { fetchJSON };
|
||||||
|
|
||||||
|
function getExchangeLabel(symbol: string, exchange?: string) {
|
||||||
|
if (exchange?.trim()) {
|
||||||
|
return exchange.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = symbol.split('.');
|
||||||
|
const suffix = parts.length > 1 ? parts[parts.length - 1].toUpperCase() : '';
|
||||||
|
|
||||||
|
if (!suffix) {
|
||||||
|
return 'US';
|
||||||
|
}
|
||||||
|
|
||||||
|
return FINNHUB_EXCHANGE_SUFFIXES.has(suffix) ? suffix : 'US';
|
||||||
|
}
|
||||||
|
|
||||||
export async function getQuote(symbol: string) {
|
export async function getQuote(symbol: string) {
|
||||||
try {
|
try {
|
||||||
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||||
const url = `${FINNHUB_BASE_URL}/quote?symbol=${encodeURIComponent(symbol)}&token=${token}`;
|
const url = `${FINNHUB_BASE_URL}/quote?symbol=${encodeURIComponent(symbol)}&token=${token}`;
|
||||||
// No caching for real-time price
|
// No caching for real-time price
|
||||||
return await fetchJSON<any>(url, 0);
|
return await fetchJSON<FinnhubQuote>(url, 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching quote for', symbol, e);
|
console.error('Error fetching quote for', symbol, e);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -39,7 +80,7 @@ export async function getCompanyProfile(symbol: string) {
|
||||||
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||||
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(symbol)}&token=${token}`;
|
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(symbol)}&token=${token}`;
|
||||||
// Cache profile for 24 hours
|
// Cache profile for 24 hours
|
||||||
return await fetchJSON<any>(url, 86400);
|
return await fetchJSON<FinnhubCompanyProfile>(url, 86400);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching profile for', symbol, e);
|
console.error('Error fetching profile for', symbol, e);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -160,7 +201,7 @@ export const searchStocks = cache(async (query?: string): Promise<StockWithWatch
|
||||||
|
|
||||||
const trimmed = typeof query === 'string' ? query.trim() : '';
|
const trimmed = typeof query === 'string' ? query.trim() : '';
|
||||||
|
|
||||||
let results: FinnhubSearchResult[] = [];
|
let results: SearchStockCandidate[] = [];
|
||||||
|
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
// Fetch top 10 popular symbols' profiles
|
// Fetch top 10 popular symbols' profiles
|
||||||
|
|
@ -170,11 +211,11 @@ export const searchStocks = cache(async (query?: string): Promise<StockWithWatch
|
||||||
try {
|
try {
|
||||||
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(sym)}&token=${token}`;
|
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(sym)}&token=${token}`;
|
||||||
// Revalidate every hour
|
// Revalidate every hour
|
||||||
const profile = await fetchJSON<any>(url, 3600);
|
const profile = await fetchJSON<FinnhubCompanyProfile>(url, 3600);
|
||||||
return { sym, profile } as { sym: string; profile: any };
|
return { sym, profile } as { sym: string; profile: FinnhubCompanyProfile | null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching profile2 for', sym, 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 +226,16 @@ export const searchStocks = cache(async (query?: string): Promise<StockWithWatch
|
||||||
const name: string | undefined = profile?.name || profile?.ticker || undefined;
|
const name: string | undefined = profile?.name || profile?.ticker || undefined;
|
||||||
const exchange: string | undefined = profile?.exchange || undefined;
|
const exchange: string | undefined = profile?.exchange || undefined;
|
||||||
if (!name) return undefined;
|
if (!name) return undefined;
|
||||||
const r: FinnhubSearchResult = {
|
const r: SearchStockCandidate = {
|
||||||
symbol,
|
symbol,
|
||||||
description: name,
|
description: name,
|
||||||
displaySymbol: symbol,
|
displaySymbol: symbol,
|
||||||
type: 'Common Stock',
|
type: 'Common Stock',
|
||||||
};
|
};
|
||||||
// We don't include exchange in FinnhubSearchResult type, so carry via mapping later using profile
|
r.__exchange = exchange;
|
||||||
// To keep pipeline simple, attach exchange via closure map stage
|
|
||||||
// We'll reconstruct exchange when mapping to final type
|
|
||||||
(r as any).__exchange = exchange; // internal only
|
|
||||||
return r;
|
return r;
|
||||||
})
|
})
|
||||||
.filter((x): x is FinnhubSearchResult => Boolean(x));
|
.filter((x): x is SearchStockCandidate => Boolean(x));
|
||||||
} else {
|
} else {
|
||||||
const url = `${FINNHUB_BASE_URL}/search?q=${encodeURIComponent(trimmed)}&token=${token}`;
|
const url = `${FINNHUB_BASE_URL}/search?q=${encodeURIComponent(trimmed)}&token=${token}`;
|
||||||
const data = await fetchJSON<FinnhubSearchResponse>(url, 1800);
|
const data = await fetchJSON<FinnhubSearchResponse>(url, 1800);
|
||||||
|
|
@ -208,9 +246,8 @@ export const searchStocks = cache(async (query?: string): Promise<StockWithWatch
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const upper = (r.symbol || '').toUpperCase();
|
const upper = (r.symbol || '').toUpperCase();
|
||||||
const name = r.description || upper;
|
const name = r.description || upper;
|
||||||
const exchangeFromDisplay = (r.displaySymbol as string | undefined) || undefined;
|
const exchangeFromProfile = r.__exchange;
|
||||||
const exchangeFromProfile = (r as any).__exchange as string | undefined;
|
const exchange = getExchangeLabel(upper, exchangeFromProfile);
|
||||||
const exchange = exchangeFromDisplay || exchangeFromProfile || 'US';
|
|
||||||
const type = r.type || 'Stock';
|
const type = r.type || 'Stock';
|
||||||
const item: StockWithWatchlistStatus = {
|
const item: StockWithWatchlistStatus = {
|
||||||
symbol: upper,
|
symbol: upper,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue