diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 000000000..497fdc46b --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,33 @@ + + + + https://superhero.com/ + daily + 0.7 + + + https://superhero.com/trends/tokens + daily + 0.7 + + + https://superhero.com/defi/swap + daily + 0.7 + + + https://superhero.com/terms + daily + 0.7 + + + https://superhero.com/privacy + daily + 0.7 + + + https://superhero.com/faq + daily + 0.7 + + diff --git a/src/App.tsx b/src/App.tsx index bc55620ab..bcaffc2f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,9 +7,8 @@ import { useAeSdk, useAccount, useWalletConnect } from "./hooks"; import { routes } from "./routes"; import "./styles/genz-components.scss"; import "./styles/mobile-optimizations.scss"; -import AppHeader from "./components/layout/app-header"; import { useSuperheroChainNames } from "./hooks/useChainName"; -import FeedbackButton from "./components/FeedbackButton"; +import CreateHashtagFab from "./components/CreateHashtagFab"; const CookiesDialog = React.lazy( () => import("./components/modals/CookiesDialog") @@ -66,13 +65,9 @@ export default function App() { }, [activeAccount]); return ( -
- + <> - -
- -
+ }> }> -
{useRoutes(routes as any)}
+ {useRoutes(routes as any)}
- -
+ + ); } diff --git a/src/components/AeButton.scss b/src/components/AeButton.scss index 62e5fe208..269ce17c4 100644 --- a/src/components/AeButton.scss +++ b/src/components/AeButton.scss @@ -225,6 +225,34 @@ } } + // Swiss Minimal variant - clean, no effects + &--swiss { + background: inherit; + color: inherit; + border: none; + border-radius: 0; + box-shadow: none; + backdrop-filter: none; + text-shadow: none; + transform: none; + + &:hover:not(:disabled) { + transform: none; + box-shadow: none; + opacity: 0.9; + } + + &:active:not(:disabled) { + transform: scale(0.98); + box-shadow: none; + } + + &:focus-visible { + outline: none; + box-shadow: none; + } + } + &__content { display: flex; align-items: center; diff --git a/src/components/ConnectWalletButton.tsx b/src/components/ConnectWalletButton.tsx index 6a91b57e3..b9b1f7014 100644 --- a/src/components/ConnectWalletButton.tsx +++ b/src/components/ConnectWalletButton.tsx @@ -2,8 +2,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useAeSdk, useWalletConnect, useModal } from '../hooks'; import Favicon from '../svg/favicon.svg?react'; -import { AeButton } from './ui/ae-button'; import { cn } from '@/lib/utils'; +import { useTheme } from '@/contexts/ThemeContext'; + +/** + * ConnectWalletButton - Swiss Minimal Design + * - Clean borders, no rounded corners + * - Black/white color scheme + * - Typography-focused + */ type Props = { label?: string; @@ -11,59 +18,68 @@ type Props = { style?: React.CSSProperties; className?: string; variant?: 'default' | 'dex'; - muted?: boolean; // greyed-out appearance while still clickable + muted?: boolean; + useThemeColors?: boolean; }; -export function ConnectWalletButton({ label, block, style, className, variant = 'default', muted = false }: Props) { +export function ConnectWalletButton({ label, block, style, className, variant = 'default', muted = false, useThemeColors = true }: Props) { const { t } = useTranslation('common'); const { activeAccount } = useAeSdk() const { connectWallet, connectingWallet } = useWalletConnect() const { openModal } = useModal(); + const { isDark } = useTheme(); const displayLabel = label || t('buttons.connectWallet'); const connectingText = t('buttons.connecting'); if (activeAccount) return null; - - const dexClasses = cn( - // Mobile (default): superhero blue with card-like radius - 'bg-[#1161FE] text-white border-none rounded-xl text-sm', - // Desktop+: elegant dark/glass pill with icon - 'sm:bg-black/80 sm:text-white sm:border sm:border-white/10 sm:backdrop-blur-[10px] sm:hover:bg-black/70 sm:!rounded-full sm:text-sm', - 'sm:shadow-[0_8px_24px_rgba(0,0,0,0.35)] hover:sm:shadow-[0_12px_32px_rgba(0,0,0,0.45)]' - ); - const baseClasses = cn( - 'rounded-xl sm:rounded-full border-border bg-card backdrop-blur-sm backdrop-saturate-120 hover:bg-card/80 hover:shadow-md text-sm', - 'sm:bg-card sm:hover:bg-card/80 sm:text-sm', - 'bg-[#1161FE] text-white border-none rounded-xl sm:rounded-full' - ); + // Swiss minimal colors + const borderColor = isDark ? '#3f3f46' : '#E4E4E7'; - const mutedClasses = cn( - 'rounded-xl sm:rounded-full text-sm', - 'bg-white/10 text-white/70 border border-white/10 hover:bg-white/10 hover:text-white/80', - 'shadow-none' - ); + // Swiss minimal style - dark theme friendly + const swissStyle: React.CSSProperties = { + background: 'transparent', + color: isDark ? '#FFFFFF' : '#000000', + border: `1px solid ${borderColor}`, + borderRadius: 0, + fontWeight: 500, + letterSpacing: '0.05em', + fontSize: '0.75rem', + ...style, + }; + + const mutedStyle: React.CSSProperties = { + background: 'transparent', + color: isDark ? '#71717A' : '#71717A', + border: `1px solid ${borderColor}`, + borderRadius: 0, + fontWeight: 500, + letterSpacing: '0.05em', + fontSize: '0.75rem', + ...style, + }; return ( - openModal({ name: 'connect-wallet' })} disabled={connectingWallet} - loading={connectingWallet} - variant="ghost" - size={variant === 'dex' ? 'default' : 'default'} - fullWidth={block} - className={cn(muted ? mutedClasses : (variant === 'dex' ? dexClasses : baseClasses), className)} - style={style} + className={cn( + 'px-4 py-2 inline-flex items-center justify-center gap-2 transition-opacity hover:opacity-90 focus:outline-none active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed', + block && 'w-full', + className + )} + style={muted ? mutedStyle : swissStyle} > - + {(connectingWallet ? connectingText : displayLabel).toUpperCase()} {(connectingWallet ? connectingText : displayLabel).toUpperCase()} - + ); } diff --git a/src/components/CreateHashtagFab.tsx b/src/components/CreateHashtagFab.tsx new file mode 100644 index 000000000..8346814b7 --- /dev/null +++ b/src/components/CreateHashtagFab.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useSectionTheme } from './layout/AppLayout'; +import { useTheme } from '@/contexts/ThemeContext'; + +export default function CreateHashtagFab() { + const [isVisible, setIsVisible] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + + // Only show on home and trends pages + const shouldShowOnPage = location.pathname === '/' || location.pathname.startsWith('/trends'); + + useEffect(() => { + if (!shouldShowOnPage) { + setIsVisible(false); + return; + } + + const handleScroll = () => { + // Show FAB after scrolling 300px (when hero button is likely out of view) + const scrollThreshold = 300; + setIsVisible(window.scrollY > scrollThreshold); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + handleScroll(); // Check initial position + + return () => window.removeEventListener('scroll', handleScroll); + }, [shouldShowOnPage]); + + const handleClick = () => { + navigate('/trends/create'); + }; + + if (!shouldShowOnPage) return null; + + return ( + + ); +} + diff --git a/src/components/SocialFeedPanel.tsx b/src/components/SocialFeedPanel.tsx new file mode 100644 index 000000000..e1c342540 --- /dev/null +++ b/src/components/SocialFeedPanel.tsx @@ -0,0 +1,342 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Link } from "react-router-dom"; +import { useTheme } from "@/contexts/ThemeContext"; + +/** + * Social Feed Panel - Floating notification-style panel + * - Floating button with unread badge + * - Expands to show social activity feed + * - Auto-closes when clicking outside + */ + +// Mock social feed data - replace with real API calls +const mockSocialFeed = [ + { + id: 1, + type: "tip", + user: "alice.chain", + action: "tipped", + target: "bob.chain", + amount: "10 AE", + hashtag: "#Bitcoin", + time: "2m ago", + avatar: "🦊", + }, + { + id: 2, + type: "comment", + user: "crypto_whale", + action: "commented on", + target: "#Ethereum", + content: "This is bullish! πŸš€", + time: "5m ago", + avatar: "πŸ‹", + }, + { + id: 3, + type: "follow", + user: "defi_master", + action: "started following", + target: "you", + time: "12m ago", + avatar: "πŸ§™", + }, + { + id: 4, + type: "post", + user: "nft_artist", + action: "posted about", + target: "#NFTs", + content: "New collection dropping soon...", + time: "18m ago", + avatar: "🎨", + }, + { + id: 5, + type: "tip", + user: "generous_giver", + action: "tipped", + target: "content_creator", + amount: "50 AE", + hashtag: "#Aeternity", + time: "25m ago", + avatar: "πŸ’Ž", + }, + { + id: 6, + type: "like", + user: "moon_boy", + action: "liked your post about", + target: "#DeFi", + time: "32m ago", + avatar: "πŸŒ™", + }, +]; + +export default function SocialFeedPanel() { + const { isDark } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + const [unreadCount, setUnreadCount] = useState(3); + const panelRef = useRef(null); + const buttonRef = useRef(null); + + // Close panel when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + panelRef.current && + buttonRef.current && + !panelRef.current.contains(event.target as Node) && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + // Clear unread when opening + useEffect(() => { + if (isOpen) { + const timer = setTimeout(() => setUnreadCount(0), 1000); + return () => clearTimeout(timer); + } + }, [isOpen]); + + // Simulate new notifications + useEffect(() => { + const interval = setInterval(() => { + if (!isOpen) { + setUnreadCount(prev => Math.min(prev + 1, 9)); + } + }, 30000); // Every 30 seconds + return () => clearInterval(interval); + }, [isOpen]); + + const getActionIcon = (type: string) => { + switch (type) { + case "tip": return "🎁"; + case "comment": return "πŸ’¬"; + case "follow": return "πŸ‘‹"; + case "post": return "πŸ“"; + case "like": return "❀️"; + default: return "πŸ“£"; + } + }; + + const getActionColor = (type: string) => { + switch (type) { + case "tip": return "#10B981"; + case "comment": return "#06B6D4"; + case "follow": return "#8B5CF6"; + case "post": return "#F59E0B"; + case "like": return "#EF4444"; + default: return "#6B7280"; + } + }; + + return ( + <> + {/* Floating Button */} + + + {/* Feed Panel */} +
+ {/* Header */} +
+
+ πŸ’¬ +

+ Social Feed +

