feat(web): redesign login page and proxy strategy market to vergex.trade

- LoginPage: two-column desktop layout with brand panel (status pill,
  gradient headline, stats strip) and form panel; single-column mobile
  layout with centered brand mark. Self-contained grid centering so
  layout no longer depends on parent flex behavior. Drop the dead
  OnboardingModeSelector (it belongs to SetupPage, not login) and add
  loader spinner, animated submit arrow, and clearer error banner.
- StrategyMarketPage: replace the 560-line bespoke marketplace with a
  branded handoff to vergex.trade/explore. Direct iframe embedding is
  currently blocked by vergex's X-Frame-Options: SAMEORIGIN and
  frame-ancestors 'self', and there is no way to reliably detect the
  block from JavaScript (load event fires for the browser error page,
  contentWindow.location throws SecurityError in both success and
  failure). The component now renders a centered card with the
  POWERED BY VERGEX.TRADE pill, headline, description, gold CTA, and
  a stats row, with all three supported languages.
- .gitignore: exclude .gstack/ (local security audit reports).
This commit is contained in:
tinkle-community
2026-05-29 16:14:46 +08:00
parent 99361cb085
commit 75832f9eb2
3 changed files with 287 additions and 592 deletions

View File

@@ -1,560 +1,141 @@
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useNavigate } from 'react-router-dom'
import useSWR from 'swr'
import {
TrendingUp,
Shield,
Zap,
Eye,
EyeOff,
Copy,
Check,
Hexagon,
Layers,
Target,
Activity,
Terminal,
Cpu,
Database,
} from 'lucide-react'
import { ExternalLink } from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner'
import { t } from '../i18n/translations'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
interface PublicStrategy {
id: string
name: string
description: string
author_email?: string
is_public: boolean
config_visible: boolean
config?: any
stats?: {
used_by: number
rating: number
}
created_at: string
updated_at: string
}
const strategyStyles: Record<
string,
{
color: string
border: string
glow: string
shadow: string
icon: any
bg: string
}
> = {
scalper: {
color: 'text-[#F0B90B]',
border: 'border-[#F0B90B]/30',
glow: 'shadow-[0_0_20px_rgba(240,185,11,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(240,185,11,0.25)]',
bg: 'bg-[#F0B90B]/5',
icon: Zap,
},
swing: {
color: 'text-cyan-400',
border: 'border-cyan-400/30',
glow: 'shadow-[0_0_20px_rgba(34,211,238,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(34,211,238,0.25)]',
bg: 'bg-cyan-400/5',
icon: TrendingUp,
},
arbitrage: {
color: 'text-purple-400',
border: 'border-purple-400/30',
glow: 'shadow-[0_0_20px_rgba(192,132,252,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(192,132,252,0.25)]',
bg: 'bg-purple-400/5',
icon: Layers,
},
conservative: {
color: 'text-emerald-400',
border: 'border-emerald-400/30',
glow: 'shadow-[0_0_20px_rgba(52,211,153,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(52,211,153,0.25)]',
bg: 'bg-emerald-400/5',
icon: Shield,
},
aggressive: {
color: 'text-red-500',
border: 'border-red-500/30',
glow: 'shadow-[0_0_20px_rgba(239,68,68,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(239,68,68,0.25)]',
bg: 'bg-red-500/5',
icon: Target,
},
default: {
color: 'text-zinc-400',
border: 'border-zinc-700',
glow: '',
shadow: 'hover:shadow-[0_0_20px_rgba(255,255,255,0.05)]',
bg: 'bg-zinc-800/20',
icon: Activity,
},
}
function getStrategyStyle(name: string) {
const lowerName = name.toLowerCase()
if (lowerName.includes('scalp')) return strategyStyles.scalper
if (lowerName.includes('swing')) return strategyStyles.swing
if (lowerName.includes('arb')) return strategyStyles.arbitrage
if (lowerName.includes('safe') || lowerName.includes('conserv'))
return strategyStyles.conservative
if (lowerName.includes('aggress') || lowerName.includes('high'))
return strategyStyles.aggressive
return strategyStyles.default
}
const VERGEX_EXPLORE_URL = 'https://vergex.trade/explore'
// Strategy Market — proxied to vergex.trade/explore.
//
// vergex.trade currently sets `X-Frame-Options: SAMEORIGIN` on /explore which
// makes browsers refuse cross-origin embedding and render their own "refused
// to connect" page inside the iframe. There is no reliable way to detect
// this from JavaScript (the iframe's `load` event fires for the browser
// error page, and `contentWindow.location` always throws cross-origin
// regardless of success or failure), so we don't try to be clever — we
// surface a clean external-launch CTA instead.
//
// TO RE-ENABLE INLINE EMBEDDING:
// 1. Ask the vergex.trade team to add NOFX origins to the /explore
// CSP `frame-ancestors` (same as /trending: 'self' https://nofxos.ai
// https://www.nofxos.ai http://127.0.0.1:3000 http://localhost:3000)
// AND drop the `X-Frame-Options: SAMEORIGIN` header on that path.
// 2. Replace this component with the same iframe pattern used by
// DataPage.tsx (which already embeds vergex.trade/trending successfully).
export function StrategyMarketPage() {
const navigate = useNavigate()
const { language } = useLanguage()
const { token, user } = useAuth()
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [copiedId, setCopiedId] = useState<string | null>(null)
const tr = (key: string) => t(`strategyMarket.${key}`, language)
const heading =
language === 'zh'
? 'Vergex 策略市场'
: language === 'id'
? 'Pasar Strategi Vergex'
: 'Vergex Strategy Market'
// Fetch public strategies
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
'public-strategies',
async () => {
const response = await fetch('/api/strategies/public')
if (!response.ok) throw new Error('Failed to fetch strategies')
const data = await response.json()
return data.strategies || []
},
{
refreshInterval: 60000,
revalidateOnFocus: false,
}
)
const description =
language === 'zh'
? '在 Vergex 上探索由社区创建的交易策略,一键复制到您的 NOFX 账户。当前需要在新窗口打开。'
: language === 'id'
? 'Jelajahi strategi trading komunitas di Vergex dan salin ke akun NOFX Anda. Saat ini terbuka di tab baru.'
: 'Explore community-built trading strategies on Vergex and copy them to your NOFX account. Currently opens in a new tab.'
const filteredStrategies =
strategies?.filter((s) => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
return (
s.name.toLowerCase().includes(query) ||
s.description?.toLowerCase().includes(query)
)
}
return true
}) || []
const ctaLabel =
language === 'zh'
? '在 Vergex 打开策略市场'
: language === 'id'
? 'Buka Pasar Strategi di Vergex'
: 'Open Strategy Market on Vergex'
const handleCopyConfig = async (strategy: PublicStrategy) => {
if (!strategy.config) return
try {
await navigator.clipboard.writeText(
JSON.stringify(strategy.config, null, 2)
)
setCopiedId(strategy.id)
toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
const subtitle =
language === 'zh'
? 'POWERED BY VERGEX.TRADE'
: language === 'id'
? 'DITENAGAI OLEH VERGEX.TRADE'
: 'POWERED BY VERGEX.TRADE'
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date
.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
.replace(',', '')
}
const getIndicatorList = (config: any) => {
const indicatorsConfig = config?.ai_config?.indicators || config?.indicators
if (!indicatorsConfig) return []
const indicators = []
if (indicatorsConfig.enable_ema) indicators.push('EMA')
if (indicatorsConfig.enable_macd) indicators.push('MACD')
if (indicatorsConfig.enable_rsi) indicators.push('RSI')
if (indicatorsConfig.enable_atr) indicators.push('ATR')
if (indicatorsConfig.enable_boll) indicators.push('BOLL')
if (indicatorsConfig.enable_volume) indicators.push('VOL')
if (indicatorsConfig.enable_oi) indicators.push('OI')
if (indicatorsConfig.enable_funding_rate) indicators.push('FR')
return indicators
}
const features =
language === 'zh'
? [
{ label: '策略数量', value: '100+' },
{ label: '覆盖市场', value: 'CEX & DEX' },
{ label: '实时数据', value: '24/7' },
]
: language === 'id'
? [
{ label: 'Total Strategi', value: '100+' },
{ label: 'Cakupan Pasar', value: 'CEX & DEX' },
{ label: 'Data Real-time', value: '24/7' },
]
: [
{ label: 'Strategies', value: '100+' },
{ label: 'Markets', value: 'CEX & DEX' },
{ label: 'Live Data', value: '24/7' },
]
return (
<DeepVoidBackground className="min-h-screen text-white font-mono py-12">
<div className="w-full px-4 md:px-8 space-y-8">
<div className="w-full relative z-10">
{/* Header Section */}
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
SYSTEM_STATUS:{' '}
<span className="text-emerald-500 animate-pulse">ONLINE</span>
<br />
MARKET_UPLINK:{' '}
<span className="text-emerald-500">ESTABLISHED</span>
<div className="relative flex h-[calc(100vh-64px)] w-full items-center justify-center overflow-hidden bg-nofx-bg px-6">
{/* Ambient halos */}
<div className="pointer-events-none absolute -left-20 top-1/4 h-96 w-96 rounded-full bg-nofx-gold/[0.06] blur-3xl" />
<div className="pointer-events-none absolute -right-20 bottom-1/4 h-80 w-80 rounded-full bg-nofx-accent/[0.04] blur-3xl" />
{/* Main card */}
<div className="relative w-full max-w-xl">
<div className="overflow-hidden rounded-2xl border border-white/[0.08] bg-zinc-950/60 shadow-2xl shadow-black/40 backdrop-blur-xl">
{/* Top gold edge */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-nofx-gold/40 to-transparent" />
<div className="p-8 sm:p-10 text-center">
{/* Subtitle */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-nofx-gold/25 bg-nofx-gold/[0.06] px-3 py-1">
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-nofx-gold" />
<span className="text-[10.5px] font-mono uppercase tracking-[0.18em] text-nofx-gold">
{subtitle}
</span>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
</div>
<div>
<h1
className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text"
data-text={tr('title')}
>
{tr('title')}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
{'// '}
{tr('subtitle')}
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{tr('description')}
{/* Heading */}
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-white leading-tight">
{heading}
</h1>
{/* Description */}
<p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-zinc-400">
{description}
</p>
</div>
{/* Search and Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 mb-8">
{/* Search */}
<div className="relative flex-1 group">
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-zinc-800/20 rounded opacity-0 group-hover:opacity-100 transition duration-500 blur"></div>
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
<Terminal size={16} />
</div>
<input
type="text"
placeholder={tr('search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
/>
<div className="pr-4">
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
</div>
</div>
</div>
{/* CTA */}
<a
href={VERGEX_EXPLORE_URL}
target="_blank"
rel="noopener noreferrer"
className="group mt-8 inline-flex items-center gap-2 rounded-lg bg-nofx-gold px-6 py-3 text-sm font-semibold text-black shadow-lg shadow-nofx-gold/20 transition-all hover:bg-yellow-400 active:scale-[0.98]"
>
{ctaLabel}
<ExternalLink
size={15}
className="transition-transform group-hover:translate-x-0.5"
/>
</a>
{/* Category Filter */}
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
{['all', 'popular', 'recent'].map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${
selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
>
{selectedCategory === cat && (
<motion.div
layoutId="filter-highlight"
className="absolute inset-0 bg-nofx-gold"
transition={{
type: 'spring',
bounce: 0.2,
duration: 0.6,
}}
/>
)}
<span className="relative z-10">{tr(cat)}</span>
</button>
{/* Stats row */}
<div className="mt-10 grid grid-cols-3 gap-6 border-t border-white/[0.05] pt-8">
{features.map((f) => (
<div key={f.label}>
<div className="font-mono text-xl font-bold text-white">
{f.value}
</div>
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-zinc-500">
{f.label}
</div>
</div>
))}
</div>
</div>
{/* Loading State */}
{isLoading && (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<div className="relative w-16 h-16">
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div className="absolute inset-0 border-2 border-nofx-gold rounded-full border-t-transparent animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<Cpu size={24} className="text-nofx-gold/50" />
</div>
</div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">
{tr('loading')}
</p>
<div className="flex gap-1">
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.4s' }}
></div>
</div>
</div>
)}
{/* Empty State */}
{!isLoading && filteredStrategies.length === 0 && (
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
<div className="relative mb-6">
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{tr('noStrategies')}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">
{tr('noStrategiesDesc')}
</p>
</div>
)}
{/* Strategy Grid */}
{!isLoading && filteredStrategies.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredStrategies.map((strategy, i) => {
const style = getStrategyStyle(strategy.name)
const Icon = style.icon
const indicators =
strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
return (
<motion.div
key={strategy.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: i * 0.05 }}
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
>
{/* Holographic Border Highlight */}
<div
className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
<div
className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
{/* Category Side Strip */}
<div
className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}
></div>
<div className="p-6 relative">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div
className={`p-2 rounded-none border ${style.border} ${style.bg}`}
>
<Icon className={`w-5 h-5 ${style.color}`} />
</div>
<div className="text-[10px] font-mono">
{strategy.config_visible ? (
<div className="flex items-center gap-1.5 text-emerald-500 border border-emerald-500/20 bg-emerald-500/10 px-2 py-1">
<Eye size={10} />
PUBLIC_ACCESS
</div>
) : (
<div className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
<EyeOff size={10} />
RESTRICTED
</div>
)}
</div>
</div>
{/* Name and Description */}
<h3
className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}
>
{strategy.name}
<span className="absolute -bottom-1 left-0 w-8 h-[2px] bg-zinc-800 group-hover:bg-nofx-gold transition-colors"></span>
</h3>
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
</p>
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col">
<span className="text-zinc-700 uppercase">
{tr('author')}
</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">
@
{strategy.author_email?.split('@')[0] ||
'UNKNOWN'}
</span>
</div>
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">
{tr('createdAt')}
</span>
<span className="text-zinc-400">
{formatDate(strategy.created_at)}
</span>
</div>
</div>
{/* Config / Indicators */}
<div className="bg-zinc-900/30 border border-zinc-800/50 p-3 mb-4 backdrop-blur-sm min-h-[90px]">
{strategy.config_visible && strategy.config ? (
<div className="space-y-3">
{/* Indicators */}
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide pb-1">
{indicators.length > 0 ? (
indicators.map((ind) => (
<span
key={ind}
className="px-1.5 py-0.5 border border-zinc-700 bg-zinc-800 text-[9px] text-zinc-300 font-mono whitespace-nowrap"
>
{ind}
</span>
))
) : (
<span className="text-[9px] text-zinc-600">
NO_INDICATORS
</span>
)}
</div>
{/* Risk Control */}
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control) && (
<div className="flex justify-between items-center text-[10px]">
<div className="flex gap-3">
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">
LEV
</span>
<span className="text-zinc-300 font-bold">
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control)
.btc_eth_max_leverage || '-'}
x
</span>
</div>
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">
POS
</span>
<span className="text-zinc-300 font-bold">
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control)
.max_positions || '-'}
</span>
</div>
</div>
<Activity
size={12}
className="text-zinc-700"
/>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">
{tr('configHiddenDesc')}
</span>
</div>
)}
</div>
{/* Action Button */}
<div>
{strategy.config_visible && strategy.config ? (
<button
onClick={() => handleCopyConfig(strategy)}
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-700 bg-black hover:bg-zinc-900 text-zinc-300 hover:text-nofx-gold hover:border-nofx-gold transition-all flex items-center justify-center gap-2 group/btn"
>
{copiedId === strategy.id ? (
<>
<Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">
{tr('copied')}
</span>
</>
) : (
<>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{tr('copyConfig')}
</>
)}
</button>
) : (
<button
disabled
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2"
>
<Shield size={12} />
{tr('hideConfig')}
</button>
)}
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</div>
)}
{/* CTA - Share Strategy */}
{user && token && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mt-16 mb-20 flex justify-center"
>
<div
className="relative group cursor-pointer"
onClick={() => navigate('/strategy')}
>
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold to-yellow-600 rounded blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon
className="text-nofx-gold animate-spin-slow"
size={24}
/>
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">
{tr('shareYours')}
</div>
<div className="text-[10px] text-zinc-500 font-mono">
CONTRIBUTE TO THE GLOBAL DATABASE
</div>
</div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
<div className="text-xs font-mono text-zinc-400 group-hover:translate-x-1 transition-transform">
INITIALIZE_UPLOAD -&gt;
</div>
</div>
</div>
</motion.div>
)}
</div>
{/* Footer hint */}
<p className="mt-4 text-center text-[11px] text-zinc-600">
vergex.trade · {VERGEX_EXPLORE_URL}
</p>
</div>
</DeepVoidBackground>
</div>
)
}