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:
k
2026-04-25 16:04:57 +08:00
parent 83e5892ddf
commit 040e1df685
28 changed files with 4133 additions and 690 deletions
+3
View File
@@ -52,6 +52,9 @@ export interface UserSettings {
take_profit_pct: number | null
stop_loss_pct: number | null
min_confidence: number
daily_budget_usd: number | null
active_from: string | null // ISO UTC
active_until: string | null // ISO UTC
}
export interface UserData {
+23 -1
View File
@@ -55,6 +55,25 @@ export interface SignedEnvelope {
*/
const VIEW_TTL_MS = 4 * 60 * 1000
/**
* Read-only: returns a cached, not-yet-expired view envelope if one exists.
* Never signs — never triggers a wallet popup. Returns null if no fresh cache.
*/
export function getCachedViewEnvelope(action: string, wallet: string): SignedEnvelope | null {
if (typeof window === 'undefined') return null
const cacheKey = `ts:view:${action}:${wallet.toLowerCase()}`
const raw = sessionStorage.getItem(cacheKey)
if (!raw) return null
try {
const env = JSON.parse(raw) as SignedEnvelope
if (Date.now() - env.timestamp < VIEW_TTL_MS) return env
} catch (err) {
console.warn('[signedRequest] corrupt cached envelope, ignoring', err)
sessionStorage.removeItem(cacheKey)
}
return null
}
export async function getOrCreateViewEnvelope(params: {
action: string
wallet: string
@@ -68,7 +87,10 @@ export async function getOrCreateViewEnvelope(params: {
try {
const env = JSON.parse(raw) as SignedEnvelope
if (Date.now() - env.timestamp < VIEW_TTL_MS) return env
} catch {}
} catch (err) {
console.warn('[signedRequest] corrupt cached envelope, ignoring', err)
sessionStorage.removeItem(cacheKey)
}
}
const env = await signRequest({
action: params.action,
+10 -1
View File
@@ -16,9 +16,12 @@ interface Handlers {
export function usePriceSocket(handlers: Handlers) {
const wsRef = useRef<WebSocket | null>(null)
const handlersRef = useRef(handlers)
const unmountedRef = useRef(false)
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
handlersRef.current = handlers
const connect = useCallback(() => {
if (unmountedRef.current) return
const ws = new WebSocket(`${WS_URL}/ws/prices`)
wsRef.current = ws
@@ -36,7 +39,10 @@ export function usePriceSocket(handlers: Handlers) {
}
ws.onclose = () => {
setTimeout(connect, 3000)
// Don't schedule a reconnect if the component already unmounted —
// otherwise we leak a socket per navigation forever.
if (unmountedRef.current) return
reconnectTimerRef.current = setTimeout(connect, 3000)
}
ws.onerror = () => {
@@ -45,8 +51,11 @@ export function usePriceSocket(handlers: Handlers) {
}, [])
useEffect(() => {
unmountedRef.current = false
connect()
return () => {
unmountedRef.current = true
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current)
wsRef.current?.close()
}
}, [connect])