+ {unreadCount > 0 && ( + + {unreadCount} new + + )} +
+ setIsOpen(false)} + className="text-xs font-medium text-violet-400 hover:text-violet-300 no-underline" + > + View all β†’ + +
+ + {/* Feed Items */} +
+ {mockSocialFeed.map((item, index) => ( +
+
+ {/* Avatar */} +
+ {item.avatar} +
+ + {/* Content */} +
+
+ + @{item.user} + + + {item.action} + + + {item.target} + +
+ + {/* Extra content */} + {item.content && ( +

+ "{item.content}" +

+ )} + {item.amount && ( + + πŸ’° {item.amount} + + )} + + {/* Time and type */} +
+ + {getActionIcon(item.type)} + + + {item.time} + +
+
+
+
+ ))} +
+ + {/* Footer */} +
+ setIsOpen(false)} + className={` + w-full flex items-center justify-center gap-2 py-2 rounded-xl + font-medium text-sm transition-all duration-200 + no-underline + `} + style={{ + background: 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)', + color: 'white', + }} + > + + + + Open Full Feed + +
+
+ + {/* Backdrop for mobile */} + {isOpen && ( +
setIsOpen(false)} + /> + )} + + ); +} + diff --git a/src/components/Trendminer/GlobalStatsAnalytics.tsx b/src/components/Trendminer/GlobalStatsAnalytics.tsx index 1ba37a3ad..98515624c 100644 --- a/src/components/Trendminer/GlobalStatsAnalytics.tsx +++ b/src/components/Trendminer/GlobalStatsAnalytics.tsx @@ -81,11 +81,11 @@ export default function GlobalStatsAnalytics() {
{statsItems.map((item) => (
-
{item.name}
-
+
{item.name}
+
{item.value} {item.fiat && ( -
+
{item.fiat}
)} diff --git a/src/components/dex/core/SwapForm.tsx b/src/components/dex/core/SwapForm.tsx index 32d1ca5ac..571daf19b 100644 --- a/src/components/dex/core/SwapForm.tsx +++ b/src/components/dex/core/SwapForm.tsx @@ -366,7 +366,7 @@ export default function SwapForm({ onPairSelected, onFromTokenSelected }: SwapFo }, [swapLoading, amountIn, amountOut, tokenIn, tokenOut, hasInsufficientBalance, routeInfo.path.length, hasNoLiquidity, routeInfo.liquidityStatus]); return ( -
+
{/* Header */}

@@ -376,13 +376,13 @@ export default function SwapForm({ onPairSelected, onFromTokenSelected }: SwapFo

-

+

{t('swap.description')}

@@ -411,7 +411,7 @@ export default function SwapForm({ onPairSelected, onFromTokenSelected }: SwapFo @@ -484,10 +484,10 @@ export default function SwapForm({ onPairSelected, onFromTokenSelected }: SwapFo + {isExpanded && ( + + )} +
+ + + {/* Suggestions (only when not expanded and no messages) */} + {!isExpanded && messages.length === 0 && ( +
+ {suggestions.map((suggestion, idx) => ( + + ))} +
+ )} +
+ ); +} + diff --git a/src/components/landing/AnimatedCounter.tsx b/src/components/landing/AnimatedCounter.tsx new file mode 100644 index 000000000..8eaad3781 --- /dev/null +++ b/src/components/landing/AnimatedCounter.tsx @@ -0,0 +1,425 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useTheme } from '@/contexts/ThemeContext'; +import { useSectionTheme } from '../layout/AppLayout'; + +// Note: useTheme and useSectionTheme are used in other components below + +interface AnimatedCounterProps { + value: number | string; + prefix?: string; + suffix?: string; + duration?: number; + decimals?: number; + className?: string; +} + +export function AnimatedCounter({ + value, + prefix = '', + suffix = '', + duration = 2000, + decimals = 0, + className = '' +}: AnimatedCounterProps) { + const [displayValue, setDisplayValue] = useState(0); + const [isVisible, setIsVisible] = useState(false); + const [lastAnimatedValue, setLastAnimatedValue] = useState(0); + const elementRef = useRef(null); + const animationRef = useRef(null); + + const numericValue = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.]/g, '')) || 0 : value; + + // Track visibility + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.1 } + ); + + if (elementRef.current) { + observer.observe(elementRef.current); + } + + // Also check if already visible on mount + if (elementRef.current) { + const rect = elementRef.current.getBoundingClientRect(); + if (rect.top < window.innerHeight && rect.bottom > 0) { + setIsVisible(true); + } + } + + return () => observer.disconnect(); + }, []); + + // Animate when visible and value changes (or first time with data) + useEffect(() => { + if (isVisible && numericValue > 0 && numericValue !== lastAnimatedValue) { + // Cancel any ongoing animation + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + + const startValue = lastAnimatedValue === 0 ? 0 : displayValue; + setLastAnimatedValue(numericValue); + animateValue(startValue, numericValue, duration); + } + }, [isVisible, numericValue, duration, lastAnimatedValue]); + + const animateValue = (start: number, end: number, dur: number) => { + const startTime = performance.now(); + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / dur, 1); + + // Easing function for smooth deceleration + const easeOutQuart = 1 - Math.pow(1 - progress, 4); + + const current = start + (end - start) * easeOutQuart; + setDisplayValue(current); + + if (progress < 1) { + animationRef.current = requestAnimationFrame(animate); + } + }; + + animationRef.current = requestAnimationFrame(animate); + }; + + const formatValue = (val: number): string => { + if (val >= 1000000) { + return (val / 1000000).toFixed(2) + 'M'; + } + if (val >= 1000) { + return (val / 1000).toFixed(2) + 'K'; + } + return val.toFixed(decimals); + }; + + return ( + + {prefix}{formatValue(displayValue)}{suffix} + + ); +} + +// Odometer-style digit roller +interface OdometerProps { + value: number | string; + prefix?: string; + suffix?: string; + className?: string; +} + +export function Odometer({ value, prefix = '', suffix = '', className = '' }: OdometerProps) { + const { isDark } = useTheme(); + const { colors } = useSectionTheme(); + + // Convert value to string for digit display + const stringValue = typeof value === 'string' ? value : formatNumber(value); + const digits = stringValue.split(''); + + return ( + + {prefix && {prefix}} + + {digits.map((digit, index) => ( + + ))} + + {suffix && {suffix}} + + ); +} + +function formatNumber(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(2) + 'M'; + } + if (num >= 1000) { + return (num / 1000).toFixed(2) + 'K'; + } + return num.toFixed(0); +} + +interface OdometerDigitProps { + digit: string; + delay: number; + isDark: boolean; + accentColor: string; +} + +function OdometerDigit({ digit, delay, isDark, accentColor }: OdometerDigitProps) { + const [currentDigit, setCurrentDigit] = useState('0'); + const [isAnimating, setIsAnimating] = useState(false); + const isNumber = /\d/.test(digit); + + useEffect(() => { + if (!isNumber) { + setCurrentDigit(digit); + return; + } + + const timer = setTimeout(() => { + setIsAnimating(true); + // Animate through digits + const targetNum = parseInt(digit); + let current = 0; + const interval = setInterval(() => { + setCurrentDigit(current.toString()); + current++; + if (current > targetNum) { + clearInterval(interval); + setIsAnimating(false); + } + }, 80); + + return () => clearInterval(interval); + }, delay); + + return () => clearTimeout(timer); + }, [digit, delay, isNumber]); + + if (!isNumber) { + return ( + {digit} + ); + } + + return ( + + + {currentDigit} + + + ); +} + +// Pulsing stat with icon +interface PulsingStatProps { + value: number | string; + label: string; + icon: React.ReactNode; + trend?: 'up' | 'down' | 'neutral'; + trendValue?: string; +} + +export function PulsingStat({ value, label, icon, trend, trendValue }: PulsingStatProps) { + const { isDark } = useTheme(); + const { colors } = useSectionTheme(); + + return ( +
+ {/* Pulsing background */} +
+ + {/* Icon */} +
+
{icon}
+
+ + {/* Value */} + + + {/* Trend indicator */} + {trend && trendValue && ( +
+ {trend === 'up' && ( + + + + )} + {trend === 'down' && ( + + + + )} + {trendValue} +
+ )} + + {/* Label */} + + {label} + +
+ ); +} + +// Mini sparkline chart +interface SparklineProps { + data: number[]; + width?: number; + height?: number; + color?: string; + showArea?: boolean; +} + +export function Sparkline({ + data, + width = 60, + height = 20, + color, + showArea = true +}: SparklineProps) { + const { colors } = useSectionTheme(); + const lineColor = color || colors.primary; + + if (!data.length) return null; + + const min = Math.min(...data); + const max = Math.max(...data); + const range = max - min || 1; + + const points = data.map((value, index) => { + const x = (index / (data.length - 1)) * width; + const y = height - ((value - min) / range) * height; + return `${x},${y}`; + }).join(' '); + + const areaPoints = `0,${height} ${points} ${width},${height}`; + + return ( + + {showArea && ( + + )} + + {/* Animated dot at the end */} + + + ); +} + +// Progress ring for percentage values +interface ProgressRingProps { + value: number; // 0-100 + size?: number; + strokeWidth?: number; + label?: string; +} + +export function ProgressRing({ + value, + size = 60, + strokeWidth = 4, + label +}: ProgressRingProps) { + const { isDark } = useTheme(); + const { colors } = useSectionTheme(); + const [animatedValue, setAnimatedValue] = useState(0); + + const radius = (size - strokeWidth) / 2; + const circumference = radius * 2 * Math.PI; + const offset = circumference - (animatedValue / 100) * circumference; + + useEffect(() => { + const timer = setTimeout(() => { + setAnimatedValue(value); + }, 500); + return () => clearTimeout(timer); + }, [value]); + + return ( +
+ + {/* Background circle */} + + {/* Progress circle */} + + + {/* Center value */} +
+ + {Math.round(animatedValue)}% + + {label && ( + + {label} + + )} +
+
+ ); +} + diff --git a/src/components/landing/EnhancedTokenList.tsx b/src/components/landing/EnhancedTokenList.tsx new file mode 100644 index 000000000..1456957cf --- /dev/null +++ b/src/components/landing/EnhancedTokenList.tsx @@ -0,0 +1,525 @@ +import React, { useState, useMemo } from "react"; +import { Link } from "react-router-dom"; +import { TokenDto } from "@/api/generated/models/TokenDto"; +import { PriceDataFormatter } from "@/features/shared/components"; +import { TokenLineChart } from "@/features/trending/components/TokenLineChart"; +import { useSectionTheme } from "../layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; +import { PerformanceTimeframeSelector } from "@/features/trending"; + +type SortOption = 'trending_score' | 'market_cap' | 'price' | 'newest' | 'holders_count'; + +interface EnhancedTokenListProps { + tokens: TokenDto[]; + loading?: boolean; + orderBy: SortOption; + onSortChange: (sort: SortOption) => void; + search: string; + onSearchChange: (search: string) => void; +} + +// Medal/Rank Badge Component +function RankBadge({ rank, colors }: { rank: number; colors: any }) { + if (rank === 1) { + return ( +
+
+ 1 +
+ {/* Crown icon */} + + + +
+ ); + } + + if (rank === 2) { + return ( +
+ 2 +
+ ); + } + + if (rank === 3) { + return ( +
+ 3 +
+ ); + } + + return ( +
+ {rank} +
+ ); +} + +// Sort Filter Chip Component +function SortChip({ + label, + value, + isActive, + onClick, + colors, + isDark +}: { + label: string; + value: SortOption; + isActive: boolean; + onClick: () => void; + colors: any; + isDark: boolean; +}) { + return ( + + ); +} + +// Get chart color based on rank - neon style colors +function getChartColorForRank(rank: number, defaultColor: string): string { + switch (rank) { + case 1: + return 'rgb(255, 215, 0)'; // Neon Gold + case 2: + return 'rgb(192, 192, 220)'; // Neon Silver + case 3: + return 'rgb(255, 140, 50)'; // Neon Bronze/Orange + default: + return 'rgb(6, 182, 212)'; // Neon Cyan + } +} + +// Token Row Component +function TokenRow({ + token, + rank, + colors, + isDark +}: { + token: TokenDto; + rank: number; + colors: any; + isDark: boolean; +}) { + const [isHovered, setIsHovered] = useState(false); + const chartColor = getChartColorForRank(rank, colors.primary); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ {/* Desktop Layout */} +
+ {/* Rank - 1 col */} +
+ +
+ + {/* Name & Symbol - 3 cols */} +
+
+ # +
+
+
+ {token.name || token.symbol} +
+
+ #{token.symbol || token.name} +
+
+
+ + {/* Price - 2 cols */} +
+
+ +
+
+ + {/* Market Cap - 2 cols */} +
+
+ +
+
+ + {/* Holders - 1 col */} +
+
+ {token.holders_count?.toLocaleString() || '0'} +
+
+ + {/* Performance Chart - 3 cols */} +
+
+ +
+ {/* Arrow indicator */} +
+ + + +
+
+
+ + {/* Mobile Layout */} +
+
+ {/* Rank */} + + + {/* Main Content */} +
+ {/* Top Row: Name & Price */} +
+
+ # + + {token.name || token.symbol} + +
+
+ +
+
+ + {/* Bottom Row: Market Cap, Holders, Chart */} +
+
+
+
MCap
+
+ +
+
+
+
Holders
+
+ {token.holders_count?.toLocaleString() || '0'} +
+
+
+ + {/* Mini Chart */} +
+ +
+
+
+
+
+ + {/* Hover glow effect */} + {isHovered && ( +
+ )} +
+ + ); +} + +export default function EnhancedTokenList({ + tokens, + loading, + orderBy, + onSortChange, + search, + onSearchChange +}: EnhancedTokenListProps) { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + + const sortOptions: { label: string; value: SortOption }[] = [ + { label: 'πŸ”₯ Trending', value: 'trending_score' }, + { label: 'πŸ’° Market Cap', value: 'market_cap' }, + { label: 'πŸ“ˆ Price', value: 'price' }, + { label: '✨ Newest', value: 'newest' }, + { label: 'πŸ‘₯ Holders', value: 'holders_count' }, + ]; + + return ( +
+ {/* Header with Search & Filters */} +
+ {/* Search Bar */} +
+ + + + onSearchChange(e.target.value)} + placeholder="Search hashtags by name..." + className={` + w-full pl-12 pr-4 py-3 rounded-2xl text-sm + transition-all duration-200 + ${isDark + ? 'bg-slate-800/50 border-slate-700 text-white placeholder-slate-500 focus:border-slate-600' + : 'bg-white border-gray-200 text-gray-900 placeholder-gray-400 focus:border-gray-300' + } + border focus:outline-none focus:ring-2 focus:ring-opacity-20 + `} + style={{ + focusRingColor: colors.primary, + }} + /> + {search && ( + + )} +
+ + {/* Filter Chips Row */} +
+
+ {sortOptions.map((option) => ( + onSortChange(option.value)} + colors={colors} + isDark={isDark} + /> + ))} +
+ + {/* Timeframe Selector */} +
+ +
+
+
+ + {/* Column Headers - Desktop Only */} +
+
Rank
+
Token Name
+
Price
+
Market Cap
+
Holders
+
Performance
+
+ + {/* Token Rows */} + {loading ? ( + // Skeleton Loading +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Mobile skeleton */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : tokens.length === 0 ? ( + // Empty State +
+ + + +

+ No hashtags found +

+

+ {search ? `No results for "${search}"` : 'Be the first to create a hashtag!'} +

+ {search && ( + + )} +
+ ) : ( + // Token List +
+ {tokens.map((token, index) => ( + + ))} +
+ )} +
+ ); +} + diff --git a/src/components/landing/FeaturedTopics.tsx b/src/components/landing/FeaturedTopics.tsx new file mode 100644 index 000000000..8e98a02c5 --- /dev/null +++ b/src/components/landing/FeaturedTopics.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import HotHashtagsPodium from "./HotHashtagsPodium"; + +/** + * FeaturedTopics - Hot Hashtags with 3D Space Podium + */ +export default function FeaturedTopics() { + return ( +
+ +
+ ); +} diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx new file mode 100644 index 000000000..611825770 --- /dev/null +++ b/src/components/landing/HeroSection.tsx @@ -0,0 +1,338 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { AnalyticsService } from "@/api/generated"; +import { Decimal } from "@/libs/decimal"; +import { useCurrencies } from "@/hooks/useCurrencies"; +import { useSectionTheme } from "../layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; +import { Link } from "react-router-dom"; +import { AnimatedCounter, Sparkline } from "./AnimatedCounter"; + +const formatDate = (date: Date): string => date.toISOString().slice(0, 10); + +/** + * HeroSection - Minimal Swiss Design + * - Ultra-clean, typography-focused + * - Grid-based, lots of whitespace + * - Minimal color palette + * - Strong typography hierarchy + * - Professional, timeless + */ + +export default function HeroSection() { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + const { getFiat, currentCurrencyInfo } = useCurrencies(); + + const { data: last24HoursData } = useQuery({ + queryFn: () => AnalyticsService.getPast24HoursAnalytics(), + queryKey: ["AnalyticsService.getPast24HoursAnalytics"], + staleTime: 1000 * 60, + }); + + const { data: tradeVolumeData } = useQuery({ + queryFn: () => { + const end = new Date(); + const start = new Date(Date.now() - 7 * 24 * 3600 * 1000); + return AnalyticsService.dailyTradeVolume({ + startDate: formatDate(start), + endDate: formatDate(end), + }); + }, + queryKey: ['AnalyticsService.getDailyTradeVolume'], + staleTime: 1000 * 60, + }); + + const totalMarketCapValue = React.useMemo( + () => Decimal.from(last24HoursData?.total_market_cap_sum ?? 0), + [last24HoursData] + ); + + const fiatValue = getFiat(totalMarketCapValue); + const fiatNumber = parseFloat(fiatValue.toString()) || 0; + + const volumeStats = React.useMemo(() => { + if (!Array.isArray(tradeVolumeData) || tradeVolumeData.length < 2) { + return { volume24h: 0, changePercent: 0, isPositive: true }; + } + const sorted = [...tradeVolumeData].sort((a, b) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); + const today = Number(sorted[0]?.volume_ae || 0); + const yesterday = Number(sorted[1]?.volume_ae || 0); + const changePercent = yesterday > 0 ? ((today - yesterday) / yesterday) * 100 : 0; + return { + volume24h: today, + changePercent: Math.abs(changePercent), + isPositive: changePercent >= 0 + }; + }, [tradeVolumeData]); + + const volume24hFiat = getFiat(Decimal.from(volumeStats.volume24h)); + const volume24hFiatNumber = parseFloat(volume24hFiat.toString()) || 0; + + const mockSparklineData = React.useMemo(() => { + const base = fiatNumber || 1000; + return Array.from({ length: 12 }, (_, i) => + base * (0.8 + Math.random() * 0.4) * (1 + i * 0.02) + ); + }, [fiatNumber]); + + // Swiss minimal colors + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#71717A' : '#71717A'; + const accent = '#FF0000'; // Swiss red + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + + return ( +
+ {/* Main Content */} +
+ + {/* Overline */} +
+ + + Trending Now + +
+ + {/* Main Headline */} +

