fix(D8): make empty-state chips market-aware
This commit is contained in:
parent
909518f82c
commit
b1f872f54c
@ -160,4 +160,27 @@ describe('TickerHeader', () => {
|
||||
await waitFor(() => expect(screen.getByText('50.0')).toBeInTheDocument());
|
||||
expect(screen.getAllByText('100.00').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('uses configured crypto symbols for empty-state suggestions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setActiveSymbol = vi.fn();
|
||||
const cryptoContext: AppContextValue = {
|
||||
...appContext,
|
||||
activeSymbol: '',
|
||||
setActiveSymbol,
|
||||
profile: { symbols: 'BTC/USDT, ETH/USDT', market_type: 'crypto' },
|
||||
botState: {
|
||||
...appContext.botState,
|
||||
symbols: {},
|
||||
} as any,
|
||||
};
|
||||
|
||||
renderTickerHeader(cryptoContext);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'BTC/USDT' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'AAPL' })).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'ETH/USDT' }));
|
||||
expect(setActiveSymbol).toHaveBeenCalledWith('ETH/USDT');
|
||||
});
|
||||
});
|
||||
|
||||
@ -41,6 +41,41 @@ interface ResearchProfile {
|
||||
exchangeShortName?: string;
|
||||
}
|
||||
|
||||
const EQUITY_EMPTY_STATE_SYMBOLS = ['AAPL','MSFT','GOOGL','AMZN','NVDA'];
|
||||
const CRYPTO_EMPTY_STATE_SYMBOLS = ['BTC/USDT','ETH/USDT','SOL/USDT','ADA/USDT','DOGE/USDT'];
|
||||
const CRYPTO_BASES = new Set(['BTC','ETH','SOL','ADA','DOGE','XRP','LTC','DOT','AVAX','MATIC','BNB']);
|
||||
|
||||
function uniqueSymbols(symbols: string[]) {
|
||||
return Array.from(new Set(symbols.map(s => s.trim().toUpperCase()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function splitSymbols(raw: unknown) {
|
||||
if (Array.isArray(raw)) return uniqueSymbols(raw.map(String));
|
||||
if (typeof raw === 'string') return uniqueSymbols(raw.split(','));
|
||||
return [];
|
||||
}
|
||||
|
||||
function isCryptoLikeSymbol(symbol: string) {
|
||||
const normalized = symbol.trim().toUpperCase();
|
||||
const base = normalized.split(/[/-]/)[0];
|
||||
return normalized.includes('/') || normalized.endsWith('USDT') || CRYPTO_BASES.has(base);
|
||||
}
|
||||
|
||||
function emptyStateSuggestions(profile: any, botSymbols: Record<string, unknown>) {
|
||||
const configuredSymbols = uniqueSymbols([
|
||||
...splitSymbols(profile?.symbols),
|
||||
...Object.keys(botSymbols ?? {}),
|
||||
]);
|
||||
if (configuredSymbols.length > 0) return configuredSymbols.slice(0, 5);
|
||||
|
||||
const marketHint = String(
|
||||
profile?.market_type ?? profile?.marketType ?? profile?.asset_class ?? profile?.assetClass ?? profile?.exchange ?? '',
|
||||
).toLowerCase();
|
||||
if (marketHint.includes('crypto')) return CRYPTO_EMPTY_STATE_SYMBOLS;
|
||||
|
||||
return EQUITY_EMPTY_STATE_SYMBOLS;
|
||||
}
|
||||
|
||||
const INDICATORS: Array<{ key: IndicatorKey; label: string; hint: string }> = [
|
||||
{ key: 'rsi', label: 'RSI', hint: '14-period momentum' },
|
||||
{ key: 'macd', label: 'MACD', hint: '12/26 EMA trend' },
|
||||
@ -751,7 +786,15 @@ function ResearchCards({
|
||||
}
|
||||
|
||||
// ─── Empty state ──────────────────────────────────────────────────────────────
|
||||
function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
|
||||
function EmptyState({
|
||||
onSelect,
|
||||
suggestions,
|
||||
}: {
|
||||
onSelect: (symbol: string) => void;
|
||||
suggestions: string[];
|
||||
}) {
|
||||
const cryptoMode = suggestions.some(isCryptoLikeSymbol);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
@ -760,28 +803,35 @@ function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
|
||||
}}>
|
||||
<div style={{ fontSize: 56 }}>📈</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: '#374151' }}>
|
||||
Search a company to get started
|
||||
Search an asset to get started
|
||||
</div>
|
||||
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
|
||||
Type a ticker symbol or company name in the search bar above to view charts, financials, and news.
|
||||
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
|
||||
</div>
|
||||
{cryptoMode && (
|
||||
<div style={{ fontSize: 12, color: '#6B7280', fontWeight: 600 }}>
|
||||
Suggested from your crypto bot configuration
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
{['AAPL','MSFT','GOOGL','AMZN','NVDA'].map(t => (
|
||||
<span
|
||||
{suggestions.map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => onSelect(t)}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
background: '#EFF6FF',
|
||||
color: '#2563EB',
|
||||
border: '1px solid #BFDBFE',
|
||||
borderRadius: 20,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -790,7 +840,7 @@ function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
|
||||
|
||||
// ─── HomeView ─────────────────────────────────────────────────────────────────
|
||||
export function HomeView() {
|
||||
const { activeSymbol, setActiveSymbol } = useAppContext();
|
||||
const { activeSymbol, setActiveSymbol, botState, profile: activeProfile } = useAppContext();
|
||||
const [profile, setProfile] = useState<ResearchProfile | null>(null);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
|
||||
@ -819,7 +869,14 @@ export function HomeView() {
|
||||
return () => { cancelled = true; };
|
||||
}, [activeSymbol]);
|
||||
|
||||
if (!activeSymbol) return <EmptyState onSelect={setActiveSymbol} />;
|
||||
if (!activeSymbol) {
|
||||
return (
|
||||
<EmptyState
|
||||
onSelect={setActiveSymbol}
|
||||
suggestions={emptyStateSuggestions(activeProfile, botState.symbols)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user