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());
|
await waitFor(() => expect(screen.getByText('50.0')).toBeInTheDocument());
|
||||||
expect(screen.getAllByText('100.00').length).toBeGreaterThanOrEqual(2);
|
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;
|
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 }> = [
|
const INDICATORS: Array<{ key: IndicatorKey; label: string; hint: string }> = [
|
||||||
{ key: 'rsi', label: 'RSI', hint: '14-period momentum' },
|
{ key: 'rsi', label: 'RSI', hint: '14-period momentum' },
|
||||||
{ key: 'macd', label: 'MACD', hint: '12/26 EMA trend' },
|
{ key: 'macd', label: 'MACD', hint: '12/26 EMA trend' },
|
||||||
@ -751,7 +786,15 @@ function ResearchCards({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Empty state ──────────────────────────────────────────────────────────────
|
// ─── 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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
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: 56 }}>📈</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700, color: '#374151' }}>
|
<div style={{ fontSize: 20, fontWeight: 700, color: '#374151' }}>
|
||||||
Search a company to get started
|
Search an asset to get started
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
|
<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>
|
</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 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
{['AAPL','MSFT','GOOGL','AMZN','NVDA'].map(t => (
|
{suggestions.map(t => (
|
||||||
<span
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
onClick={() => onSelect(t)}
|
onClick={() => onSelect(t)}
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
background: '#EFF6FF',
|
background: '#EFF6FF',
|
||||||
color: '#2563EB',
|
color: '#2563EB',
|
||||||
|
border: '1px solid #BFDBFE',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
</span>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -790,7 +840,7 @@ function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
|
|||||||
|
|
||||||
// ─── HomeView ─────────────────────────────────────────────────────────────────
|
// ─── HomeView ─────────────────────────────────────────────────────────────────
|
||||||
export function HomeView() {
|
export function HomeView() {
|
||||||
const { activeSymbol, setActiveSymbol } = useAppContext();
|
const { activeSymbol, setActiveSymbol, botState, profile: activeProfile } = useAppContext();
|
||||||
const [profile, setProfile] = useState<ResearchProfile | null>(null);
|
const [profile, setProfile] = useState<ResearchProfile | null>(null);
|
||||||
const [profileLoading, setProfileLoading] = useState(false);
|
const [profileLoading, setProfileLoading] = useState(false);
|
||||||
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
|
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
|
||||||
@ -819,7 +869,14 @@ export function HomeView() {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [activeSymbol]);
|
}, [activeSymbol]);
|
||||||
|
|
||||||
if (!activeSymbol) return <EmptyState onSelect={setActiveSymbol} />;
|
if (!activeSymbol) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
onSelect={setActiveSymbol}
|
||||||
|
suggestions={emptyStateSuggestions(activeProfile, botState.symbols)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user