+ + Tokenize + + + What's{' '} + Trending + +

+ + {/* Subtitle */} +

+ Every hashtag is a token. Own the trends, trade opinions, shape the conversation. +

+ + {/* Stats Row */} +
+ {/* Stat 1 */} +
+
+ Active +
+ +
+ + {/* Stat 2 */} +
+
+ Volume 24h +
+
+ + + {volumeStats.isPositive ? '+' : '-'}{volumeStats.changePercent.toFixed(1)}% + +
+
+ + {/* Stat 3 */} +
+
+ Total Value +
+ +
+ + {/* Stat 4 */} +
+
+ Traders +
+
+ + +
+
+
+ + {/* CTA */} +
+ + Create Hashtag + + + + + + + Explore All + + + + +
+
+ + {/* Minimal decoration - vertical line */} +
+ + {/* Styles */} + +
+ ); +} diff --git a/src/components/landing/HotHashtagsPodium.tsx b/src/components/landing/HotHashtagsPodium.tsx new file mode 100644 index 000000000..7d2549cd0 --- /dev/null +++ b/src/components/landing/HotHashtagsPodium.tsx @@ -0,0 +1,245 @@ +import React, { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { TokensService } from "@/api/generated"; +import { Link } from "react-router-dom"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useCurrencies } from "@/hooks/useCurrencies"; +import { Decimal } from "@/libs/decimal"; + +/** + * HotHashtagsPodium - Minimal Swiss Design + * - Ultra-clean, typography-focused + * - Grid-based with clear hierarchy + * - Minimal color (black/white + one accent) + * - Strong typography + * - Professional, timeless + */ + +export default function HotHashtagsPodium() { + const { isDark } = useTheme(); + const { getFiat } = useCurrencies(); + + const { data: tokens = [], isLoading } = useQuery({ + queryFn: () => TokensService.listTokens({ page: 1, pageSize: 6, sortBy: 'latest_trade_at' }), + queryKey: ['TokensService.listTokens', 'hot', 6], + staleTime: 1000 * 30, + }); + + // Mock data for fallback + const mockHashtags = [ + { id: '1', name: 'Superhero', symbol: 'HERO', price: '$1.24', change24h: 12.5, rank: 1, holders: 234 }, + { id: '2', name: 'Aengel', symbol: 'ANGEL', price: '$0.89', change24h: -3.2, rank: 2, holders: 156 }, + { id: '3', name: 'Make-Love', symbol: 'LOVE', price: '$0.45', change24h: 8.7, rank: 3, holders: 89 }, + { id: '4', name: 'Growae', symbol: 'GROW', price: '$0.23', change24h: -5.1, rank: 4, holders: 67 }, + { id: '5', name: 'CryptoCastle', symbol: 'CASTLE', price: '$0.12', change24h: 15.3, rank: 5, holders: 45 }, + { id: '6', name: 'Emoter', symbol: 'EMO', price: '$0.08', change24h: -1.8, rank: 6, holders: 32 }, + ]; + + const hotHashtags = useMemo(() => { + const tokenList = Array.isArray(tokens) + ? tokens + : typeof tokens === 'object' && 'items' in tokens + ? (tokens as any).items + : []; + + if (tokenList.length === 0) { + return mockHashtags; + } + + return tokenList.slice(0, 6).map((token: any, idx: number) => ({ + id: token.address || `${idx}`, + name: token.name || `Token ${idx + 1}`, + symbol: token.ticker || '???', + price: getFiat(Decimal.from(token.price_ae || 0)).toString(), + change24h: token.price_change_24h || (Math.random() * 40 - 20), + rank: idx + 1, + holders: token.holders_count || Math.floor(Math.random() * 500), + })); + }, [tokens, getFiat]); + + // Swiss minimal colors + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#71717A' : '#71717A'; + const accent = '#FF0000'; // Swiss red + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + const bgCard = isDark ? '#09090B' : '#FAFAFA'; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Section Header */} +
+
+ +

+ Hot Hashtags +

+
+ + View All β†’ + +
+ + {/* Grid */} +
+ {hotHashtags.map((hashtag, idx) => ( + +
+ {/* Rank */} +
+ + {String(hashtag.rank).padStart(2, '0')} + + {idx === 0 && ( + + )} +
+ + {/* Symbol */} +
+ #{hashtag.symbol} +
+ + {/* Name */} +
+ {hashtag.name} +
+ + {/* Price & Change */} +
+ + {hashtag.price} + + = 0 ? '#22C55E' : '#EF4444', + }} + > + {hashtag.change24h >= 0 ? '+' : ''}{hashtag.change24h.toFixed(1)}% + +
+ + {/* Holders - subtle */} +
+ {hashtag.holders} holders +
+
+ + ))} +
+ + {/* Bottom accent line */} +
+
+ + Updated live + +
+
+ + {/* Styles */} + +
+ ); +} diff --git a/src/components/landing/LiveActivityTicker.tsx b/src/components/landing/LiveActivityTicker.tsx new file mode 100644 index 000000000..030cc2199 --- /dev/null +++ b/src/components/landing/LiveActivityTicker.tsx @@ -0,0 +1,168 @@ +import React from "react"; +import { useLatestTransactions } from "@/hooks/useLatestTransactions"; +import { useSectionTheme } from "../layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; +import { Link } from "react-router-dom"; + +/** + * LiveActivityTicker - Swiss Minimal Design + * - Clean, no rounded corners + * - 1px borders + * - Minimal color palette + */ + +const formatAddress = (address: string): string => { + if (!address) return ""; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +}; + +const getActionText = (type: string): string => { + switch (type) { + case "create": + case "token_create": + return "CREATED"; + case "buy": + case "token_buy": + return "BOUGHT"; + case "sell": + case "token_sell": + return "SOLD"; + default: + return "TRADED"; + } +}; + +const getActionColor = (type: string): string => { + switch (type) { + case "create": + case "token_create": + return "#EF4444"; // Swiss Red for create + case "buy": + case "token_buy": + return "#22C55E"; // Green for buy + case "sell": + case "token_sell": + return "#F87171"; // Lighter red for sell + default: + return "#78716C"; // Warm neutral gray + } +}; + +export default function LiveActivityTicker() { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + const { latestTransactions } = useLatestTransactions(); + + // Swiss colors - Red accent with black background + const textPrimary = '#FFFFFF'; // Always white on black bg + const textSecondary = '#A8A29E'; // Always light gray on black bg + const borderColor = isDark ? '#292524' : '#27272A'; + const bgColor = '#000000'; // Black background + const accent = '#EF4444'; // Swiss Red + + if (!latestTransactions || latestTransactions.length === 0) { + return ( +
+
+
+ + Loading activity... + +
+
+ ); + } + + // Duplicate items for seamless marquee loop + const tickerItems = [...latestTransactions.slice(0, 15), ...latestTransactions.slice(0, 15)]; + + return ( +
+
+ {/* Live Badge */} +
+
+
+
+ + Live + +
+ + {/* Divider */} +
+ + {/* Ticker */} +
+
+ {tickerItems.map((tx, index) => ( + + e.currentTarget.style.color = textPrimary} + onMouseOut={(e) => e.currentTarget.style.color = textSecondary} + > + {formatAddress(tx.account)} + + + {getActionText(tx.tx_type)} + + {tx.token && ( + e.currentTarget.style.color = accent} + onMouseOut={(e) => e.currentTarget.style.color = textPrimary} + > + #{tx.token.name} + + )} + + β€” + + + ))} +
+
+
+
+ ); +} diff --git a/src/components/landing/index.ts b/src/components/landing/index.ts new file mode 100644 index 000000000..6e5391a9e --- /dev/null +++ b/src/components/landing/index.ts @@ -0,0 +1,5 @@ +export { default as HeroSection } from "./HeroSection"; +export { default as FeaturedTopics } from "./FeaturedTopics"; +export { default as AiAssistantBar } from "./AiAssistantBar"; +export { default as LiveActivityTicker } from "./LiveActivityTicker"; + diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 000000000..9cc660881 --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,256 @@ +import React, { createContext, useContext, useMemo } from "react"; +import { useLocation, Outlet } from "react-router-dom"; +import LeftSidebar from "./LeftSidebar"; +import OnboardingTour from "@/components/onboarding/OnboardingTour"; +import SocialFeedPanel from "@/components/SocialFeedPanel"; +import { useTheme } from "@/contexts/ThemeContext"; + +// Section theme context +export type SectionTheme = "topics" | "social" | "defi" | "default"; + +interface SectionColors { + primary: string; + primaryLight: string; + primaryDark: string; + gradient: string; + border: string; + bgGradient: string; + bgTint: string; // Subtle background tint + cardBg: string; + sidebarBg: string; + iconBg: string; + textPrimary: string; + textSecondary: string; + accentName: string; // Human-readable name +} + +interface SectionThemeContextValue { + theme: SectionTheme; + colors: SectionColors; +} + +// Swiss Minimal Light mode themes - Clean, typography-focused +// Each section has a distinct accent color but maintains Swiss minimalism +const lightThemes: Record = { + topics: { + primary: "#FF0000", // Swiss red + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#E4E4E7", + bgGradient: "#FAFAFA", + bgTint: "rgba(255, 0, 0, 0.015)", // Very subtle red tint + cardBg: "#FFFFFF", + sidebarBg: "#FFFFFF", + iconBg: "#000000", + textPrimary: "#000000", + textSecondary: "#71717A", + accentName: "Red", + }, + social: { + primary: "#FF0000", + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#E4E4E7", + bgGradient: "#FAFAFA", + bgTint: "rgba(255, 0, 0, 0.015)", + cardBg: "#FFFFFF", + sidebarBg: "#FFFFFF", + iconBg: "#000000", + textPrimary: "#000000", + textSecondary: "#71717A", + accentName: "Red", + }, + defi: { + primary: "#22C55E", // Green for DeFi + primaryLight: "#4ADE80", + primaryDark: "#16A34A", + gradient: "#22C55E", + border: "#D4E7DC", // Subtle green-tinted border + bgGradient: "#F8FBF9", // Very subtle green background + bgTint: "rgba(34, 197, 94, 0.03)", // Subtle green tint + cardBg: "#FFFFFF", + sidebarBg: "#FFFFFF", + iconBg: "#000000", + textPrimary: "#000000", + textSecondary: "#71717A", + accentName: "Green", + }, + default: { + primary: "#FF0000", + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#E4E4E7", + bgGradient: "#FAFAFA", + bgTint: "transparent", + cardBg: "#FFFFFF", + sidebarBg: "#FFFFFF", + iconBg: "#000000", + textPrimary: "#000000", + textSecondary: "#71717A", + accentName: "Red", + }, +}; + +// Swiss Minimal Dark mode themes +const darkThemes: Record = { + topics: { + primary: "#FF0000", // Swiss red + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#27272A", + bgGradient: "#09090B", + bgTint: "rgba(255, 0, 0, 0.02)", // Very subtle red tint in dark + cardBg: "#18181B", + sidebarBg: "#09090B", + iconBg: "#FFFFFF", + textPrimary: "#FFFFFF", + textSecondary: "#A1A1AA", + accentName: "Red", + }, + social: { + primary: "#FF0000", + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#27272A", + bgGradient: "#09090B", + bgTint: "rgba(255, 0, 0, 0.02)", + cardBg: "#18181B", + sidebarBg: "#09090B", + iconBg: "#FFFFFF", + textPrimary: "#FFFFFF", + textSecondary: "#A1A1AA", + accentName: "Red", + }, + defi: { + primary: "#22C55E", + primaryLight: "#4ADE80", + primaryDark: "#16A34A", + gradient: "#22C55E", + border: "#1E3A2F", // Dark green-tinted border + bgGradient: "#080C0A", // Very dark with green tint + bgTint: "rgba(34, 197, 94, 0.03)", // Subtle green tint + cardBg: "#111612", // Slight green tint in cards + sidebarBg: "#09090B", + iconBg: "#FFFFFF", + textPrimary: "#FFFFFF", + textSecondary: "#A1A1AA", + accentName: "Green", + }, + default: { + primary: "#FF0000", + primaryLight: "#FF3333", + primaryDark: "#CC0000", + gradient: "#FF0000", + border: "#27272A", + bgGradient: "#09090B", + bgTint: "transparent", + cardBg: "#18181B", + sidebarBg: "#09090B", + iconBg: "#FFFFFF", + textPrimary: "#FFFFFF", + textSecondary: "#A1A1AA", + accentName: "Red", + }, +}; + +const SectionThemeContext = createContext({ + theme: "default", + colors: lightThemes.default, +}); + +export const useSectionTheme = () => useContext(SectionThemeContext); + +function getSectionFromPath(pathname: string): SectionTheme { + if (pathname.startsWith("/trends") || pathname === "/") { + return "topics"; + } + if (pathname.startsWith("/social") || pathname.startsWith("/post") || pathname.startsWith("/users")) { + return "social"; + } + if (pathname.startsWith("/defi")) { + return "defi"; + } + return "default"; +} + +interface AppLayoutProps { + children?: React.ReactNode; +} + +export default function AppLayout({ children }: AppLayoutProps) { + const location = useLocation(); + const { isDark } = useTheme(); + const currentSection = getSectionFromPath(location.pathname); + + const themeValue = useMemo( + () => ({ + theme: currentSection, + colors: isDark ? darkThemes[currentSection] : lightThemes[currentSection], + }), + [currentSection, isDark] + ); + + return ( + + {/* Import Inter font */} + + +
+ {/* Left Sidebar - fixed on desktop */} + + + {/* Main content area */} +
+ {/* Main content */} +
+
+ {children ?? } +
+
+
+ + {/* Social Feed Panel - Floating notification button */} + + + {/* Onboarding Tour */} + +
+
+ ); +} diff --git a/src/components/layout/Breadcrumbs.tsx b/src/components/layout/Breadcrumbs.tsx new file mode 100644 index 000000000..9a09e1a48 --- /dev/null +++ b/src/components/layout/Breadcrumbs.tsx @@ -0,0 +1,172 @@ +import React, { useMemo } from "react"; +import { Link, useLocation, useParams } from "react-router-dom"; +import { useSectionTheme } from "./AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; + +interface BreadcrumbItem { + label: string; + path?: string; + icon?: React.ReactNode; +} + +// Icons for breadcrumbs +const HomeIcon = () => ( + + + +); + +const HashtagIcon = () => ( + + + +); + +const SocialIcon = () => ( + + + +); + +const DeFiIcon = () => ( + + + +); + +export default function Breadcrumbs() { + const location = useLocation(); + const params = useParams(); + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + + const breadcrumbs = useMemo(() => { + const pathname = location.pathname; + const items: BreadcrumbItem[] = [{ label: "Home", path: "/", icon: }]; + + if (pathname === "/" || pathname === "") { + return items; + } + + // Hashtags section + if (pathname.startsWith("/trends")) { + items.push({ label: "Hashtags", path: "/trends/tokens", icon: }); + + if (pathname === "/trends/tokens") { + // Just show Hashtags + } else if (pathname === "/trends/create") { + items.push({ label: "Create" }); + } else if (pathname === "/trends/daos") { + items.push({ label: "DAOs" }); + } else if (pathname.startsWith("/trends/dao/")) { + items.push({ label: "DAOs", path: "/trends/daos" }); + items.push({ label: "DAO Details" }); + } else if (pathname === "/trends/leaderboard") { + items.push({ label: "Leaderboard" }); + } else if (pathname === "/trends/invite") { + items.push({ label: "Invite & Earn" }); + } else if (pathname.startsWith("/trends/tokens/")) { + const tokenName = params.tokenName || pathname.split("/").pop(); + items.push({ label: `#${decodeURIComponent(tokenName || "")}` }); + } else if (pathname.startsWith("/trends/accounts")) { + items.push({ label: "Accounts" }); + } + } + + // Social section + if (pathname === "/social" || pathname.startsWith("/post") || pathname.startsWith("/users")) { + items.push({ label: "Social", path: "/social", icon: }); + + if (pathname.startsWith("/post/")) { + items.push({ label: "Post" }); + } else if (pathname.startsWith("/users/")) { + items.push({ label: "Profile" }); + } + } + + // DeFi section + if (pathname.startsWith("/defi")) { + items.push({ label: "DeFi", path: "/defi/swap", icon: }); + + if (pathname === "/defi/swap") { + items.push({ label: "Swap" }); + } else if (pathname === "/defi/pool") { + items.push({ label: "Pool" }); + } else if (pathname === "/defi/wrap") { + items.push({ label: "Wrap" }); + } else if (pathname === "/defi/bridge") { + items.push({ label: "Bridge" }); + } else if (pathname === "/defi/buy-ae-with-eth") { + items.push({ label: "Buy AE" }); + } else if (pathname.startsWith("/defi/explore")) { + items.push({ label: "Explore" }); + } + } + + // Other pages + if (pathname === "/voting") { + items.push({ label: "Governance" }); + } + if (pathname === "/terms") { + items.push({ label: "Terms" }); + } + if (pathname === "/privacy") { + items.push({ label: "Privacy" }); + } + if (pathname === "/faq") { + items.push({ label: "FAQ" }); + } + + return items; + }, [location.pathname, params]); + + if (breadcrumbs.length <= 1) { + return null; + } + + return ( + + ); +} diff --git a/src/components/layout/LeftSidebar.tsx b/src/components/layout/LeftSidebar.tsx new file mode 100644 index 000000000..20efa9aac --- /dev/null +++ b/src/components/layout/LeftSidebar.tsx @@ -0,0 +1,537 @@ +import React, { useState } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { useSectionTheme, SectionTheme } from "./AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useOnboarding } from "@/contexts/OnboardingContext"; +import ConnectWalletButton from "../ConnectWalletButton"; +import ThemeSwitcher from "./ThemeSwitcher"; +import { useAeSdk } from "@/hooks/useAeSdk"; +import { useAccountBalances } from "@/hooks/useAccountBalances"; +import { useWallet } from "@/hooks"; +import AddressAvatarWithChainName from "@/@components/Address/AddressAvatarWithChainName"; + +/** + * LeftSidebar - Swiss Minimal Design + * - Clean, typography-focused + * - No rounded corners + * - Minimal color (black/white + accent) + * - Strong hierarchy + */ + +// Haptic feedback utility +const triggerHaptic = (pattern: number | number[] = 10) => { + if (typeof navigator !== 'undefined' && 'vibrate' in navigator) { + try { + navigator.vibrate(pattern); + } catch (e) {} + } +}; + +interface NavItem { + id: string; + label: string; + emoji: string; + path: string; + theme: SectionTheme; +} + +const navItems: NavItem[] = [ + { id: "home", label: "Home", emoji: "🏠", path: "/", theme: "topics" }, + { id: "hashtags", label: "Hashtags", emoji: "πŸ”₯", path: "/trends/tokens", theme: "topics" }, + { id: "defi", label: "DeFi", emoji: "πŸ’Ž", path: "/defi/swap", theme: "defi" }, +]; + +const subNavItems: Record = { + hashtags: [ + { id: "explore", label: "Explore", path: "/trends/tokens" }, + { id: "create", label: "Create", path: "/trends/create" }, + { id: "daos", label: "DAOs", path: "/trends/daos" }, + { id: "leaderboard", label: "Leaderboard", path: "/trends/leaderboard" }, + { id: "invite", label: "Invite & Earn", path: "/trends/invite" }, + ], + defi: [ + { id: "swap", label: "Swap", path: "/defi/swap" }, + { id: "pool", label: "Pool", path: "/defi/pool" }, + { id: "wrap", label: "Wrap", path: "/defi/wrap" }, + { id: "bridge", label: "Bridge", path: "/defi/bridge" }, + { id: "buy-ae", label: "Buy AE", path: "/defi/buy-ae-with-eth" }, + ], +}; + +export default function LeftSidebar() { + const location = useLocation(); + const navigate = useNavigate(); + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + const { startOnboarding, hasSeenOnboarding, resetOnboarding } = useOnboarding(); + const [mobileOpen, setMobileOpen] = useState(false); + + // Wallet data + const { activeAccount } = useAeSdk(); + const { decimalBalance } = useAccountBalances(activeAccount); + const { chainNames } = useWallet(); + const chainName = activeAccount ? chainNames?.[activeAccount] : null; + const balanceAe = Number(decimalBalance?.toString() || 0); + + // Don't auto-expand - only expand manually or when on a specific sub-route + const getExpandedFromRoute = (pathname: string): Set => { + const expanded = new Set(); + // Only expand if user is on a specific sub-page (not home or main section page) + if (pathname.startsWith("/trends/") && pathname !== "/trends/tokens") { + expanded.add("hashtags"); + } + if (pathname.startsWith("/defi/") && pathname !== "/defi/swap") { + expanded.add("defi"); + } + return expanded; + }; + + const [expandedItems, setExpandedItems] = useState>(() => getExpandedFromRoute(location.pathname)); + + React.useEffect(() => { + // Only auto-expand when navigating to sub-pages + const newExpanded = getExpandedFromRoute(location.pathname); + if (newExpanded.size > 0) { + setExpandedItems(prev => { + const next = new Set(prev); + newExpanded.forEach(item => next.add(item)); + return next; + }); + } + }, [location.pathname]); + + React.useEffect(() => { + const handleExpandMenu = (event: CustomEvent<{ menuId: string }>) => { + const { menuId } = event.detail; + setExpandedItems((prev) => { + const next = new Set(prev); + next.add(menuId); + return next; + }); + }; + window.addEventListener("expand-sidebar-menu" as any, handleExpandMenu); + return () => { + window.removeEventListener("expand-sidebar-menu" as any, handleExpandMenu); + }; + }, []); + + const isActive = (path: string) => { + if (path === "/") return location.pathname === "/"; + if (path === "/trends/tokens") { + return location.pathname.startsWith("/trends") && + !location.pathname.includes("/create") && + !location.pathname.includes("/daos") && + !location.pathname.includes("/leaderboard") && + !location.pathname.includes("/invite"); + } + return location.pathname.startsWith(path); + }; + + const isParentActive = (item: NavItem) => { + if (item.id === "home") return location.pathname === "/"; + if (item.id === "hashtags") return location.pathname.startsWith("/trends") || location.pathname === "/"; + return location.pathname.startsWith(item.path); + }; + + const toggleExpand = (id: string) => { + setExpandedItems((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const handleNavClick = (item: NavItem) => { + triggerHaptic(15); + + if (item.id === "home") { + navigate("/"); + setMobileOpen(false); + return; + } + + if (subNavItems[item.id]) { + toggleExpand(item.id); + if (!expandedItems.has(item.id)) { + navigate(item.path); + } + } else { + navigate(item.path); + } + setMobileOpen(false); + }; + + // Swiss colors - improved visibility + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#A1A1AA' : '#52525B'; // Improved contrast + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + const bgColor = isDark ? '#09090B' : '#FFFFFF'; + const hoverBg = isDark ? '#18181B' : '#F4F4F5'; + + // Section-specific accent colors - Swiss Red + const getAccentColor = (theme: SectionTheme): string => { + switch (theme) { + case 'defi': return '#22C55E'; // Green + case 'topics': + case 'social': + default: return '#EF4444'; // Swiss Red + } + }; + + // Current section accent (for logo and global elements) + const currentAccent = colors.primary; + + const sidebarContent = ( + <> + {/* Logo */} +
+ setMobileOpen(false)} + > + {/* Minimal square logo - uses current section accent */} +
+ + + +
+ + Superhero + + +
+ + {/* Navigation */} + + + {/* AI Assistant */} + + + {/* Theme + Tour Row */} +
+ + {hasSeenOnboarding && ( + + )} +
+ + ); + + return ( + <> + {/* Mobile hamburger button */} + + + {/* Desktop Sidebar */} + + + {/* Mobile Sidebar Overlay */} + {mobileOpen && ( +
setMobileOpen(false)} + /> + )} + + {/* Mobile Sidebar */} + + + ); +} diff --git a/src/components/layout/RightRail.tsx b/src/components/layout/RightRail.tsx index e802d09c4..1825d5056 100644 --- a/src/components/layout/RightRail.tsx +++ b/src/components/layout/RightRail.tsx @@ -9,6 +9,7 @@ import { BuyAeWidget } from "../../features/ae-eth-buy"; import { useWallet } from "../../hooks"; import { useAddressByChainName } from "../../hooks/useChainName"; import { useCurrencies } from "@/hooks/useCurrencies"; +import { useSectionTheme } from "./AppLayout"; export default function RightRail({ hidePriceSection = true, @@ -21,6 +22,7 @@ export default function RightRail({ const params = useParams(); const { activeAccount } = useAeSdk(); const { currentCurrencyCode, setCurrentCurrency, currencyRates } = useCurrencies(); + const { colors } = useSectionTheme(); // Resolve chain name if present const isChainName = params.address?.endsWith(".chain"); @@ -71,14 +73,20 @@ export default function RightRail({
{/* Network & Wallet Overview - Hidden on own profile */} {!isOwnProfile && ( -
+
)} {/* Enhanced Price Section (hidden by default via hidePriceSection) */} {!hidePriceSection && ( -
+
πŸ“ˆ @@ -173,10 +181,13 @@ export default function RightRail({ {/* Trading Leaderboard promo */} -
+
πŸ† -

+

Top Traders

@@ -184,7 +195,8 @@ export default function RightRail({ See which wallets are leading the markets by PnL, ROI and AUM on the Trading Leaderboard.

{/* Quick Actions - moved to Right Rail bottom */} -
+
⚑ -

+

Quick Actions

{/* Buy AE with ETH widget (compact) */} -
+
diff --git a/src/components/layout/ThemeSwitcher.tsx b/src/components/layout/ThemeSwitcher.tsx new file mode 100644 index 000000000..fba8d3930 --- /dev/null +++ b/src/components/layout/ThemeSwitcher.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import { useTheme } from "@/contexts/ThemeContext"; + +/** + * ThemeSwitcher - Swiss Minimal Design + * - Clean, no rounded corners + * - Simple icon toggle + * - Minimal styling + */ + +// Sun icon for light mode +const SunIcon = () => ( + + + +); + +// Moon icon for dark mode +const MoonIcon = () => ( + + + +); + +interface ThemeSwitcherProps { + className?: string; +} + +export default function ThemeSwitcher({ className = "" }: ThemeSwitcherProps) { + const { toggleMode, isDark } = useTheme(); + + // Swiss colors + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#71717A' : '#71717A'; + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + const hoverBg = isDark ? '#18181B' : '#F4F4F5'; + + return ( + + ); +} + +// Compact version for mobile or tight spaces +export function ThemeSwitcherCompact({ className = "" }: ThemeSwitcherProps) { + const { toggleMode, isDark } = useTheme(); + + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#71717A' : '#71717A'; + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + + return ( + + ); +} diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx new file mode 100644 index 000000000..d747959b7 --- /dev/null +++ b/src/components/layout/TopBar.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import Breadcrumbs from "./Breadcrumbs"; +import UserProfileChip from "./UserProfileChip"; +import { useTheme } from "@/contexts/ThemeContext"; + +/** + * TopBar - Swiss Minimal Design + * - Clean, no blur effects + * - Simple border + * - Typography-focused + */ + +export default function TopBar() { + const { isDark } = useTheme(); + + const bgColor = isDark ? '#09090B' : '#FFFFFF'; + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + + return ( +
+ {/* Left: Breadcrumbs */} +
+ +
+ + {/* Right: User Profile only (when connected) */} +
+ +
+
+ ); +} diff --git a/src/components/layout/UserProfileChip.tsx b/src/components/layout/UserProfileChip.tsx new file mode 100644 index 000000000..9fa4b2c2a --- /dev/null +++ b/src/components/layout/UserProfileChip.tsx @@ -0,0 +1,222 @@ +import React, { useState, useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAeSdk } from "../../hooks/useAeSdk"; +import { useAccountBalances } from "../../hooks/useAccountBalances"; +import { useWallet } from "../../hooks"; +import AddressAvatarWithChainName from "../../@components/Address/AddressAvatarWithChainName"; +import ConnectWalletButton from "../ConnectWalletButton"; +import { useTheme } from "@/contexts/ThemeContext"; + +/** + * UserProfileChip - Swiss Minimal Design + * - Clean borders, no rounded corners + * - Minimal color palette + * - Typography-focused + */ + +interface UserProfileChipProps { + showOnlyWhenConnected?: boolean; +} + +export default function UserProfileChip({ showOnlyWhenConnected = false }: UserProfileChipProps) { + const navigate = useNavigate(); + const { activeAccount } = useAeSdk(); + const { decimalBalance } = useAccountBalances(activeAccount); + const { chainNames } = useWallet(); + const { isDark } = useTheme(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + const chainName = activeAccount ? chainNames?.[activeAccount] : null; + const displayName = chainName || (activeAccount ? `${activeAccount.slice(0, 8)}...` : null); + const balanceAe = Number(decimalBalance?.toString() || 0); + + // Swiss colors + const textPrimary = isDark ? '#FFFFFF' : '#000000'; + const textSecondary = isDark ? '#71717A' : '#71717A'; + const borderColor = isDark ? '#27272A' : '#E4E4E7'; + const bgColor = isDark ? '#09090B' : '#FFFFFF'; + const hoverBg = isDark ? '#18181B' : '#F4F4F5'; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropdownOpen]); + + if (!activeAccount) { + // If showOnlyWhenConnected, return null (Connect button is in sidebar) + if (showOnlyWhenConnected) return null; + return ; + } + + return ( +
+ + + {/* Dropdown */} + {dropdownOpen && ( +
+ {/* User Info */} +
+
+ +
+
+ {chainName || `${activeAccount.slice(0, 12)}...`} +
+
+ {balanceAe.toLocaleString(undefined, { maximumFractionDigits: 4 })} AE +
+
+
+
+ + {/* Quick Actions */} +
+ + + setDropdownOpen(false)} + className="w-full flex items-center gap-2 px-3 py-2 text-sm no-underline transition-colors" + style={{ color: textSecondary }} + onMouseEnter={(e) => { + e.currentTarget.style.background = hoverBg; + e.currentTarget.style.color = textPrimary; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + e.currentTarget.style.color = textSecondary; + }} + > + + + + View on aeScan + +
+
+ )} +
+ ); +} diff --git a/src/components/onboarding/OnboardingTour.tsx b/src/components/onboarding/OnboardingTour.tsx new file mode 100644 index 000000000..373c07fba --- /dev/null +++ b/src/components/onboarding/OnboardingTour.tsx @@ -0,0 +1,259 @@ +import React, { useEffect, useState, useRef } from "react"; +import { createPortal } from "react-dom"; +import { useOnboarding, OnboardingStep } from "@/contexts/OnboardingContext"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useSectionTheme } from "@/components/layout/AppLayout"; + +interface TooltipPosition { + top: number; + left: number; + arrowPosition: "top" | "bottom" | "left" | "right"; +} + +function calculatePosition( + targetRect: DOMRect, + tooltipWidth: number, + tooltipHeight: number, + position: OnboardingStep["position"] +): TooltipPosition { + const padding = 16; + const arrowSize = 12; + + switch (position) { + case "right": + return { + top: targetRect.top + targetRect.height / 2 - tooltipHeight / 2, + left: targetRect.right + padding, + arrowPosition: "left", + }; + case "left": + return { + top: targetRect.top + targetRect.height / 2 - tooltipHeight / 2, + left: targetRect.left - tooltipWidth - padding, + arrowPosition: "right", + }; + case "bottom": + return { + top: targetRect.bottom + padding, + left: targetRect.left + targetRect.width / 2 - tooltipWidth / 2, + arrowPosition: "top", + }; + case "top": + default: + return { + top: targetRect.top - tooltipHeight - padding, + left: targetRect.left + targetRect.width / 2 - tooltipWidth / 2, + arrowPosition: "bottom", + }; + } +} + +export default function OnboardingTour() { + const { + isOnboardingActive, + currentStep, + steps, + nextStep, + prevStep, + skipOnboarding, + } = useOnboarding(); + const { isDark } = useTheme(); + const { colors } = useSectionTheme(); + const [tooltipPosition, setTooltipPosition] = useState(null); + const [targetRect, setTargetRect] = useState(null); + const tooltipRef = useRef(null); + + const currentStepData = steps[currentStep]; + + // Expand parent menu if needed + useEffect(() => { + if (!isOnboardingActive || !currentStepData) return; + + if (currentStepData.expandParent) { + // Dispatch custom event to expand the menu (without toggling) + const event = new CustomEvent("expand-sidebar-menu", { + detail: { menuId: currentStepData.expandParent }, + }); + window.dispatchEvent(event); + } + }, [isOnboardingActive, currentStep, currentStepData]); + + // Find target element and calculate position + useEffect(() => { + if (!isOnboardingActive || !currentStepData) return; + + const findAndPositionTooltip = () => { + const targetElement = document.getElementById(currentStepData.targetId); + if (!targetElement || !tooltipRef.current) return; + + const rect = targetElement.getBoundingClientRect(); + setTargetRect(rect); + + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + const position = calculatePosition( + rect, + tooltipRect.width || 320, + tooltipRect.height || 200, + currentStepData.position + ); + + // Ensure tooltip stays within viewport + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (position.left < 16) position.left = 16; + if (position.left + 320 > viewportWidth - 16) { + position.left = viewportWidth - 320 - 16; + } + if (position.top < 16) position.top = 16; + if (position.top + 200 > viewportHeight - 16) { + position.top = viewportHeight - 200 - 16; + } + + setTooltipPosition(position); + }; + + // Delay to allow for menu expansion animation + const timer = setTimeout(findAndPositionTooltip, 200); + + // Reposition on resize + window.addEventListener("resize", findAndPositionTooltip); + window.addEventListener("scroll", findAndPositionTooltip, true); + + return () => { + clearTimeout(timer); + window.removeEventListener("resize", findAndPositionTooltip); + window.removeEventListener("scroll", findAndPositionTooltip, true); + }; + }, [isOnboardingActive, currentStep, currentStepData]); + + if (!isOnboardingActive || !currentStepData) return null; + + const arrowClasses = { + top: "bottom-full left-1/2 -translate-x-1/2 border-l-transparent border-r-transparent border-t-transparent", + bottom: "top-full left-1/2 -translate-x-1/2 border-l-transparent border-r-transparent border-b-transparent", + left: "right-full top-1/2 -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent", + right: "left-full top-1/2 -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent", + }; + + return createPortal( + <> + {/* Overlay */} +
+ + {/* Highlight cutout for target element */} + {targetRect && ( +
+ )} + + {/* Tooltip */} +
+ {/* Arrow */} +
+ + {/* Step indicator */} +
+
+ {steps.map((_, index) => ( +
+ ))} +
+ +
+ + {/* Content */} +

{currentStepData.title}

+

+ {currentStepData.description} +

+ + {/* Navigation */} +
+ + +
+
+ , + document.body + ); +} + diff --git a/src/contexts/OnboardingContext.tsx b/src/contexts/OnboardingContext.tsx new file mode 100644 index 000000000..577c8e114 --- /dev/null +++ b/src/contexts/OnboardingContext.tsx @@ -0,0 +1,160 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"; + +export interface OnboardingStep { + id: string; + targetId: string; // DOM element ID to highlight + title: string; + description: string; + position: "top" | "bottom" | "left" | "right"; + expandParent?: string; // Nav item ID to expand before showing this step +} + +interface OnboardingContextValue { + isOnboardingActive: boolean; + currentStep: number; + steps: OnboardingStep[]; + startOnboarding: () => void; + nextStep: () => void; + prevStep: () => void; + skipOnboarding: () => void; + completeOnboarding: () => void; + hasSeenOnboarding: boolean; + resetOnboarding: () => void; +} + +const OnboardingContext = createContext(undefined); + +const ONBOARDING_STORAGE_KEY = "superhero-onboarding-completed"; + +// Define the onboarding steps +const onboardingSteps: OnboardingStep[] = [ + { + id: "hashtags", + targetId: "nav-hashtags", + title: "# Explore Hashtags", + description: "Every hashtag is a token! Discover trending topics on bonding curves. Price moves with buys and sells. Each hashtag creates a DAO with its own treasury.", + position: "right", + expandParent: "hashtags", + }, + { + id: "create", + targetId: "nav-hashtags-create", + title: "✨ Create a Hashtag", + description: "Have a trending idea? Tokenize it! Launch your own hashtag with a DAO and treasury. Let others invest in your vision and watch it grow.", + position: "right", + expandParent: "hashtags", + }, + { + id: "invite", + targetId: "nav-hashtags-invite", + title: "🎁 Invite & Earn", + description: "Share Superhero with friends and earn rewards! Get a percentage of trading fees from users you refer. The more you share, the more you earn.", + position: "right", + expandParent: "hashtags", + }, + { + id: "social", + targetId: "nav-social", + title: "πŸ’¬ Social", + description: "Connect with the community! Post updates, share insights, and tip creators directly on-chain. All your social interactions are permanently stored on the Γ¦ternity blockchain.", + position: "right", + }, + { + id: "defi", + targetId: "nav-defi", + title: "🏦 DeFi", + description: "Access decentralized finance tools! Swap tokens, provide liquidity to pools, wrap AE, bridge assets from Ethereum, and buy AE directly with ETH.", + position: "right", + }, +]; + +interface OnboardingProviderProps { + children: ReactNode; +} + +export function OnboardingProvider({ children }: OnboardingProviderProps) { + const [hasSeenOnboarding, setHasSeenOnboarding] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem(ONBOARDING_STORAGE_KEY) === "true"; + } + return false; + }); + + const [isOnboardingActive, setIsOnboardingActive] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + + // Auto-start onboarding for new users after a short delay + useEffect(() => { + if (!hasSeenOnboarding) { + const timer = setTimeout(() => { + setIsOnboardingActive(true); + }, 1500); // 1.5 second delay to let the page load + return () => clearTimeout(timer); + } + }, [hasSeenOnboarding]); + + const startOnboarding = useCallback(() => { + setCurrentStep(0); + setIsOnboardingActive(true); + }, []); + + const nextStep = useCallback(() => { + if (currentStep < onboardingSteps.length - 1) { + setCurrentStep((prev) => prev + 1); + } else { + completeOnboarding(); + } + }, [currentStep]); + + const prevStep = useCallback(() => { + if (currentStep > 0) { + setCurrentStep((prev) => prev - 1); + } + }, [currentStep]); + + const skipOnboarding = useCallback(() => { + setIsOnboardingActive(false); + setHasSeenOnboarding(true); + localStorage.setItem(ONBOARDING_STORAGE_KEY, "true"); + }, []); + + const completeOnboarding = useCallback(() => { + setIsOnboardingActive(false); + setHasSeenOnboarding(true); + localStorage.setItem(ONBOARDING_STORAGE_KEY, "true"); + }, []); + + const resetOnboarding = useCallback(() => { + localStorage.removeItem(ONBOARDING_STORAGE_KEY); + setHasSeenOnboarding(false); + setCurrentStep(0); + }, []); + + const value: OnboardingContextValue = { + isOnboardingActive, + currentStep, + steps: onboardingSteps, + startOnboarding, + nextStep, + prevStep, + skipOnboarding, + completeOnboarding, + hasSeenOnboarding, + resetOnboarding, + }; + + return ( + + {children} + + ); +} + +export function useOnboarding() { + const context = useContext(OnboardingContext); + if (!context) { + throw new Error("useOnboarding must be used within an OnboardingProvider"); + } + return context; +} + diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 000000000..0c4246d86 --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -0,0 +1,99 @@ +import React, { createContext, useContext, useEffect, useState, ReactNode } from "react"; + +export type ThemeMode = "light" | "dark"; + +interface ThemeContextValue { + mode: ThemeMode; + toggleMode: () => void; + setMode: (mode: ThemeMode) => void; + isDark: boolean; +} + +const ThemeContext = createContext(undefined); + +const THEME_STORAGE_KEY = "superhero-theme-mode"; + +function getInitialTheme(): ThemeMode { + // Check localStorage first + if (typeof window !== "undefined") { + const stored = localStorage.getItem(THEME_STORAGE_KEY); + if (stored === "light" || stored === "dark") { + return stored; + } + // Check system preference + if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } + } + return "dark"; // Default to dark +} + +interface ThemeProviderProps { + children: ReactNode; +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const [mode, setModeState] = useState(getInitialTheme); + + // Apply theme class to document + useEffect(() => { + const root = document.documentElement; + + if (mode === "dark") { + root.classList.add("dark"); + root.classList.remove("light"); + } else { + root.classList.add("light"); + root.classList.remove("dark"); + } + + // Save to localStorage + localStorage.setItem(THEME_STORAGE_KEY, mode); + }, [mode]); + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const handleChange = (e: MediaQueryListEvent) => { + const stored = localStorage.getItem(THEME_STORAGE_KEY); + // Only auto-switch if user hasn't set a preference + if (!stored) { + setModeState(e.matches ? "dark" : "light"); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); + + const toggleMode = () => { + setModeState((prev) => (prev === "light" ? "dark" : "light")); + }; + + const setMode = (newMode: ThemeMode) => { + setModeState(newMode); + }; + + const value: ThemeContextValue = { + mode, + toggleMode, + setMode, + isDark: mode === "dark", + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} + diff --git a/src/features/dex/components/PoolHeader.tsx b/src/features/dex/components/PoolHeader.tsx index f4719d966..e4cf970ae 100644 --- a/src/features/dex/components/PoolHeader.tsx +++ b/src/features/dex/components/PoolHeader.tsx @@ -2,6 +2,7 @@ import { PairDto } from "@/api/generated"; import { AddressChip } from "@/components/AddressChip"; import AeButton from "@/components/AeButton"; import { TokenChip } from "@/components/TokenChip"; +import { useSectionTheme } from "@/components/layout/AppLayout"; import { useNavigate } from "react-router-dom"; interface PoolHeaderProps { @@ -10,6 +11,7 @@ interface PoolHeaderProps { export function PoolHeader({ pairData }: PoolHeaderProps) { const navigate = useNavigate(); + const { colors } = useSectionTheme(); return ( <> @@ -38,7 +40,8 @@ export function PoolHeader({ pairData }: PoolHeaderProps) { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Swap @@ -50,7 +53,8 @@ export function PoolHeader({ pairData }: PoolHeaderProps) { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Add Liquidity @@ -60,7 +64,8 @@ export function PoolHeader({ pairData }: PoolHeaderProps) { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > View {pairData?.token0?.symbol || "Token"} @@ -70,7 +75,8 @@ export function PoolHeader({ pairData }: PoolHeaderProps) { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > View {pairData?.token1?.symbol || "Token"} diff --git a/src/features/social/components/SortControls.tsx b/src/features/social/components/SortControls.tsx index 4c4a8e0c2..3511a81b2 100644 --- a/src/features/social/components/SortControls.tsx +++ b/src/features/social/components/SortControls.tsx @@ -8,6 +8,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { useSectionTheme } from "@/components/layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; interface SortControlsProps { sortBy: string; @@ -21,6 +23,9 @@ interface SortControlsProps { // Component: Sort Controls const SortControls = memo( ({ sortBy, onSortChange, className = "", popularWindow = 'all', onPopularWindowChange, popularFeedEnabled = true }: SortControlsProps) => { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + // Show "Latest Feed" title if popular feed is disabled if (!popularFeedEnabled) { return ( @@ -144,7 +149,12 @@ const SortControls = memo( {/* Desktop: keep existing pill style */}
-
+
onSortChange("hot")} variant={sortBy === "hot" ? "default" : "ghost"} @@ -152,10 +162,15 @@ const SortControls = memo( noShadow={true} className={cn( "rounded-full px-3 py-1 text-xs font-semibold transition-all flex-1 w-full md:w-24 md:uppercase", - sortBy === "hot" - ? "bg-[#1161FE] text-white hover:bg-[#1161FE] focus:bg-[#1161FE]" - : "text-white/70 hover:text-white hover:bg-white/10 focus:text-white focus:bg-white/10" + sortBy !== "hot" && (isDark + ? "text-slate-400 hover:text-white hover:bg-slate-700" + : "text-gray-600 hover:text-gray-900 hover:bg-gray-100") )} + style={sortBy === "hot" ? { + background: colors.gradient, + color: 'white', + boxShadow: `0 4px 12px ${colors.primary}40` + } : undefined} > Popular @@ -166,16 +181,26 @@ const SortControls = memo( noShadow={true} className={cn( "rounded-full px-3 py-1 text-xs font-semibold transition-all flex-1 w-full md:w-24 md:uppercase", - sortBy === "latest" - ? "bg-[#1161FE] text-white hover:bg-[#1161FE] focus:bg-[#1161FE]" - : "text-white/70 hover:text-white hover:bg-white/10 focus:text-white focus:bg-white/10" + sortBy !== "latest" && (isDark + ? "text-slate-400 hover:text-white hover:bg-slate-700" + : "text-gray-600 hover:text-gray-900 hover:bg-gray-100") )} + style={sortBy === "latest" ? { + background: colors.gradient, + color: 'white', + boxShadow: `0 4px 12px ${colors.primary}40` + } : undefined} > Latest
{sortBy === 'hot' && ( -
+
{(['24h','7d','all'] as const).map((tf) => { const isActive = popularWindow === tf; const label = tf === '24h' ? 'Today' : tf === '7d' ? 'This week' : 'All time'; @@ -185,10 +210,16 @@ const SortControls = memo( onClick={() => onPopularWindowChange && onPopularWindowChange(tf)} className={cn( 'px-3 py-1.5 text-[11px] rounded-full border transition-all duration-300', - isActive - ? 'bg-[#1161FE] text-white border-transparent shadow-sm' - : 'bg-transparent text-white/80 border-white/10 hover:bg-white/10' + !isActive && (isDark + ? 'bg-transparent text-slate-400 border-transparent hover:bg-slate-700' + : 'bg-transparent text-gray-600 border-transparent hover:bg-gray-100') )} + style={isActive ? { + background: colors.gradient, + color: 'white', + border: 'transparent', + boxShadow: `0 4px 12px ${colors.primary}40` + } : undefined} > {label} diff --git a/src/features/trending/components/TokenLineChart.tsx b/src/features/trending/components/TokenLineChart.tsx index f1bace38e..ff9bf3b16 100644 --- a/src/features/trending/components/TokenLineChart.tsx +++ b/src/features/trending/components/TokenLineChart.tsx @@ -14,6 +14,9 @@ interface TokenLineChartProps { hideTimeframe?: boolean; timeframe?: string; className?: string; + lineColor?: string; + topColor?: string; + bottomColor?: string; } @@ -32,11 +35,18 @@ export function TokenLineChart({ height = 200, hideTimeframe = false, className, + lineColor = 'rgb(245, 158, 11)', + topColor, + bottomColor, }: TokenLineChartProps) { const [loading, setLoading] = useState(false); const areaSeries = useRef | undefined>(); const performanceChartTimeframe = useAtomValue(performanceChartTimeframeAtom); + // Generate gradient colors from line color if not provided + const computedTopColor = topColor || lineColor.replace('rgb', 'rgba').replace(')', ', 0.25)'); + const computedBottomColor = bottomColor || lineColor.replace('rgb', 'rgba').replace(')', ', 0.02)'); + const { data } = useQuery({ queryFn: () => TransactionHistoricalService.getForPreview({ @@ -79,9 +89,9 @@ export function TokenLineChart({ onChartReady: (chartInstance) => { const seriesOptions: AreaSeriesPartialOptions = { priceLineVisible: false, - lineColor: 'rgb(245, 158, 11)', - topColor: 'rgba(245, 158, 11, 0.2)', - bottomColor: 'rgba(245, 158, 11, 0.01)', + lineColor: lineColor, + topColor: computedTopColor, + bottomColor: computedBottomColor, lineWidth: 2, crosshairMarkerVisible: false, baseLineVisible: true, @@ -156,4 +166,4 @@ export function TokenLineChart({ } -export default TokenLineChart; \ No newline at end of file +export default TokenLineChart; diff --git a/src/features/trending/components/TrendminerBanner.tsx b/src/features/trending/components/TrendminerBanner.tsx index 31e860d5d..973fa8ee4 100644 --- a/src/features/trending/components/TrendminerBanner.tsx +++ b/src/features/trending/components/TrendminerBanner.tsx @@ -1,19 +1,33 @@ import React from "react"; import AeButton from "../../../components/AeButton"; import GlobalStatsAnalytics from "../../../components/Trendminer/GlobalStatsAnalytics"; +import { useSectionTheme } from "@/components/layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; export default function TrendminerBanner() { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + return ( -
+
-
- Tokenize Trends. +
+ Every #Hashtag
- Own the Hype. + is a Token.
- Build Communities. + Own the Trends.
@@ -22,9 +36,13 @@ export default function TrendminerBanner() { size="md" rounded onClick={() => (window.location.href = "/trends/create")} - className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300 text-white hover:opacity-90" + style={{ + background: colors.gradient, + boxShadow: `0 4px 16px ${colors.primary}40` + }} > - TOKENIZE A TREND + CREATE HASHTAG (window.location.href = "/trends/daos")} - className="bg-gradient-to-r from-pink-400 to-rose-400 hover:from-pink-500 hover:to-rose-500 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300 text-white hover:opacity-90" + style={{ + background: colors.gradient, + boxShadow: `0 4px 16px ${colors.primary}40` + }} > EXPLORE DAOS @@ -43,18 +65,21 @@ export default function TrendminerBanner() { onClick={() => (window.location.href = "/trends/invite") } - className="bg-gradient-to-r from-slate-600 to-slate-700 hover:from-slate-500 hover:to-slate-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300 text-white hover:opacity-90" + style={{ + background: colors.gradient, + boxShadow: `0 4px 16px ${colors.primary}40` + }} > INVITE & EARN
-
- Tokenized trends are community DAO tokens launched on a bonding - curve. Price moves with buys/sells, no order books. Each token - creates a DAO with treasury that can fund initiatives via on-chain - votes. Connect your wallet to trade and participate. +
+ Hashtags are tokenized trends with their own DAO and treasury. + Prices move on a bonding curve - buy to support, sell to exit. + Each hashtag community can fund initiatives through on-chain voting.
diff --git a/src/features/trending/views/TokenList.tsx b/src/features/trending/views/TokenList.tsx index 8d04dd4b2..b42f2edfc 100644 --- a/src/features/trending/views/TokenList.tsx +++ b/src/features/trending/views/TokenList.tsx @@ -187,8 +187,8 @@ export default function TokenList() { return (
@@ -202,8 +202,8 @@ export default function TokenList() { {/* Left: Token List */}
-
- Tokenized Trends +
+ All Hashtags
{/* FILTERS */} @@ -211,12 +211,12 @@ export default function TokenList() { {/* OrderBy Filter */}
setSearch(e.target.value)} - placeholder="Search tokens" - className="px-2 py-2 h-10 min-h-[auto] bg-white/[0.02] text-white border border-white/10 backdrop-blur-[10px] rounded-lg text-xs focus:outline-none focus:border-[#1161FE] placeholder-white/50 transition-all duration-300 hover:bg-white/[0.05] w-full md:flex-1 min-w-[160px] md:max-w-none" - /> + setSearch(e.target.value)} + placeholder="Search hashtags..." + className="px-2 py-2 h-10 min-h-[auto] bg-white text-gray-900 border border-gray-200 rounded-lg text-xs focus:outline-none focus:border-[#1161FE] placeholder-gray-400 transition-all duration-300 hover:bg-gray-50 w-full md:flex-1 min-w-[160px] md:max-w-none" + /> {/* Performance Timeframe Selector */}
@@ -257,8 +257,8 @@ export default function TokenList() { {/* Message Box for no results */} {(!data?.pages?.length || !data?.pages[0].items.length) && !isFetching && ( -
-

No Token Sales

+
+

No Token Sales

No tokens found matching your criteria.

)} @@ -286,9 +286,9 @@ export default function TokenList() { ref={loadMoreBtn} onClick={() => fetchNextPage()} disabled={isFetching} - className={`px-6 py-3 rounded-full border-none text-white cursor-pointer text-base font-semibold tracking-wide transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] ${isFetching - ? 'bg-white/10 cursor-not-allowed opacity-60' - : 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300' + className={`px-6 py-3 rounded-full border-none cursor-pointer text-base font-semibold tracking-wide transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] ${isFetching + ? 'bg-gray-200 text-gray-500 cursor-not-allowed opacity-60' + : 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-300' }`} > {isFetching ? ( diff --git a/src/main.tsx b/src/main.tsx index 939f68744..2133ef1de 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,6 +10,8 @@ import ErrorBoundary from './components/ErrorBoundary'; import ToastProvider from './components/ToastProvider'; import { AeSdkProvider } from './context/AeSdkProvider'; import { AePricePollingProvider } from './context/AePricePollingProvider'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { OnboardingProvider } from './contexts/OnboardingContext'; import './i18n'; import './styles/base.scss'; import './styles/tailwind.css'; @@ -44,9 +46,13 @@ const queryClient = new QueryClient({ - - - + + + + + + + diff --git a/src/routes.tsx b/src/routes.tsx index 0841e0c8b..dcceab568 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,8 +1,15 @@ import React, { lazy } from "react"; import { RouteObject, Navigate, useParams } from "react-router-dom"; -import SocialLayout from "./components/layout/SocialLayout"; +import AppLayout from "./components/layout/AppLayout"; +// New Home page +const Home = lazy(() => import("./views/Home")); + +// Social components const FeedList = lazy(() => import("./features/social/views/FeedList")); +const PostDetail = lazy(() => import("./features/social/views/PostDetail")); +const UserProfile = lazy(() => import("./views/UserProfile")); + const NotFound = lazy(() => import("./views/NotFound")); const TokenList = lazy(() => import("./features/trending/views/TokenList")); const TrendCloudVisx = lazy(() => import("./views/Trendminer/TrendCloudVisx")); @@ -26,8 +33,6 @@ const LeaderboardView = lazy( const TokenSaleDetails = lazy( () => import("./features/trending/views/TokenSaleDetails") ); -const PostDetail = lazy(() => import("./features/social/views/PostDetail")); -const UserProfile = lazy(() => import("./views/UserProfile")); const Landing = lazy(() => import("./views/Landing")); const Conference = lazy(() => import("./views/Conference")); const Governance = lazy(() => import("./views/Governance")); @@ -91,167 +96,162 @@ function NavigateUserProfile() { } export const routes: RouteObject[] = [ + // All routes wrapped in AppLayout { path: "/", - element: , + element: , children: [ - { index: true, element: }, - // Post routes - slug-based (also handles IDs, which will redirect in PostDetail) - { path: "post/:slug", element: }, + // New landing page (Topics-focused) + { index: true, element: }, + + // Social routes (moved from root) + { path: "social", element: }, + { path: "post/:slug", element: }, + { path: "post/:slug/comment/:id", element: }, + { path: "users/:address", element: }, + + // Topics/Trends routes + { path: "trends/tokens", element: }, + { path: "trends", element: }, + { path: "trends/visx", element: }, + { path: "trends/tokens/:tokenName", element: }, + { path: "trends/leaderboard", element: }, + { path: "trends/invite", element: }, + { path: "trends/daos", element: }, + { path: "trends/dao/:saleAddress", element: }, + { path: "trends/dao/:saleAddress/vote/:voteId/:voteAddress", element: }, + { path: "trends/accounts", element: }, + { path: "trends/accounts/:address", element: }, + { path: "trends/create", element: }, + + // DeFi routes + { path: "defi", element: }, { - path: "post/:slug/comment/:id", - element: , + path: "defi/swap", + element: ( + + + + ), + }, + { + path: "defi/wrap", + element: ( + + + + ), + }, + { + path: "defi/buy-ae-with-eth", + element: ( + + + + ), + }, + { + path: "defi/bridge", + element: ( + + + + ), + }, + { + path: "defi/pool", + element: ( + + + + ), + }, + { + path: "defi/pool/add-tokens", + element: ( + + + + ), + }, + { + path: "defi/explore/tokens", + element: ( + + + + ), + }, + { + path: "defi/explore/tokens/:tokenAddress", + element: ( + + + + ), + }, + { + path: "defi/explore/pools", + element: ( + + + + ), + }, + { + path: "defi/explore/pools/:poolAddress", + element: ( + + + + ), + }, + { + path: "defi/explore/transactions", + element: ( + + + + ), }, - { path: "users/:address", element: }, - ], - }, - // New trends routes - { path: "/trends/tokens", element: }, - { path: "/trends", element: }, - { path: "/trends/visx", element: }, - { path: "/trends/tokens/:tokenName", element: }, - { path: "/trends/leaderboard", element: }, - { path: "/tx-queue/:id", element: }, - { path: "/trends/invite", element: }, - { path: "/trends/daos", element: }, - { path: "/trends/dao/:saleAddress", element: }, - { path: "/trends/dao/:saleAddress/vote/:voteId/:voteAddress", element: }, - { path: "/trends/accounts", element: }, - { path: "/trends/accounts/:address", element: }, - { path: "/trends/create", element: }, - // Legacy redirects from /trending/* -> /trends/* - { path: "/trending", element: }, - { path: "/trending/tokens", element: }, - { path: "/trending/visx", element: }, - { path: "/trending/invite", element: }, - { path: "/trending/daos", element: }, - { path: "/trending/accounts", element: }, - { path: "/trending/create", element: }, - { path: "/trending/leaderboard", element: }, - // Param redirects using small wrappers - { path: "/trending/tokens/:tokenName", element: }, - { path: "/trending/dao/:saleAddress", element: }, - { path: "/trending/dao/:saleAddress/vote/:voteId/:voteAddress", element: }, - { path: "/trending/accounts/:address", element: }, - // Redirect /user/* to /users/* for consistency - { - path: "/user/:address", - element: , - }, - { path: "/landing", element: }, - { path: "/meet/:room?", element: }, - { path: "/voting", element: }, - { path: "/voting/p/:id", element: }, - { path: "/voting/account", element: }, - { path: "/voting/create", element: }, - // New DEX Routes with Layout - { - path: "/defi", - element: , - }, - { - path: "/defi/swap", - element: ( - - - - ), - }, - { - path: "/defi/wrap", - element: ( - - - - ), - }, - { - path: "/defi/buy-ae-with-eth", - element: ( - - - - ), - }, - { - path: "/defi/bridge", - element: ( - - - - ), - }, - { - path: "/defi/pool", - element: ( - - - - ), - }, - { - path: "/defi/pool/add-tokens", - element: ( - - - - ), - }, - { - path: "/defi/explore/tokens", - element: ( - - - - ), - }, - { - path: "/defi/explore/tokens/:tokenAddress", - element: ( - - - - ), - }, - { - path: "/defi/explore/pools", - element: ( - - - - ), - }, - { - path: "/defi/explore/pools/:poolAddress", - element: ( - - - - ), - }, - { - path: "/defi/explore/transactions", - element: ( - - - - ), - }, + // Other routes + { path: "tx-queue/:id", element: }, + { path: "landing", element: }, + { path: "meet/:room?", element: }, + { path: "voting", element: }, + { path: "voting/p/:id", element: }, + { path: "voting/account", element: }, + { path: "voting/create", element: }, + { path: "terms", element: }, + { path: "privacy", element: }, + { path: "faq", element: }, - // Legacy DEX Routes (for backward compatibility) - { path: "/swap", element: }, - { path: "/pool", element: }, - { path: "/explore", element: }, - { path: "/explore/tokens/:id", element: }, - { path: "/explore/pools/:id", element: }, - { path: "/pool/add-tokens", element: }, + // Legacy redirects + { path: "trending", element: }, + { path: "trending/tokens", element: }, + { path: "trending/visx", element: }, + { path: "trending/invite", element: }, + { path: "trending/daos", element: }, + { path: "trending/accounts", element: }, + { path: "trending/create", element: }, + { path: "trending/leaderboard", element: }, + { path: "trending/tokens/:tokenName", element: }, + { path: "trending/dao/:saleAddress", element: }, + { path: "trending/dao/:saleAddress/vote/:voteId/:voteAddress", element: }, + { path: "trending/accounts/:address", element: }, + { path: "user/:address", element: }, - { path: "/terms", element: }, - { path: "/privacy", element: }, - { path: "/faq", element: }, - { - path: "*", - element: , + // Legacy DEX Routes + { path: "swap", element: }, + { path: "pool", element: }, + { path: "explore", element: }, + { path: "explore/tokens/:id", element: }, + { path: "explore/pools/:id", element: }, + { path: "pool/add-tokens", element: }, + + // 404 + { path: "*", element: }, + ], }, ]; diff --git a/src/styles/base.scss b/src/styles/base.scss index be39ad1f8..0e88a3033 100644 --- a/src/styles/base.scss +++ b/src/styles/base.scss @@ -2,6 +2,16 @@ @use './mixins.scss' as *; @use './mobile-optimizations.scss' as *; @import './tailwind.css'; +@import './themes.scss'; + +// Import Space Grotesk for distinctive headings +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); + +// Typography - Use Space Grotesk for headings +h1, h2, h3, .font-display { + font-family: 'Space Grotesk', Inter, -apple-system, BlinkMacSystemFont, sans-serif; + letter-spacing: -0.02em; +} :root { color-scheme: dark; diff --git a/src/styles/themes.scss b/src/styles/themes.scss new file mode 100644 index 000000000..acfca2055 --- /dev/null +++ b/src/styles/themes.scss @@ -0,0 +1,95 @@ +// Section-specific color theming for Superhero +// Each major section (Topics, Social, DeFi) has its own color palette + +:root { + // Topics (Trends) - Amber/Orange theme + --topics-primary: #F59E0B; + --topics-primary-light: #FCD34D; + --topics-primary-dark: #D97706; + --topics-gradient: linear-gradient(135deg, #F59E0B 0%, #D97706 100%); + --topics-bg-gradient: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.05) 100%); + --topics-border: rgba(245, 158, 11, 0.3); + + // Social - Cyan/Teal theme + --social-primary: #06B6D4; + --social-primary-light: #67E8F9; + --social-primary-dark: #0891B2; + --social-gradient: linear-gradient(135deg, #06B6D4 0%, #0891B2 100%); + --social-bg-gradient: linear-gradient(135deg, rgba(6, 182, 212, 0.1) 0%, rgba(8, 145, 178, 0.05) 100%); + --social-border: rgba(6, 182, 212, 0.3); + + // DeFi - Violet/Indigo theme + --defi-primary: #8B5CF6; + --defi-primary-light: #C4B5FD; + --defi-primary-dark: #6366F1; + --defi-gradient: linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%); + --defi-bg-gradient: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(99, 102, 241, 0.05) 100%); + --defi-border: rgba(139, 92, 246, 0.3); + + // Dynamic section variables (set via JS in AppLayout) + --section-primary: var(--topics-primary); + --section-primary-light: var(--topics-primary-light); + --section-gradient: var(--topics-gradient); + --section-border: var(--topics-border); +} + +// Section-specific body backgrounds (optional enhancement) +body[data-section="topics"] { + --section-primary: var(--topics-primary); + --section-primary-light: var(--topics-primary-light); + --section-gradient: var(--topics-gradient); + --section-border: var(--topics-border); +} + +body[data-section="social"] { + --section-primary: var(--social-primary); + --section-primary-light: var(--social-primary-light); + --section-gradient: var(--social-gradient); + --section-border: var(--social-border); +} + +body[data-section="defi"] { + --section-primary: var(--defi-primary); + --section-primary-light: var(--defi-primary-light); + --section-gradient: var(--defi-gradient); + --section-border: var(--defi-border); +} + +// Utility classes for section theming +.section-text { + color: var(--section-primary); +} + +.section-bg { + background: var(--section-gradient); +} + +.section-border { + border-color: var(--section-border); +} + +.section-bg-subtle { + background: linear-gradient(135deg, + rgba(var(--section-primary), 0.1) 0%, + rgba(var(--section-primary), 0.02) 100% + ); +} + +// Topics-specific styles +.topics-theme { + --local-primary: var(--topics-primary); + --local-gradient: var(--topics-gradient); +} + +// Social-specific styles +.social-theme { + --local-primary: var(--social-primary); + --local-gradient: var(--social-gradient); +} + +// DeFi-specific styles +.defi-theme { + --local-primary: var(--defi-primary); + --local-gradient: var(--defi-gradient); +} + diff --git a/src/views/Home.tsx b/src/views/Home.tsx new file mode 100644 index 000000000..2e95091e5 --- /dev/null +++ b/src/views/Home.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useRef, useState, useMemo } from "react"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import HeroSection from "../components/landing/HeroSection"; +import FeaturedTopics from "../components/landing/FeaturedTopics"; +import LiveActivityTicker from "../components/landing/LiveActivityTicker"; +import EnhancedTokenList from "../components/landing/EnhancedTokenList"; +import Head from "../seo/Head"; +import { useSectionTheme } from "@/components/layout/AppLayout"; +import { useTheme } from "@/contexts/ThemeContext"; +import { TokensService } from "@/api/generated"; +import { useAccount } from "@/hooks"; +import Spinner from "@/components/Spinner"; + +type SortOption = 'trending_score' | 'market_cap' | 'price' | 'newest' | 'holders_count'; + +export default function Home() { + const { colors } = useSectionTheme(); + const { isDark } = useTheme(); + const { activeAccount } = useAccount(); + + // Scroll tracking for section transition + const [hasScrolledPastHero, setHasScrolledPastHero] = useState(false); + const sectionRef = useRef(null); + + // Token list state + const [orderBy, setOrderBy] = useState('trending_score'); + const [search, setSearch] = useState(""); + const [searchThrottled, setSearchThrottled] = useState(""); + const loadMoreBtn = useRef(null); + + // Track scroll position + useEffect(() => { + const handleScroll = () => { + if (sectionRef.current) { + const rect = sectionRef.current.getBoundingClientRect(); + setHasScrolledPastHero(rect.top <= 100); + } + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + // Throttle search input + useEffect(() => { + const timeoutId = setTimeout(() => { + setSearchThrottled(search); + }, 500); + return () => clearTimeout(timeoutId); + }, [search]); + + const orderByMapped = useMemo(() => { + if (orderBy === 'newest') { + return 'created_at'; + } + return orderBy; + }, [orderBy]); + + const finalOrderDirection = useMemo((): 'ASC' | 'DESC' => { + if (orderBy === 'newest') return 'DESC'; + return 'DESC'; + }, [orderBy]); + + const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ + initialPageParam: 1, + queryFn: ({ pageParam = 1 }) => + TokensService.listAll({ + orderBy: orderByMapped as any, + orderDirection: finalOrderDirection, + search: searchThrottled || undefined, + limit: 20, + page: pageParam, + }), + getNextPageParam: (lastPage: any, allPages, lastPageParam) => + lastPage?.meta?.currentPage === lastPage?.meta?.totalPages + ? undefined + : lastPageParam + 1, + queryKey: [ + "Home.TokenList", + orderBy, + orderByMapped, + finalOrderDirection, + searchThrottled, + ], + staleTime: 1000 * 60, + }); + + // Flatten all token items from pages + const allTokens = useMemo(() => + data?.pages?.flatMap(page => page.items) || [], + [data?.pages] + ); + + // Intersection observer for infinite loading + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.intersectionRatio === 1 && hasNextPage && !isFetching) { + fetchNextPage(); + } + }, + { threshold: 1 } + ); + if (loadMoreBtn.current) { + observer.observe(loadMoreBtn.current); + } + return () => observer.disconnect(); + }, [hasNextPage, isFetching, fetchNextPage]); + + return ( +
+ + + {/* Hero Section */} + + + {/* Live Activity Ticker */} + + + {/* Featured Topics/Hashtags Grid */} + + + {/* Full Token List - Continuous Scroll */} +
+ {/* Enhanced Token List */} + + + {/* Load More Button */} + {hasNextPage && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/views/TokenDetail.tsx b/src/views/TokenDetail.tsx index 427571da3..aa4bb5ea9 100644 --- a/src/views/TokenDetail.tsx +++ b/src/views/TokenDetail.tsx @@ -10,6 +10,7 @@ import { useAeSdk } from "../hooks"; import { Decimal } from "../libs/decimal"; import Spinner from "../components/Spinner"; import { getPairsByTokenUsd, getTokenWithUsd } from "../libs/dexBackend"; +import { useSectionTheme } from "@/components/layout/AppLayout"; interface TokenData { address: string; @@ -41,6 +42,7 @@ export default function TokenDetail() { const { activeNetwork } = useAeSdk(); const { tokenAddress } = useParams(); const navigate = useNavigate(); + const { colors } = useSectionTheme(); const [token, setToken] = useState(null); const [tokenMetaData, setTokenMetaData] = useState(null); const [loading, setLoading] = useState(true); @@ -172,7 +174,8 @@ export default function TokenDetail() { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Swap @@ -182,7 +185,8 @@ export default function TokenDetail() { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Add Liquidity @@ -192,7 +196,8 @@ export default function TokenDetail() { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Pools ({tokenDetails?.pairs_count || 0}) @@ -202,7 +207,8 @@ export default function TokenDetail() { } variant="secondary-dark" size="medium" - className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300" + className="border-0 shadow-lg hover:shadow-xl transition-all duration-300" + style={{ background: colors.gradient }} > Transactions diff --git a/tailwind.config.js b/tailwind.config.js index c1045dcfc..3042731c6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -88,6 +88,7 @@ export default { }, fontFamily: { sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'sans-serif'], + display: ['Space Grotesk', 'Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], }, keyframes: { "accordion-down": {