// shared.jsx — design tokens, primitives, mini chart, sentiment bar, sparklines // ───────────────────────── Tokens (themed) ───────────────────────── function useTheme(dark) { const t = dark ? { name: 'dark', bg: '#0a0a0a', bgSubtle: '#0f0f10', surface: '#161617', surfaceHi: '#1d1d1f', border: 'rgba(255,255,255,0.08)', borderStrong: 'rgba(255,255,255,0.14)', text: '#fafafa', textMuted: 'rgba(255,255,255,0.62)', textDim: 'rgba(255,255,255,0.42)', up: '#00D26A', upSoft: 'rgba(0,210,106,0.14)', down: '#FF453A', downSoft: 'rgba(255,69,58,0.14)', accent: '#3B82F6', accentSoft: 'rgba(59,130,246,0.16)', chip: 'rgba(255,255,255,0.06)', } : { name: 'light', bg: '#F7F7F5', bgSubtle: '#F1F1EE', surface: '#FFFFFF', surfaceHi: '#FFFFFF', border: 'rgba(0,0,0,0.06)', borderStrong: 'rgba(0,0,0,0.12)', text: '#0a0a0a', textMuted: 'rgba(0,0,0,0.62)', textDim: 'rgba(0,0,0,0.42)', up: '#00B85C', upSoft: 'rgba(0,184,92,0.10)', down: '#E5342A', downSoft: 'rgba(229,52,42,0.08)', accent: '#2563EB', accentSoft: 'rgba(37,99,235,0.10)', chip: 'rgba(0,0,0,0.04)', }; return t; } // Standard font stacks — Geist for everything const SW_FONT = "'Geist', ui-sans-serif, system-ui, -apple-system, sans-serif"; const SW_MONO = "'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace"; // ───────────────────────── Number formatting ───────────────────────── function fmtPrice(v, currency = '€') { return currency + v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtPct(v) { const sign = v > 0 ? '+' : ''; return sign + v.toFixed(2) + '%'; } function fmtSigned(v, currency = '€') { const sign = v > 0 ? '+' : v < 0 ? '−' : ''; return sign + currency + Math.abs(v).toFixed(2); } // ───────────────────────── Mini Line Chart ───────────────────────── // `markers` overlays prediction dots on the line: [{ i: , dir: 'buy'|'sell' }] // `bgColor` controls the stroke ring around markers so they pop off the line. function MiniChart({ data, w = 320, h = 80, color = '#00D26A', stroke = 2, fill = true, fillOpacity = 0.15, glow = false, markers = [], markerSize = 5, bgColor, upColor = '#00D26A', downColor = '#FF453A' }) { if (!data || data.length < 2) return null; const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const pad = 2; const innerW = w - pad * 2; const innerH = h - pad * 2; const xFor = (i) => pad + (i / (data.length - 1)) * innerW; const yFor = (v) => pad + innerH - ((v - min) / range) * innerH; const points = data.map((v, i) => [xFor(i), yFor(v)]); const pathD = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const areaD = `${pathD} L${points[points.length - 1][0]},${h - pad} L${points[0][0]},${h - pad} Z`; const gradId = 'mc-grad-' + Math.floor(Math.random() * 1e9); return ( {fill && } {/* prediction markers */} {markers.map((m, idx) => { if (m.i < 0 || m.i >= data.length) return null; const x = xFor(m.i), y = yFor(data[m.i]); const c = m.dir === 'sell' ? downColor : upColor; const r = m.size || markerSize; return ( {/* glow halo for emphasis */} {/* main dot */} ); })} ); } // Predefined predictions for AMZN demo — green-heavy (97% bullish narrative) const PREDICTIONS_AMZN = [ { i: 4, dir: 'buy' }, { i: 10, dir: 'buy' }, { i: 14, dir: 'sell' }, { i: 18, dir: 'buy' }, { i: 24, dir: 'buy' }, { i: 28, dir: 'buy' }, { i: 33, dir: 'sell' }, { i: 37, dir: 'buy' }, { i: 42, dir: 'buy' }, { i: 46, dir: 'buy' }, { i: 50, dir: 'buy' }, ]; // ───────────────────────── Sparkline (tiny, for cards) ───────────────────────── function Spark({ data, w = 60, h = 22, color = '#00D26A' }) { return ; } // ───────────────────────── Upside Potential ───────────────────────── // Compact visualization replacing absolute "TP €253.48" labels. // A short horizontal bar whose fill width scales with absolute upside (0..maxPct), // plus a mono percentage label colored by direction. Tiny enough to fit anywhere. function UpsidePotential({ t, current, target, maxAbsPct = 25, barWidth = 32, size = 'sm' }) { const pct = ((target - current) / current) * 100; const up = pct >= 0; const c = up ? t.up : t.down; const fillW = Math.min(100, (Math.abs(pct) / maxAbsPct) * 100); const sizes = { sm: { fs: 11, gap: 5, barH: 3 }, md: { fs: 12.5, gap: 6, barH: 4 }, }; const sz = sizes[size] || sizes.sm; return (
{up ? '+' : ''}{pct.toFixed(1)}%
); } function SentimentBar({ buy, sell, t, height = 8, showLabels = false, animated = true }) { const total = buy + sell; const buyPct = total ? (buy / total) * 100 : 50; return (
{showLabels && (
{buy} BUY {Math.round(buyPct)}% bullish SELL {sell}
)}
); } // ───────────────────────── Avatar (initials, gradient bg) ───────────────────────── function Avatar({ name, size = 28, color }) { const palette = ['#F97316', '#3B82F6', '#A855F7', '#EC4899', '#14B8A6', '#EAB308', '#F43F5E', '#6366F1']; const seed = (name || '?').charCodeAt(0) + (name?.charCodeAt(1) || 0); const bg = color || palette[seed % palette.length]; const initials = (name || '?').slice(0, 2).toUpperCase(); return (
{initials}
); } // ───────────────────────── Ticker logo bubble (square, brand-ish) ───────────────────────── function Ticker({ symbol, size = 36, bg, fg = '#fff' }) { const palette = { AMZN: '#FF9900', GOOGL: '#4285F4', AAPL: '#000', MSFT: '#00A4EF', NVDA: '#76B900', GPRE: '#1E40AF', GRAL: '#7C3AED', GILD: '#0F766E', PFI: '#475569', WHR: '#DC2626', SCCO: '#B45309', FCEL: '#059669', TSEM: '#1E3A8A', BIO: '#BE185D', NOV: '#9333EA', REGN: '#0EA5E9', }; const c = bg || palette[symbol] || '#525252'; return (
{symbol.slice(0, 1)}
); } // ───────────────────────── Icons (lightweight SVG, stroke-based) ───────────────────────── const Icon = { search: (c = 'currentColor', s = 18) => ( ), bell: (c = 'currentColor', s = 18) => ( ), home: (c = 'currentColor', s = 22) => ( ), trending: (c = 'currentColor', s = 22) => ( ), users: (c = 'currentColor', s = 22) => ( ), news: (c = 'currentColor', s = 22) => ( ), briefcase: (c = 'currentColor', s = 22) => ( ), more: (c = 'currentColor', s = 18) => ( ), back: (c = 'currentColor', s = 20) => ( ), share: (c = 'currentColor', s = 18) => ( ), bookmark: (c = 'currentColor', s = 18) => ( ), arrowUp: (c = 'currentColor', s = 14) => ( ), arrowDown: (c = 'currentColor', s = 14) => ( ), check: (c = 'currentColor', s = 16) => ( ), chevR: (c = 'currentColor', s = 16) => ( ), message: (c = 'currentColor', s = 16) => ( ), heart: (c = 'currentColor', s = 16, filled = false) => ( ), plus: (c = 'currentColor', s = 18) => ( ), flame: (c = 'currentColor', s = 14) => ( ), star: (c = 'currentColor', s = 14, filled = false) => ( ), award: (c = 'currentColor', s = 14) => ( ), }; // ───────────────────────── Sharewise logo (original brand, 1:1) ───────────────────────── function SwLogo({ size = 22, dark, compact = false }) { // compact = use the round icon-only mark instead of the full wordmark. // useful in tight top-app-bars where profile/avatar lives on the right. const src = compact ? 'assets/sharewise-icon.png' : ((typeof window !== 'undefined' && window.__resources && window.__resources.swLogo) || 'assets/sharewise-logo.png'); // On dark backgrounds, lift the logo slightly so the saturated brand colors // pop against the near-black. Subtle — not an inversion, just a brightness boost. const darkFilter = dark ? 'brightness(1.15) saturate(1.1)' : 'none'; return ( sharewise ); } // ───────────────────────── Top App Bar (mobile) ───────────────────────── // Slim utility bar at the very top of top-level screens: compact icon-only logo on // the left, search + bell + avatar on the right. Same hierarchy as the desktop // top-nav (logo · nav · search · bell · avatar). function TopAppBar({ t, hasNotifications = false, showSearch = true, showBell = true, showAvatar = true, userInitial = 'St', userColor }) { return (
{showSearch && (
{Icon.search(t.text, 20)}
)} {showBell && (
{Icon.bell(t.text, 20)} {hasNotifications && (
)}
)} {showAvatar && (
)}
); } // ───────────────────────── Bottom tab bar (mobile) ───────────────────────── function BottomTabs({ t, active = 'home', userInitial = 'St', userColor }) { const tabs = [ { id: 'home', label: 'Home', icon: Icon.home }, { id: 'markets', label: 'Markets', icon: Icon.trending }, { id: 'community', label: 'Community', icon: Icon.users }, { id: 'news', label: 'News', icon: Icon.news }, { id: 'profile', label: 'Profil', kind: 'avatar' }, ]; return (
{tabs.map(tab => { const on = tab.id === active; const c = on ? t.text : t.textDim; return (
{tab.kind === 'avatar' ? (
) : tab.icon(c, 22)} {tab.label}
); })}
); } // ───────────────────────── Card (themed) ───────────────────────── function Card({ t, children, style, pad = 16, radius = 16 }) { return (
{children}
); } // ───────────────────────── Chip / Pill ───────────────────────── function Chip({ t, children, color, bg, style, size = 'sm' }) { const sizes = { xs: { fs: 10, pad: '3px 7px', radius: 6, fw: 600 }, sm: { fs: 11, pad: '4px 9px', radius: 7, fw: 600 }, md: { fs: 12, pad: '5px 11px', radius: 8, fw: 600 }, }; const sz = sizes[size]; return ( {children} ); } // ───────────────────────── Sample data ───────────────────────── const SAMPLE = { // AMZN price history — synthetic, looks like a real chart amzn: [165,168,164,170,175,172,178,176,180,178,176,172,168,164,160,158,162,158,154,156,162,168,170,174,178,182,180,178,184,188,192,195,198,196,200,204,208,212,210,206,202,200,198,202,206,212,218,222,220,226,227,225,227], amznDown: [228,227,224,226,222,219,222,224,222,219,217,219,221,224,227,229,231,228,225,227], // Sentiment numbers buy: 214, sell: 6, hold: 12, }; // ───────────────────────── Action icons (filled, brand-grade) ───────────────────────── const ActIcon = { trade: (c, s = 18) => ( ), predict: (c, s = 18) => ( ), watch: (c, s = 18, filled = false) => ( ), paper: (c, s = 18) => ( ), alert: (c, s = 18) => ( ), share2: (c, s = 18) => ( ), more: (c, s = 18) => ( ), external: (c, s = 14) => ( ), reply: (c, s = 14) => ( ), }; // ───────────────────────── Bottom action bar (Stock Detail) ───────────────────────── // One primary Trade button + secondary Predict + icon shelf (Watch, More). // Floats above the tab bar; opens sheets when tapped. function StockActionBar({ t, watched = false, side = null /* 'buy'|'sell'|null */, layout = 'standard' }) { // layout: 'standard' = trade primary + predict ghost + icons // 'duo' = trade + predict equal weight const tradeBtn = (
{ActIcon.trade('#fff', 16)} Trade via Flatex ›
); const predictBtn = (
{ActIcon.predict(t.text, 15)} Predict
); const iconBtn = (icon, filled) => (
{icon}
); return (
{tradeBtn} {predictBtn} {layout !== 'duo' && iconBtn(ActIcon.watch(watched ? t.up : t.text, 18, watched), watched)} {layout !== 'duo' && iconBtn(ActIcon.more(t.text, 18))}
); } // ───────────────────────── Merged Post Card (prediction + discussion) ───────────────────────── // A post is just a post. It may have a prediction attached (Buy/Sell + TP + confidence) or not. // Anyone can reply, regardless of whether they have a prediction themselves. function PostCard({ t, post }) { const p = post; const hasPred = !!p.prediction; const up = hasPred && p.prediction.dir === 'Buy'; const dn = hasPred && p.prediction.dir === 'Sell'; const predColor = up ? t.up : dn ? t.down : t.textMuted; const delta = hasPred ? ((p.prediction.tp - 227.35) / 227.35 * 100).toFixed(1) : null; return (
{p.name} {p.badge && {p.badge}} {p.replyTo && ( replied to @{p.replyTo} )}
{p.acc} accuracy · {p.when}
{/* Prediction attached? show as inline chip-row, not a card-within-card */} {hasPred && (
{up ? Icon.arrowUp(predColor, 10) : Icon.arrowDown(predColor, 10)} {p.prediction.dir.toUpperCase()} €{p.prediction.tp} {delta > 0 ? '+' : ''}{delta}% {p.prediction.conf && ( · conf {p.prediction.conf}/10 )}
)}
{p.body}
{Icon.heart(t.textDim, 14)} {p.likes} 5 ? t.text : t.textDim, fontWeight: 600 }}> {ActIcon.reply(p.replies > 5 ? t.text : t.textDim, 14)} {p.replies} {p.replies > 0 ? `View ${p.replies} ${p.replies === 1 ? 'reply' : 'replies'}` : 'Reply'} {Icon.chevR(t.accent, 13)}
); } // ───────────────────────── Sample posts (predictions + discussions merged) ───────────────────────── const POSTS = [ { name: 'PapaSmurf', acc: '78%', when: '2h', badge: 'TOP 1%', prediction: { dir: 'Buy', tp: 268, conf: 9 }, body: 'AWS revenue growth re-accelerating + ad business compounding. Q2 setup looks like a blowout. Holding through earnings.', likes: 47, replies: 12, }, { name: 'Lisa_K', acc: '54%', when: '4h', replyTo: 'PapaSmurf', // Discussion post — no prediction. Reply to the prediction above. body: 'How are you thinking about the FTC overhang? Feels like a real risk before EOY ruling.', likes: 8, replies: 3, }, { name: 'fenjal', acc: '71%', when: '6h', prediction: { dir: 'Buy', tp: 255, conf: 7 }, body: 'Cost discipline is real. Margin expansion in NA retail is the underrated story here.', likes: 23, replies: 4, }, { name: 'Investorin_DE', acc: '49%', when: '12h', body: 'Frage: macht jemand hier covered calls auf AMZN? IV ist gerade attraktiv vor den Earnings.', likes: 11, replies: 7, }, { name: 'Bankster', acc: '64%', when: '1d', prediction: { dir: 'Sell', tp: 195, conf: 6 }, body: 'Multiple is stretched vs historical average. Wait for pullback.', likes: 11, replies: 18, }, ]; Object.assign(window, { useTheme, SW_FONT, SW_MONO, fmtPrice, fmtPct, fmtSigned, MiniChart, Spark, SentimentBar, UpsidePotential, Avatar, Ticker, Icon, SwLogo, ActIcon, BottomTabs, TopAppBar, Card, Chip, SAMPLE, PREDICTIONS_AMZN, StockActionBar, PostCard, POSTS, });