diff --git a/src/api/generated/index.ts b/src/api/generated/index.ts index d69663ea1..2ddcc1994 100644 --- a/src/api/generated/index.ts +++ b/src/api/generated/index.ts @@ -24,6 +24,7 @@ export type { PairDto } from './models/PairDto'; export type { PairSummaryDto } from './models/PairSummaryDto'; export type { PairTransactionDto } from './models/PairTransactionDto'; export type { PairWithSummaryDto } from './models/PairWithSummaryDto'; +export { PerformancePeriodDto } from './models/PerformancePeriodDto'; export type { PeriodData } from './models/PeriodData'; export type { PostDto } from './models/PostDto'; export type { PriceChangeData } from './models/PriceChangeData'; @@ -32,8 +33,10 @@ export type { PriceMovementDto } from './models/PriceMovementDto'; export type { Tip } from './models/Tip'; export type { TokenDto } from './models/TokenDto'; export type { TokenHolderDto } from './models/TokenHolderDto'; +export type { TokenPerformanceDto } from './models/TokenPerformanceDto'; export type { TokenPriceMovementDto } from './models/TokenPriceMovementDto'; export type { Topic } from './models/Topic'; +export type { TopicDto } from './models/TopicDto'; export type { TotalUniqueUsersResultDto } from './models/TotalUniqueUsersResultDto'; export type { TransactionDto } from './models/TransactionDto'; export type { TrendingTagItemDto } from './models/TrendingTagItemDto'; diff --git a/src/api/generated/models/PerformancePeriodDto.ts b/src/api/generated/models/PerformancePeriodDto.ts new file mode 100644 index 000000000..e06738286 --- /dev/null +++ b/src/api/generated/models/PerformancePeriodDto.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PriceDto } from './PriceDto'; +export type PerformancePeriodDto = { + current: PriceDto; + current_date: string; + current_change: number; + current_change_percent: number; + current_change_direction: PerformancePeriodDto.current_change_direction; + high: PriceDto; + high_date: string; + low: PriceDto; + low_date: string; + last_updated: string; +}; +export namespace PerformancePeriodDto { + export enum current_change_direction { + UP = 'up', + DOWN = 'down', + NEUTRAL = 'neutral', + } +} + diff --git a/src/api/generated/models/PostDto.ts b/src/api/generated/models/PostDto.ts index b9c870400..88e424192 100644 --- a/src/api/generated/models/PostDto.ts +++ b/src/api/generated/models/PostDto.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { TopicDto } from './TopicDto'; export type PostDto = { /** * Unique identifier for the post @@ -34,7 +35,7 @@ export type PostDto = { /** * Array of topics/hashtags associated with the post */ - topics: Array; + topics: Array; /** * Array of media URLs associated with the post */ diff --git a/src/api/generated/models/TokenDto.ts b/src/api/generated/models/TokenDto.ts index 036f6c3d3..b98cbc15d 100644 --- a/src/api/generated/models/TokenDto.ts +++ b/src/api/generated/models/TokenDto.ts @@ -3,17 +3,23 @@ /* tslint:disable */ /* eslint-disable */ import type { PriceDto } from './PriceDto'; +import type { TokenPerformanceDto } from './TokenPerformanceDto'; export type TokenDto = { id: number; network_id: string; factory_address: string; sale_address: string; + create_tx_hash: string; creator_address: string; + dao_address: string; owner_address: string; beneficiary_address: string; bonding_curve_address: string; collection: string; metaInfo: Record; + unlisted: boolean; + last_sync_tx_count: number; + tx_count: number; address: string; name: string; symbol: string; @@ -28,6 +34,9 @@ export type TokenDto = { market_cap_data: PriceDto; total_supply: string; dao_balance: string; + trending_score: number; + trending_score_update_at: string; + performance: TokenPerformanceDto | null; created_at: string; tx_type: string; volume: string; diff --git a/src/api/generated/models/TokenPerformanceDto.ts b/src/api/generated/models/TokenPerformanceDto.ts new file mode 100644 index 000000000..79f15bfa5 --- /dev/null +++ b/src/api/generated/models/TokenPerformanceDto.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PerformancePeriodDto } from './PerformancePeriodDto'; +export type TokenPerformanceDto = { + sale_address: string; + past_24h: PerformancePeriodDto | null; + past_7d: PerformancePeriodDto | null; + past_30d: PerformancePeriodDto | null; + all_time: PerformancePeriodDto | null; +}; + diff --git a/src/api/generated/models/TopicDto.ts b/src/api/generated/models/TopicDto.ts new file mode 100644 index 000000000..60cc5dcf4 --- /dev/null +++ b/src/api/generated/models/TopicDto.ts @@ -0,0 +1,40 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TokenDto } from './TokenDto'; +export type TopicDto = { + /** + * Unique identifier for the topic + */ + id: string; + /** + * Name of the topic/hashtag + */ + name: string; + /** + * Description of the topic + */ + description: string | null; + /** + * Number of posts with this topic + */ + post_count: number; + /** + * Associated token if topic name matches a token symbol + */ + token: TokenDto | null; + /** + * Timestamp when the topic was created + */ + created_at: string; + /** + * Timestamp when the topic was last updated + */ + updated_at: string; + /** + * Version number + */ + version: number; +}; + diff --git a/src/api/generated/services/DexService.ts b/src/api/generated/services/DexService.ts index bfe2da53b..c7353d3b6 100644 --- a/src/api/generated/services/DexService.ts +++ b/src/api/generated/services/DexService.ts @@ -85,6 +85,42 @@ export class DexService { }, }); } + /** + * Get comprehensive token price analysis + * Get detailed price analysis including liquidity-weighted pricing, confidence metrics, and all possible paths + * @returns any Comprehensive price analysis with liquidity weighting + * @throws ApiError + */ + public static getTokenPriceWithLiquidityAnalysis({ + address, + baseToken, + debug, + }: { + /** + * Token contract address + */ + address: string, + /** + * Base token for price calculation (default: WAE) + */ + baseToken?: string, + /** + * Include detailed path analysis + */ + debug?: boolean, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/dex/tokens/{address}/price/analysis', + path: { + 'address': address, + }, + query: { + 'base_token': baseToken, + 'debug': debug, + }, + }); + } /** * Get DEX token summary * Get comprehensive summary data for a token including aggregated volume and price changes across all pools where the token appears. diff --git a/src/api/generated/services/PostsService.ts b/src/api/generated/services/PostsService.ts index 7cd841a80..3467c0563 100644 --- a/src/api/generated/services/PostsService.ts +++ b/src/api/generated/services/PostsService.ts @@ -54,6 +54,37 @@ export class PostsService { }, }); } + /** + * Popular posts + * Returns popular posts for selected time window. Views are ignored in v1. + * @returns any + * @throws ApiError + */ + public static popular({ + window, + debug, + limit, + page, + }: { + window?: '24h' | '7d' | 'all', + /** + * Return feature breakdown when set to 1 + */ + debug?: number, + limit?: number, + page?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/posts/popular', + query: { + 'window': window, + 'debug': debug, + 'limit': limit, + 'page': page, + }, + }); + } /** * Get post by ID * Retrieve a specific post by its unique identifier diff --git a/src/api/generated/services/TokensService.ts b/src/api/generated/services/TokensService.ts index b03deffc2..4164755d4 100644 --- a/src/api/generated/services/TokensService.ts +++ b/src/api/generated/services/TokensService.ts @@ -166,4 +166,26 @@ export class TokensService { }, }); } + /** + * Get token performance (alias for /performance) + * Returns performance data using database view. This endpoint is kept for backward compatibility. + * @returns TokenPriceMovementDto + * @throws ApiError + */ + public static performanceRaw({ + address, + }: { + /** + * Token address or name + */ + address: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/tokens/{address}/performance-raw', + path: { + 'address': address, + }, + }); + } } diff --git a/src/components/MiniWalletInfo.tsx b/src/components/MiniWalletInfo.tsx index b0c21c801..dcb01b678 100644 --- a/src/components/MiniWalletInfo.tsx +++ b/src/components/MiniWalletInfo.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useWallet, useWalletConnect } from '../hooks'; import Identicon from './Identicon'; import { AeButton } from './ui/ae-button'; diff --git a/src/components/dex/core/SwapForm.tsx b/src/components/dex/core/SwapForm.tsx index 0ebf0f48e..63d11fa89 100644 --- a/src/components/dex/core/SwapForm.tsx +++ b/src/components/dex/core/SwapForm.tsx @@ -16,6 +16,7 @@ import SwapInfoDisplay from './SwapInfoDisplay'; import NoLiquidityWarning from './NoLiquidityWarning'; import TokenInput from './TokenInput'; import { Decimal } from '../../../libs/decimal'; +import { emitTrade } from '../../../libs/events'; import { useAccount, useDex } from '../../../hooks'; import { useAeSdk } from '../../../hooks/useAeSdk'; @@ -270,6 +271,11 @@ export default function SwapForm({ onPairSelected, onFromTokenSelected }: SwapFo setAmountIn(''); setAmountOut(''); setShowConfirm(false); + try { + const inAddr = tokenIn?.address || ''; + const outAddr = tokenOut?.address || ''; + emitTrade([inAddr, outAddr].filter(Boolean)); + } catch {} } } catch (error) { console.error('Swap failed:', error); diff --git a/src/features/social/components/CommentItem.tsx b/src/features/social/components/CommentItem.tsx index 6924813ac..718f361f9 100644 --- a/src/features/social/components/CommentItem.tsx +++ b/src/features/social/components/CommentItem.tsx @@ -144,7 +144,7 @@ const CommentItem = memo(({
- {linkify(comment.content, { knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })} + {linkify(comment.content, { post: comment, knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })}
{/* Media display for comments */} diff --git a/src/features/social/components/FeedItem.tsx b/src/features/social/components/FeedItem.tsx index 6820ac818..5f898ee11 100644 --- a/src/features/social/components/FeedItem.tsx +++ b/src/features/social/components/FeedItem.tsx @@ -165,7 +165,7 @@ const FeedItem = memo(({ item, commentCount, onItemClick, isFirst = false }: Fee
{parentError || !parent ? 'Parent unavailable/not visible' - : linkify(parent.content, { knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })} + : linkify(parent.content, { post: parent, knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })}
@@ -228,7 +228,7 @@ const FeedItem = memo(({ item, commentCount, onItemClick, isFirst = false }: Fee {/* Right-side block above handles on-chain link; remove duplicate area */}
- {linkify(item.content, { knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })} + {linkify(item.content, { post: item, knownChainNames: new Set(Object.values(chainNames || {}).map(n => n?.toLowerCase())) })}
{(() => { diff --git a/src/features/social/components/HashtagWithChange.tsx b/src/features/social/components/HashtagWithChange.tsx new file mode 100644 index 000000000..706ca197e --- /dev/null +++ b/src/features/social/components/HashtagWithChange.tsx @@ -0,0 +1,154 @@ +import { Link } from 'react-router-dom'; +import { PostDto, TokensService } from '../../../api/generated'; +import { useQuery } from '@tanstack/react-query'; +import { TrendminerApi } from '../../../api/backend'; + +export default function HashtagWithChange({ tag, post }: { tag: string, post?: PostDto }) { + const clean = String(tag || '').replace(/^#/, ''); + const upper = clean.toUpperCase(); + const normalized = clean.toLowerCase(); + + // Try to find topic in post topics (case-insensitive) + // Handle both TopicDto objects and string arrays (for backward compatibility) + // Also handle trailing punctuation in topic names (e.g., "people." vs "people") + const normalizeTopicName = (name: string) => name.toLowerCase().replace(/[.,!?;:]+$/, ''); + const topic = post?.topics?.find((t) => { + if (typeof t === 'string') { + return normalizeTopicName(t) === normalized; + } + return normalizeTopicName(t.name || '') === normalized; + }); + const topicPerf = typeof topic === 'object' && topic !== null && 'token' in topic + ? topic.token?.performance + : null; + const changePercentFromTopic = topicPerf?.past_30d?.current_change_percent + ?? topicPerf?.past_7d?.current_change_percent + ?? topicPerf?.past_24h?.current_change_percent; + const hasTopicData = changePercentFromTopic != null; + const topicToken = typeof topic === 'object' && topic !== null && 'token' in topic + ? topic.token + : null; + // Check if topic token has actual change percent data (not just null periods) + const topicTokenHasPerformance = !!( + topicToken?.performance?.past_30d?.current_change_percent != null || + topicToken?.performance?.past_7d?.current_change_percent != null || + topicToken?.performance?.past_24h?.current_change_percent != null + ); + + // If topic has token but no performance, fetch performance for that token + const shouldFetchTopicPerformance = !!topicToken && !topicTokenHasPerformance && !!topicToken?.sale_address; + const { data: topicPerformanceData } = useQuery({ + queryKey: ['topic-token-performance', topicToken?.sale_address], + queryFn: async () => { + if (!topicToken?.sale_address) return null; + try { + const result = await TrendminerApi.getTokenPerformance(topicToken.sale_address); + return result; + } catch (err) { + console.warn(`[HashtagWithChange] Failed to fetch performance for topic token ${upper}:`, err); + return null; + } + }, + enabled: shouldFetchTopicPerformance, + retry: false, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }); + + // Fallback: if we don't have change percent data, try to fetch token by symbol + // Fetch if: no topic data AND no topic token (if topic has token, we fetch performance instead) + const shouldFetchToken = !hasTopicData && !topicToken && !!normalized; + const { data: tokenData, isLoading: isLoadingToken } = useQuery({ + queryKey: ['token-by-symbol', upper], + queryFn: async () => { + try { + const result = await TokensService.findByAddress({ address: upper }); + if (result) { + console.log(`[HashtagWithChange] Fetched token for ${upper}:`, { + hasPerformance: !!result.performance, + saleAddress: result.sale_address + }); + } + return result; + } catch (err) { + console.warn(`[HashtagWithChange] Failed to fetch token for ${upper}:`, err); + return null; + } + }, + enabled: shouldFetchToken, + retry: false, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }); + + // If token exists but doesn't have performance data, fetch it separately + const tokenExists = !!tokenData; + // Check if token has actual change percent data (not just null periods) + const tokenHasPerformance = !!( + tokenData?.performance?.past_30d?.current_change_percent != null || + tokenData?.performance?.past_7d?.current_change_percent != null || + tokenData?.performance?.past_24h?.current_change_percent != null + ); + const shouldFetchPerformance = shouldFetchToken && tokenExists && !tokenHasPerformance && !!tokenData?.sale_address; + const { data: performanceData } = useQuery({ + queryKey: ['token-performance', tokenData?.sale_address], + queryFn: async () => { + if (!tokenData?.sale_address) return null; + try { + const result = await TrendminerApi.getTokenPerformance(tokenData.sale_address); + return result; + } catch (err) { + console.warn(`[HashtagWithChange] Failed to fetch performance for ${upper}:`, err); + return null; + } + }, + enabled: shouldFetchPerformance, + retry: false, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }); + + // Try to get change percent from various sources, checking multiple time periods + const getChangePercent = (perf: any) => { + return perf?.past_30d?.current_change_percent + ?? perf?.past_7d?.current_change_percent + ?? perf?.past_24h?.current_change_percent + ?? null; + }; + + const changePercent = changePercentFromTopic + ?? getChangePercent(topicPerformanceData) + ?? getChangePercent(tokenData?.performance) + ?? getChangePercent(performanceData); + const isUp = changePercent != null && changePercent > 0; + const isDown = changePercent != null && changePercent < 0; + + + const linkTo = `/trends/tokens/${upper}`; + + return ( + + e.stopPropagation()} + > + #{clean} + + {changePercent != null && ( + + {changePercent.toFixed(2)}% + + )} + + ); +} + + diff --git a/src/features/social/components/PostContent.tsx b/src/features/social/components/PostContent.tsx index 97f839e53..348a071e8 100644 --- a/src/features/social/components/PostContent.tsx +++ b/src/features/social/components/PostContent.tsx @@ -17,7 +17,7 @@ const PostContent = memo(({ post }: PostContentProps) => {
{post.title && (
- {linkify(post.title, { knownChainNames: known })} + {linkify(post.title, { post, knownChainNames: known })}
)} diff --git a/src/features/social/components/ReplyToFeedItem.tsx b/src/features/social/components/ReplyToFeedItem.tsx index d22cec0b5..2f2835b6c 100644 --- a/src/features/social/components/ReplyToFeedItem.tsx +++ b/src/features/social/components/ReplyToFeedItem.tsx @@ -268,7 +268,7 @@ const ReplyToFeedItem = memo(({ item, onOpenPost, commentCount = 0, hideParentCo
{parentError || !parent ? "Parent unavailable/not visible" - : linkify(parent.content, { knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })} + : linkify(parent.content, { post: parent, knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })}
Show post
@@ -276,7 +276,7 @@ const ReplyToFeedItem = memo(({ item, onOpenPost, commentCount = 0, hideParentCo {/* Body */}
- {linkify(item.content, { knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })} + {linkify(item.content, { post: item, knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })}
{/* Media */} diff --git a/src/features/social/components/TokenCreatedActivityItem.tsx b/src/features/social/components/TokenCreatedActivityItem.tsx index f4e97a460..b82aebc27 100644 --- a/src/features/social/components/TokenCreatedActivityItem.tsx +++ b/src/features/social/components/TokenCreatedActivityItem.tsx @@ -77,7 +77,7 @@ const TokenCreatedActivityItem = memo(({ item, hideMobileDivider = false, mobile created {tokenName && ( - {linkify(`#${tokenName}`)} + {linkify(`#${tokenName}`, { post: item })} )} · diff --git a/src/features/social/components/TokenCreatedFeedItem.tsx b/src/features/social/components/TokenCreatedFeedItem.tsx index 12459d5fb..b31bc4ccc 100644 --- a/src/features/social/components/TokenCreatedFeedItem.tsx +++ b/src/features/social/components/TokenCreatedFeedItem.tsx @@ -102,7 +102,7 @@ const TokenCreatedFeedItem = memo(({ item, onOpenPost }: TokenCreatedFeedItemPro Created {tokenName && ( - {linkify(`#${tokenName}`, { knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })} + {linkify(`#${tokenName}`, { post: item, knownChainNames: new Set(Object.values(chainNames || {}).map((n) => n?.toLowerCase())) })} )}
diff --git a/src/features/trending/components/TokenRanking/TokenRanking.tsx b/src/features/trending/components/TokenRanking/TokenRanking.tsx index d20096709..2f51ba188 100644 --- a/src/features/trending/components/TokenRanking/TokenRanking.tsx +++ b/src/features/trending/components/TokenRanking/TokenRanking.tsx @@ -11,7 +11,7 @@ interface TokenRankingProps { symbol?: string; total_supply?: string; rank?: number; - }; + } | null | undefined; } interface RankingToken { @@ -36,20 +36,24 @@ export default function TokenRanking({ token }: TokenRankingProps) { const [rankingData, setRankingData] = useState(null); const [loading, setLoading] = useState(false); + if (!token) { + return null; + } + // Calculate ranking limit based on token rank (similar to Vue computed) const tokenRankingLimit = useMemo(() => { - const rank = token.rank || 1; + const rank = token?.rank || 1; if (rank === 1) return LIST_SIZE + 3; if (rank === 2) return LIST_SIZE + 1; return LIST_SIZE; - }, [token.rank]); + }, [token?.rank]); // Fetch ranking data useEffect(() => { let cancelled = false; async function fetchRankings() { - if (!token.sale_address) return; + if (!token?.sale_address) return; setLoading(true); try { @@ -83,8 +87,9 @@ export default function TokenRanking({ token }: TokenRankingProps) { // Find current token rank from ranking data or use prop const tokenRank = useMemo(() => { + if (!token?.sale_address) return token?.rank || 1; return rankingTokens.find(item => item.sale_address === token.sale_address)?.rank || token.rank || 1; - }, [rankingTokens, token.sale_address, token.rank]); + }, [rankingTokens, token?.sale_address, token?.rank]); // Calculate tokens ahead to level up const tokensAhead = useMemo(() => { @@ -166,7 +171,7 @@ export default function TokenRanking({ token }: TokenRankingProps) { {tokenRank > 1 ? (
Buy {tokensAhead === '0' ? 'any amount' : tokensAhead} more{' '} - {token.name} to level up! + {token.name || token.symbol || 'token'} to level up!
) : (
diff --git a/src/features/trending/components/TokenTradeCard.tsx b/src/features/trending/components/TokenTradeCard.tsx index fd2083029..74b97c09f 100644 --- a/src/features/trending/components/TokenTradeCard.tsx +++ b/src/features/trending/components/TokenTradeCard.tsx @@ -15,7 +15,7 @@ import TradeTokenInput from "./TradeTokenInput"; import TransactionConfirmDetailRow from "./TransactionConfirmDetailRow"; interface TokenTradeCardProps { - token: TokenDto; + token: TokenDto | null | undefined; onClose?: () => void; } @@ -27,6 +27,10 @@ export default function TokenTradeCard({ const [settingsDialogVisible, setSettingsDialogVisible] = useState(false); const [detailsShown, setDetailsShown] = useState(false); + if (!token?.sale_address) { + return null; + } + const { tokenA, tokenB, @@ -53,10 +57,6 @@ export default function TokenTradeCard({ const currentStepText = isBuying ? "" : "1/2"; - if (!token?.sale_address) { - return null; - } - return (
{/* Header with Tabs */} diff --git a/src/features/trending/hooks/useLiveTokenData.ts b/src/features/trending/hooks/useLiveTokenData.ts index 4b067555f..d508a3451 100644 --- a/src/features/trending/hooks/useLiveTokenData.ts +++ b/src/features/trending/hooks/useLiveTokenData.ts @@ -4,7 +4,7 @@ import { TokenDto } from '@/api/generated'; import WebSocketClient from '@/libs/WebSocketClient'; export interface ILiveTokenData { - token: TokenDto; + token: TokenDto | null | undefined; onUpdate?: (token: TokenDto) => void; } @@ -14,21 +14,22 @@ export interface ILiveTokenData { * @returns An object containing the reactive token data. */ export function useLiveTokenData({ token, onUpdate }: ILiveTokenData) { - const [tokenData, setTokenData] = useState({ - ...token, - }); + const [tokenData, setTokenData] = useState(token || null); const subscriptionRef = useRef<(() => void) | null>(null); const queryClient = useQueryClient(); useEffect(() => { - if (!token?.sale_address) return; + if (!token || !token.sale_address) { + setTokenData(token || null); + return; + } subscriptionRef.current = WebSocketClient.subscribeForTokenUpdates( token.sale_address, (newComingTokenData: any) => { setTokenData(currentTokenData => { const newTokenData = { - ...currentTokenData, + ...(currentTokenData || token), ...newComingTokenData.data, }; @@ -60,7 +61,12 @@ export function useLiveTokenData({ token, onUpdate }: ILiveTokenData) { }; }, [token?.sale_address, token?.symbol, queryClient, onUpdate]); + // Update tokenData when token changes + useEffect(() => { + setTokenData(token || null); + }, [token]); + return { - tokenData, + tokenData: tokenData || null, }; } diff --git a/src/features/trending/hooks/useTokenTrade.ts b/src/features/trending/hooks/useTokenTrade.ts index 295568e19..ef30b8b16 100644 --- a/src/features/trending/hooks/useTokenTrade.ts +++ b/src/features/trending/hooks/useTokenTrade.ts @@ -27,7 +27,7 @@ const PROTOCOL_DAO_AFFILIATION_FEE = 0.05; const PROTOCOL_DAO_TOKEN_AE_RATIO = 1000; interface UseTokenTradeProps { - token: TokenDto; + token: TokenDto | null | undefined; } export function useTokenTrade({ token }: UseTokenTradeProps) { @@ -37,9 +37,40 @@ export function useTokenTrade({ token }: UseTokenTradeProps) { // Use the new token trade store const store = useTokenTradeStore(); - const tokenRef = useRef(token); + const tokenRef = useRef(token || null); const errorMessage = useRef(); + // Update tokenRef when token changes + useEffect(() => { + tokenRef.current = token || null; + }, [token]); + + if (!token || !token.sale_address) { + return { + tokenA: '', + tokenB: '', + tokenAFocused: true, + isBuying: true, + loadingTransaction: false, + errorMessage: undefined, + successTxData: undefined, + isInsufficientBalance: false, + averageTokenPrice: Decimal.ZERO, + priceImpactDiff: Decimal.ZERO, + priceImpactPercent: Decimal.ZERO, + protocolTokenReward: Decimal.ZERO, + userBalance: Decimal.ZERO, + spendableAeBalance: Decimal.ZERO, + estimatedNextTokenPriceImpactDifferenceFormattedPercentage: '', + slippage: 0.01, + switchTradeView: () => {}, + setTokenAmount: () => {}, + setSlippage: () => {}, + placeTokenTradeOrder: async () => {}, + resetFormState: () => {}, + }; + } + // Calculate next price using bonding curve const calculateNextPrice = useCallback((currentSupply: Decimal) => { try { @@ -60,6 +91,7 @@ export function useTokenTrade({ token }: UseTokenTradeProps) { // Calculate token cost based on bonding curve const calculateTokenCost = useCallback((amount?: number, _isBuying = false, _isUsingToken = false): number => { + if (!tokenRef.current) return 0; const tokenDecimals = tokenRef.current.decimals ?? 18; const tokenSupply = new BigNumber(tokenRef.current.total_supply ?? 0); let currentSupply = Decimal.from(toAe(tokenSupply.toString())); diff --git a/src/features/trending/views/TokenSaleDetails.tsx b/src/features/trending/views/TokenSaleDetails.tsx index 92221dd9c..d1b9a2c89 100644 --- a/src/features/trending/views/TokenSaleDetails.tsx +++ b/src/features/trending/views/TokenSaleDetails.tsx @@ -115,6 +115,7 @@ export default function TokenSaleDetails() { const { tokenData } = useLiveTokenData({ token: _token }); const token = useMemo(() => { + if (!_token) return null; return { ..._token, ...(tokenData || {}), diff --git a/src/libs/events.ts b/src/libs/events.ts new file mode 100644 index 000000000..8b8a1ada5 --- /dev/null +++ b/src/libs/events.ts @@ -0,0 +1,48 @@ +// Lightweight event bus for app-wide notifications +// Currently used for trade events to refresh token performance data + +type TradeEventPayload = { addresses: string[] }; + +type Listener = (payload: T) => void; + +class EventBus { + private listeners: Map>> = new Map(); + + on(event: string, cb: Listener): () => void { + if (!this.listeners.has(event)) this.listeners.set(event, new Set()); + const set = this.listeners.get(event)!; + set.add(cb as Listener); + return () => { + set.delete(cb as Listener); + if (set.size === 0) this.listeners.delete(event); + }; + } + + emit(event: string, payload: T): void { + const set = this.listeners.get(event); + if (!set) return; + for (const cb of Array.from(set)) { + try { + cb(payload); + } catch { + // ignore listener errors + } + } + } +} + +const bus = new EventBus(); + +const TRADE_EVENT = 'trade'; + +export function onTrade(cb: Listener): () => void { + return bus.on(TRADE_EVENT, cb); +} + +export function emitTrade(addresses: string[]): void { + const unique = Array.from(new Set(addresses.filter(Boolean))); + if (unique.length === 0) return; + bus.emit(TRADE_EVENT, { addresses: unique }); +} + + diff --git a/src/styles/base.scss b/src/styles/base.scss index ca24d7d7f..b9f1f9716 100644 --- a/src/styles/base.scss +++ b/src/styles/base.scss @@ -171,12 +171,12 @@ body::before { } a { - color: $custom_links_color; + color: #00ff9d; text-decoration: none; - background: $accent_gradient; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + background: none; + -webkit-background-clip: initial; + -webkit-text-fill-color: #00ff9d; + background-clip: initial; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } diff --git a/src/utils/linkify.tsx b/src/utils/linkify.tsx index 524a8d006..c2a3eb6cc 100644 --- a/src/utils/linkify.tsx +++ b/src/utils/linkify.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import HashtagWithChange from '../features/social/components/HashtagWithChange'; import { formatAddress } from './address'; +import { PostDto } from '../api/generated'; // URL matcher (external links) const URL_REGEX = /((https?:\/\/)?[\w.-]+\.[a-z]{2,}(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?)/gi; @@ -11,7 +13,10 @@ const ACCOUNT_TAG_REGEX = /@?(ak_[A-Za-z0-9]+)/gi; // Hashtags like #TOKEN, #TREND-123, #ROCK-N-ROLL; allow letters, numbers, and dashes only const HASHTAG_REGEX = /#([A-Za-z0-9-]{1,50})/g; -export function linkify(text: string, options?: { knownChainNames?: Set }): React.ReactNode[] { +export function linkify(text: string, options?: { + knownChainNames?: Set, + post?: PostDto +}): React.ReactNode[] { if (!text) return []; const raw = String(text); @@ -28,12 +33,9 @@ export function linkify(text: string, options?: { knownChainNames?: Set {match} @@ -66,12 +68,9 @@ export function linkify(text: string, options?: { knownChainNames?: Set {display} @@ -114,10 +113,7 @@ export function linkify(text: string, options?: { knownChainNames?: Set lineHeight: 'inherit', margin: 0, padding: 0, - WebkitTextFillColor: 'currentColor', - WebkitBackgroundClip: 'initial', - backgroundClip: 'initial', - background: 'none', + color: '#00ff9d', verticalAlign: 'baseline', }} title={m} @@ -132,7 +128,7 @@ export function linkify(text: string, options?: { knownChainNames?: Set if (segLast < segment.length) urlLinkedParts.push(segment.slice(segLast)); }); - // Pass 3: Hashtags → router link to trending tokens page (/trends/tokens/) + // Pass 3: Hashtags → router link with inline 24h change if tokenized trend const finalParts: React.ReactNode[] = []; urlLinkedParts.forEach((node, idx) => { if (typeof node !== 'string') { @@ -143,22 +139,10 @@ export function linkify(text: string, options?: { knownChainNames?: Set let last = 0; segment.replace(HASHTAG_REGEX, (m: string, tag: string, off: number) => { if (off > last) finalParts.push(segment.slice(last, off)); - const target = `/trends/tokens/${tag.toUpperCase()}`; finalParts.push( - e.stopPropagation()} - > - {m} - + + + ); last = off + m.length; return m; diff --git a/src/views/TokenDetail.tsx b/src/views/TokenDetail.tsx index f9d473101..01370157c 100644 --- a/src/views/TokenDetail.tsx +++ b/src/views/TokenDetail.tsx @@ -89,7 +89,7 @@ export default function TokenDetail() { setError(null); try { - const [t, metaData] = await Promise.all([ + const [t, pairs, metaData] = await Promise.all([ getTokenWithUsd(tokenAddress), getPairsByTokenUsd(tokenAddress), getTokenMetaData(tokenAddress), @@ -440,7 +440,9 @@ export default function TokenDetail() { }} > { - Decimal.from(aex9Data?.event_supply).div(10 ** aex9Data?.decimals).prettify() + aex9Data?.event_supply && aex9Data?.decimals + ? Decimal.from(aex9Data.event_supply).div(10 ** aex9Data.decimals).prettify() + : "N/A" }
{ - Decimal - .from(aex9Data?.event_supply) - .div(10 ** aex9Data?.decimals) - .mul(Decimal.from(tokenDetails?.price?.ae || 0)) - .shorten() + aex9Data?.event_supply && aex9Data?.decimals + ? Decimal + .from(aex9Data.event_supply) + .div(10 ** aex9Data.decimals) + .mul(Decimal.from(tokenDetails?.price?.ae || 0)) + .shorten() + : "N/A" } AE