feat: revamp dashboard, trades, and add landing/legal pages
- Major UI updates across dashboard, analytics, posts, trades, settings - New landing page, robots/sitemap, contact/privacy/terms pages - Updated globals.css with extensive styling and new landing.css - Refactor signedRequest, realtime data hook, and dashboard store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@ interface ChartPanelProps {
|
||||
candles?: Candle[]
|
||||
externalSelectedId?: number | null
|
||||
onSelectPost?: (id: number | null) => void
|
||||
onSelectDayPosts?: (posts: TrumpPost[]) => void
|
||||
}
|
||||
|
||||
export default function ChartPanel({ posts = [], candles = [], externalSelectedId, onSelectPost }: ChartPanelProps) {
|
||||
export default function ChartPanel({ posts = [], candles = [], externalSelectedId, onSelectPost, onSelectDayPosts }: ChartPanelProps) {
|
||||
const { timeframe } = useDashboardStore()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const chartRef = useRef<unknown>(null)
|
||||
@@ -25,6 +26,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
timeframeRef.current = timeframe
|
||||
const onSelectRef = useRef(onSelectPost)
|
||||
onSelectRef.current = onSelectPost
|
||||
const onSelectDayPostsRef = useRef(onSelectDayPosts)
|
||||
onSelectDayPostsRef.current = onSelectDayPosts
|
||||
|
||||
// Detect current theme for chart colors
|
||||
function getChartColors() {
|
||||
@@ -41,6 +44,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || typeof window === 'undefined') return
|
||||
let destroyed = false
|
||||
let ro: ResizeObserver | null = null
|
||||
let themeObserver: MutationObserver | null = null
|
||||
|
||||
import('lightweight-charts').then(({ createChart, CrosshairMode }) => {
|
||||
if (destroyed || !containerRef.current) return
|
||||
@@ -100,38 +105,57 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
|
||||
return Math.floor(pt / bucketSecs) * bucketSecs === clickBucket
|
||||
})
|
||||
.sort((a, b) => (b.ai_confidence ?? 0) - (a.ai_confidence ?? 0))
|
||||
|
||||
if (inBucket.length === 0) {
|
||||
onSelectRef.current?.(null)
|
||||
return
|
||||
}
|
||||
|
||||
const currentIdx = inBucket.findIndex((p) => p.id === selectedPostIdRef.current)
|
||||
if (currentIdx >= 0) {
|
||||
const nextIdx = currentIdx + 1
|
||||
if (nextIdx >= inBucket.length) {
|
||||
onSelectRef.current?.(null)
|
||||
} else {
|
||||
onSelectRef.current?.(inBucket[nextIdx].id)
|
||||
}
|
||||
} else {
|
||||
onSelectRef.current?.(inBucket[0].id)
|
||||
// Multiple posts in this candle bucket → show all of them in the right rail
|
||||
if (inBucket.length > 1 && onSelectDayPostsRef.current) {
|
||||
const sorted = [...inBucket].sort(
|
||||
(a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
|
||||
)
|
||||
onSelectDayPostsRef.current(sorted)
|
||||
return
|
||||
}
|
||||
|
||||
// Single post → show detail directly
|
||||
onSelectRef.current?.(inBucket[0].id)
|
||||
})
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
ro = new ResizeObserver(() => {
|
||||
if (containerRef.current && !destroyed) {
|
||||
chart.applyOptions({ width: containerRef.current.clientWidth })
|
||||
}
|
||||
})
|
||||
ro.observe(containerRef.current)
|
||||
|
||||
return () => { ro.disconnect() }
|
||||
themeObserver = new MutationObserver(() => {
|
||||
const colors = getChartColors()
|
||||
chart.applyOptions({
|
||||
layout: {
|
||||
background: { color: colors.background },
|
||||
textColor: colors.textColor,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: colors.gridColor },
|
||||
horzLines: { color: colors.gridColor },
|
||||
},
|
||||
rightPriceScale: { borderColor: colors.borderColor },
|
||||
timeScale: { borderColor: colors.borderColor },
|
||||
})
|
||||
})
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme'],
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
destroyed = true
|
||||
ro?.disconnect()
|
||||
themeObserver?.disconnect()
|
||||
fittedRef.current = false
|
||||
if (chartRef.current) {
|
||||
// @ts-expect-error lightweight-charts type
|
||||
@@ -169,48 +193,46 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
return t >= minTime && t <= maxTime
|
||||
})
|
||||
|
||||
if (visible.length > 0) {
|
||||
const bucketByTf: Record<string, number> = {
|
||||
'5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400, '1w': 604800,
|
||||
}
|
||||
const candleSpacing = sorted.length > 1 ? sorted[1].time - sorted[0].time : 300
|
||||
const bucketSecs = bucketByTf[timeframeRef.current.toLowerCase()] ?? candleSpacing
|
||||
|
||||
const bucketMap = new Map<number, typeof visible>()
|
||||
for (const p of visible) {
|
||||
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
|
||||
const bucket = Math.floor(pt / bucketSecs) * bucketSecs
|
||||
if (!bucketMap.has(bucket)) bucketMap.set(bucket, [])
|
||||
bucketMap.get(bucket)!.push(p)
|
||||
}
|
||||
bucketMap.forEach((ps) => ps.sort((a, b) => (b.ai_confidence ?? 0) - (a.ai_confidence ?? 0)))
|
||||
|
||||
const markers = Array.from(bucketMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([bucketTime, bPosts]) => {
|
||||
const isSelected = bPosts.some((p) => p.id === externalSelectedId)
|
||||
const best = bPosts[0]
|
||||
const count = bPosts.length
|
||||
const signalColor = isSelected
|
||||
? '#f59e0b'
|
||||
: best.signal === 'short' || best.signal === 'sell'
|
||||
? '#ef5350'
|
||||
: best.signal === 'buy'
|
||||
? '#26a69a'
|
||||
: '#aaaaaa'
|
||||
return {
|
||||
time: bucketTime as number,
|
||||
position: 'aboveBar' as const,
|
||||
color: signalColor,
|
||||
shape: 'circle' as const,
|
||||
text: count > 1 ? String(count) : '',
|
||||
size: isSelected ? 2 : count > 1 ? 1.5 : 1,
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-expect-error lightweight-charts type
|
||||
series.setMarkers(markers)
|
||||
const bucketByTf: Record<string, number> = {
|
||||
'5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400, '1w': 604800,
|
||||
}
|
||||
const candleSpacing = sorted.length > 1 ? sorted[1].time - sorted[0].time : 300
|
||||
const bucketSecs = bucketByTf[timeframeRef.current.toLowerCase()] ?? candleSpacing
|
||||
|
||||
const bucketMap = new Map<number, typeof visible>()
|
||||
for (const p of visible) {
|
||||
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
|
||||
const bucket = Math.floor(pt / bucketSecs) * bucketSecs
|
||||
if (!bucketMap.has(bucket)) bucketMap.set(bucket, [])
|
||||
bucketMap.get(bucket)!.push(p)
|
||||
}
|
||||
bucketMap.forEach((ps) => ps.sort((a, b) => (b.ai_confidence ?? 0) - (a.ai_confidence ?? 0)))
|
||||
|
||||
const markers = Array.from(bucketMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([bucketTime, bPosts]) => {
|
||||
const isSelected = bPosts.some((p) => p.id === externalSelectedId)
|
||||
const best = bPosts[0]
|
||||
const count = bPosts.length
|
||||
const signalColor = isSelected
|
||||
? '#f59e0b'
|
||||
: best.signal === 'short' || best.signal === 'sell'
|
||||
? '#ef5350'
|
||||
: best.signal === 'buy'
|
||||
? '#26a69a'
|
||||
: '#aaaaaa'
|
||||
return {
|
||||
time: bucketTime as number,
|
||||
position: 'aboveBar' as const,
|
||||
color: signalColor,
|
||||
shape: 'circle' as const,
|
||||
text: count > 1 ? String(count) : '',
|
||||
size: isSelected ? 2 : count > 1 ? 1.5 : 1,
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-expect-error lightweight-charts type
|
||||
series.setMarkers(markers)
|
||||
|
||||
if (!fittedRef.current) {
|
||||
// @ts-expect-error lightweight-charts type
|
||||
|
||||
Reference in New Issue
Block a user