mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
feat: implement hybrid database architecture and frontend encryption
- Add PostgreSQL + SQLite hybrid database support with automatic switching - Implement frontend AES-GCM + RSA-OAEP encryption for sensitive data - Add comprehensive DatabaseInterface with all required methods - Fix compilation issues with interface consistency - Update all database method signatures to use DatabaseInterface - Add missing UpdateTraderInitialBalance method to PostgreSQL implementation - Integrate RSA public key distribution via /api/config endpoint - Add frontend crypto service with proper error handling - Support graceful degradation between encrypted and plaintext transmission - Add directory creation for RSA keys and PEM parsing fixes - Test both SQLite and PostgreSQL modes successfully 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -10,18 +10,18 @@ interface AboutSectionProps {
|
||||
|
||||
export default function AboutSection({ language }: AboutSectionProps) {
|
||||
return (
|
||||
<AnimatedSection id='about' backgroundColor='var(--brand-dark-gray)'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<div className='grid lg:grid-cols-2 gap-12 items-center'>
|
||||
<AnimatedSection id="about" backgroundColor="var(--brand-dark-gray)">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<motion.div
|
||||
className='space-y-6'
|
||||
className="space-y-6"
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<motion.div
|
||||
className='inline-flex items-center gap-2 px-4 py-2 rounded-full'
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
@@ -29,11 +29,11 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<Target
|
||||
className='w-4 h-4'
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
<span
|
||||
className='text-sm font-semibold'
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
{t('aboutNofx', language)}
|
||||
@@ -41,45 +41,49 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
</motion.div>
|
||||
|
||||
<h2
|
||||
className='text-4xl font-bold'
|
||||
className="text-4xl font-bold"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('whatIsNofx', language)}
|
||||
</h2>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
className="text-lg leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)} {t('nofxDescription2', language)}
|
||||
{t('nofxNotAnotherBot', language)}{' '}
|
||||
{t('nofxDescription1', language)}{' '}
|
||||
{t('nofxDescription2', language)}
|
||||
</p>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
className="text-lg leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('nofxDescription3', language)} {t('nofxDescription4', language)} {t('nofxDescription5', language)}
|
||||
{t('nofxDescription3', language)}{' '}
|
||||
{t('nofxDescription4', language)}{' '}
|
||||
{t('nofxDescription5', language)}
|
||||
</p>
|
||||
<motion.div
|
||||
className='flex items-center gap-3 pt-4'
|
||||
className="flex items-center gap-3 pt-4"
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
<div
|
||||
className='w-12 h-12 rounded-full flex items-center justify-center'
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
|
||||
>
|
||||
<Shield
|
||||
className='w-6 h-6'
|
||||
className="w-6 h-6"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className='font-semibold'
|
||||
className="font-semibold"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('youFullControl', language)}
|
||||
</div>
|
||||
<div
|
||||
className='text-sm'
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('fullControlDesc', language)}
|
||||
@@ -88,9 +92,9 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<div className='relative'>
|
||||
<div className="relative">
|
||||
<div
|
||||
className='rounded-2xl p-8'
|
||||
className="rounded-2xl p-8"
|
||||
style={{
|
||||
background: 'var(--brand-black)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
@@ -108,7 +112,7 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
]}
|
||||
typingSpeed={70}
|
||||
lineDelay={900}
|
||||
className='text-sm font-mono'
|
||||
className="text-sm font-mono"
|
||||
style={{
|
||||
color: '#00FF88',
|
||||
textShadow: '0 0 8px rgba(0,255,136,0.4)',
|
||||
@@ -121,4 +125,3 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function AnimatedSection({
|
||||
<motion.section
|
||||
id={id}
|
||||
ref={ref}
|
||||
className='py-20 px-4'
|
||||
className="py-20 px-4"
|
||||
style={{ background: backgroundColor }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
@@ -27,4 +27,3 @@ export default function AnimatedSection({
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,31 +2,40 @@ import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
|
||||
interface CardProps {
|
||||
quote: string;
|
||||
authorName: string;
|
||||
handle: string;
|
||||
avatarUrl: string;
|
||||
tweetUrl: string;
|
||||
delay: number;
|
||||
quote: string
|
||||
authorName: string
|
||||
handle: string
|
||||
avatarUrl: string
|
||||
tweetUrl: string
|
||||
delay: number
|
||||
}
|
||||
|
||||
function TestimonialCard({ quote, authorName, delay }: CardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className='p-6 rounded-xl'
|
||||
style={{ background: 'var(--brand-dark-gray)', border: '1px solid rgba(240, 185, 11, 0.1)' }}
|
||||
className="p-6 rounded-xl"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.1)',
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<p className='text-lg mb-4' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<p className="text-lg mb-4" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
"{quote}"
|
||||
</p>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='w-8 h-8 rounded-full' style={{ background: 'var(--binance-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--text-secondary)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full"
|
||||
style={{ background: 'var(--binance-yellow)' }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{authorName}
|
||||
</span>
|
||||
</div>
|
||||
@@ -35,7 +44,9 @@ function TestimonialCard({ quote, authorName, delay }: CardProps) {
|
||||
}
|
||||
|
||||
export default function CommunitySection() {
|
||||
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
|
||||
const staggerContainer = {
|
||||
animate: { transition: { staggerChildren: 0.1 } },
|
||||
}
|
||||
|
||||
// 推特内容整合(保持原三列布局,超出自动换行)
|
||||
const items: CardProps[] = [
|
||||
@@ -74,12 +85,12 @@ export default function CommunitySection() {
|
||||
|
||||
return (
|
||||
<AnimatedSection>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
className='grid md:grid-cols-3 gap-6'
|
||||
className="grid md:grid-cols-3 gap-6"
|
||||
variants={staggerContainer}
|
||||
initial='initial'
|
||||
whileInView='animate'
|
||||
initial="initial"
|
||||
whileInView="animate"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
|
||||
@@ -10,61 +10,78 @@ interface FeaturesSectionProps {
|
||||
|
||||
export default function FeaturesSection({ language }: FeaturesSectionProps) {
|
||||
return (
|
||||
<AnimatedSection id='features'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
|
||||
<AnimatedSection id="features">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<motion.div
|
||||
className='inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6'
|
||||
style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<Rocket className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
<Rocket
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
{t('coreFeatures', language)}
|
||||
</span>
|
||||
</motion.div>
|
||||
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<h2
|
||||
className="text-4xl font-bold mb-4"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('whyChooseNofx', language)}
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('openCommunityDriven', language)}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto'>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
||||
<CryptoFeatureCard
|
||||
icon={<Code className='w-8 h-8' />}
|
||||
icon={<Code className="w-8 h-8" />}
|
||||
title={t('openSourceSelfHosted', language)}
|
||||
description={t('openSourceDesc', language)}
|
||||
features={[
|
||||
t('openSourceFeatures1', language),
|
||||
t('openSourceFeatures2', language),
|
||||
t('openSourceFeatures3', language),
|
||||
t('openSourceFeatures4', language)
|
||||
t('openSourceFeatures4', language),
|
||||
]}
|
||||
delay={0}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Cpu className='w-8 h-8' />}
|
||||
icon={<Cpu className="w-8 h-8" />}
|
||||
title={t('multiAgentCompetition', language)}
|
||||
description={t('multiAgentDesc', language)}
|
||||
features={[
|
||||
t('multiAgentFeatures1', language),
|
||||
t('multiAgentFeatures2', language),
|
||||
t('multiAgentFeatures3', language),
|
||||
t('multiAgentFeatures4', language)
|
||||
t('multiAgentFeatures4', language),
|
||||
]}
|
||||
delay={0.1}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Lock className='w-8 h-8' />}
|
||||
icon={<Lock className="w-8 h-8" />}
|
||||
title={t('secureReliableTrading', language)}
|
||||
description={t('secureDesc', language)}
|
||||
features={[
|
||||
t('secureFeatures1', language),
|
||||
t('secureFeatures2', language),
|
||||
t('secureFeatures3', language),
|
||||
t('secureFeatures4', language)
|
||||
t('secureFeatures4', language),
|
||||
]}
|
||||
delay={0.2}
|
||||
/>
|
||||
@@ -73,4 +90,3 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,57 +6,62 @@ interface FooterSectionProps {
|
||||
|
||||
export default function FooterSection({ language }: FooterSectionProps) {
|
||||
return (
|
||||
<footer style={{ borderTop: '1px solid var(--panel-border)', background: 'var(--brand-dark-gray)' }}>
|
||||
<div className='max-w-[1200px] mx-auto px-6 py-10'>
|
||||
<footer
|
||||
style={{
|
||||
borderTop: '1px solid var(--panel-border)',
|
||||
background: 'var(--brand-dark-gray)',
|
||||
}}
|
||||
>
|
||||
<div className="max-w-[1200px] mx-auto px-6 py-10">
|
||||
{/* Brand */}
|
||||
<div className='flex items-center gap-3 mb-8'>
|
||||
<img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
|
||||
<div>
|
||||
<div className='text-lg font-bold' style={{ color: '#EAECEF' }}>
|
||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||
NOFX
|
||||
</div>
|
||||
<div className='text-xs' style={{ color: '#848E9C' }}>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('futureStandardAI', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multi-link columns */}
|
||||
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 gap-8'>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3
|
||||
className='text-sm font-semibold mb-3'
|
||||
className="text-sm font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('links', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://github.com/tinkle-community/nofx'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://t.me/nofx_dev_community'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://t.me/nofx_dev_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Telegram
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://x.com/nofx_ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://x.com/nofx_ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
X (Twitter)
|
||||
</a>
|
||||
@@ -66,38 +71,38 @@ export default function FooterSection({ language }: FooterSectionProps) {
|
||||
|
||||
<div>
|
||||
<h3
|
||||
className='text-sm font-semibold mb-3'
|
||||
className="text-sm font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('resources', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://github.com/tinkle-community/nofx/blob/main/README.md'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://github.com/tinkle-community/nofx/blob/main/README.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('documentation', language)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://github.com/tinkle-community/nofx/issues'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://github.com/tinkle-community/nofx/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Issues
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://github.com/tinkle-community/nofx/pulls'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://github.com/tinkle-community/nofx/pulls"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Pull Requests
|
||||
</a>
|
||||
@@ -107,50 +112,53 @@ export default function FooterSection({ language }: FooterSectionProps) {
|
||||
|
||||
<div>
|
||||
<h3
|
||||
className='text-sm font-semibold mb-3'
|
||||
className="text-sm font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('supporters', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://asterdex.com/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://asterdex.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Aster DEX
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://www.binance.com/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://www.binance.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Binance
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://hyperliquid.xyz/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://hyperliquid.xyz/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Hyperliquid
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://amber.ac/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="hover:text-[#F0B90B]"
|
||||
href="https://amber.ac/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Amber.ac <span className='opacity-70'>{t('strategicInvestment', language)}</span>
|
||||
Amber.ac{' '}
|
||||
<span className="opacity-70">
|
||||
{t('strategicInvestment', language)}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -159,11 +167,14 @@ export default function FooterSection({ language }: FooterSectionProps) {
|
||||
|
||||
{/* Bottom note (kept subtle) */}
|
||||
<div
|
||||
className='pt-6 mt-8 text-center text-xs'
|
||||
style={{ color: 'var(--text-tertiary)', borderTop: '1px solid var(--panel-border)' }}
|
||||
className="pt-6 mt-8 text-center text-xs"
|
||||
style={{
|
||||
color: 'var(--text-tertiary)',
|
||||
borderTop: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className='mt-1'>{t('footerWarning', language)}</p>
|
||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -16,7 +16,17 @@ interface HeaderBarProps {
|
||||
onPageChange?: (page: string) => void
|
||||
}
|
||||
|
||||
export default function HeaderBar({ isLoggedIn = false, isHomePage = false, currentPage, language = 'zh' as Language, onLanguageChange, user, onLogout, isAdminMode = false, onPageChange }: HeaderBarProps) {
|
||||
export default function HeaderBar({
|
||||
isLoggedIn = false,
|
||||
isHomePage = false,
|
||||
currentPage,
|
||||
language = 'zh' as Language,
|
||||
onLanguageChange,
|
||||
user,
|
||||
onLogout,
|
||||
isAdminMode = false,
|
||||
onPageChange,
|
||||
}: HeaderBarProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
|
||||
@@ -26,10 +36,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setLanguageDropdownOpen(false)
|
||||
}
|
||||
if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
userDropdownRef.current &&
|
||||
!userDropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setUserDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
@@ -41,231 +57,311 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className='fixed top-0 w-full z-50 header-bar'>
|
||||
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex items-center justify-between h-16'>
|
||||
<nav className="fixed top-0 w-full z-50 header-bar">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<a href='/' className='flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer'>
|
||||
<img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<span className='text-xl font-bold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
<a
|
||||
href="/"
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer"
|
||||
>
|
||||
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
|
||||
<span
|
||||
className="text-xl font-bold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
NOFX
|
||||
</span>
|
||||
<span className='text-sm hidden sm:block' style={{ color: 'var(--text-secondary)' }}>
|
||||
<span
|
||||
className="text-sm hidden sm:block"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
Agentic Trading OS
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className='hidden md:flex items-center justify-between flex-1 ml-8'>
|
||||
<div className="hidden md:flex items-center justify-between flex-1 ml-8">
|
||||
{/* Left Side - Navigation Tabs */}
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className="flex items-center gap-4">
|
||||
{isLoggedIn ? (
|
||||
// Main app navigation when logged in
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('实时 button clicked, onPageChange:', onPageChange);
|
||||
onPageChange?.('competition');
|
||||
console.log(
|
||||
'实时 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('competition')
|
||||
}}
|
||||
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('配置 button clicked, onPageChange:', onPageChange);
|
||||
onPageChange?.('traders');
|
||||
console.log(
|
||||
'配置 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('traders')
|
||||
}}
|
||||
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'traders'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'traders') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'traders') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'traders' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('configNav', language)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('看板 button clicked, onPageChange:', onPageChange);
|
||||
onPageChange?.('trader');
|
||||
console.log(
|
||||
'看板 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('trader')
|
||||
}}
|
||||
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'trader'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'trader') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'trader') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'trader' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('dashboardNav', language)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
// Landing page navigation when not logged in
|
||||
<a
|
||||
href='/competition'
|
||||
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
href="/competition"
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right Side - Original Navigation Items and Login */}
|
||||
<div className='flex items-center gap-6'>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Only show original navigation items on home page */}
|
||||
{isHomePage && [
|
||||
{ key: 'features', label: t('features', language) },
|
||||
{ key: 'howItWorks', label: t('howItWorks', language) },
|
||||
{ key: 'GitHub', label: 'GitHub' },
|
||||
{ key: 'community', label: t('community', language) }
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={
|
||||
item.key === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item.key === 'community'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
|
||||
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
|
||||
className='text-sm transition-colors relative group'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className='absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300'
|
||||
style={{ background: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
{isHomePage &&
|
||||
[
|
||||
{ key: 'features', label: t('features', language) },
|
||||
{ key: 'howItWorks', label: t('howItWorks', language) },
|
||||
{ key: 'GitHub', label: 'GitHub' },
|
||||
{ key: 'community', label: t('community', language) },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={
|
||||
item.key === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item.key === 'community'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={
|
||||
item.key === 'GitHub' || item.key === 'community'
|
||||
? '_blank'
|
||||
: undefined
|
||||
}
|
||||
rel={
|
||||
item.key === 'GitHub' || item.key === 'community'
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
}
|
||||
className="text-sm transition-colors relative group"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className="absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style={{ background: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* User Info and Actions */}
|
||||
{isLoggedIn && user ? (
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* User Info with Dropdown */}
|
||||
<div className='relative' ref={userDropdownRef}>
|
||||
<div className="relative" ref={userDropdownRef}>
|
||||
<button
|
||||
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
|
||||
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
|
||||
style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--panel-bg)'}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background =
|
||||
'rgba(255, 255, 255, 0.05)')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = 'var(--panel-bg)')
|
||||
}
|
||||
>
|
||||
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
>
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</span>
|
||||
<ChevronDown className='w-4 h-4' style={{ color: 'var(--brand-light-gray)' }} />
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{user.email}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
{userDropdownOpen && (
|
||||
<div className='absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
|
||||
<div className='px-3 py-2 border-b' style={{ borderColor: 'var(--panel-border)' }}>
|
||||
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
|
||||
<div className='text-sm font-medium' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="px-3 py-2 border-b"
|
||||
style={{ borderColor: 'var(--panel-border)' }}
|
||||
>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-medium"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
{!isAdminMode && onLogout && (
|
||||
<button
|
||||
@@ -273,10 +369,13 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
onLogout()
|
||||
setUserDropdownOpen(false)
|
||||
}}
|
||||
className='w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center'
|
||||
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
|
||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -285,43 +384,58 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
</div>
|
||||
) : (
|
||||
/* Show login/register buttons when not logged in and not on login/register pages */
|
||||
currentPage !== 'login' && currentPage !== 'register' && (
|
||||
<div className='flex items-center gap-3'>
|
||||
currentPage !== 'login' &&
|
||||
currentPage !== 'register' && (
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href='/login'
|
||||
className='px-3 py-2 text-sm font-medium transition-colors rounded'
|
||||
href="/login"
|
||||
className="px-3 py-2 text-sm font-medium transition-colors rounded"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
{t('signIn', language)}
|
||||
</a>
|
||||
<a
|
||||
href='/register'
|
||||
className='px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
href="/register"
|
||||
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
|
||||
{/* Language Toggle - Always at the rightmost */}
|
||||
<div className='relative' ref={dropdownRef}>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background =
|
||||
'rgba(255, 255, 255, 0.05)')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = 'transparent')
|
||||
}
|
||||
>
|
||||
<span className='text-lg'>
|
||||
<span className="text-lg">
|
||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
||||
</span>
|
||||
<ChevronDown className='w-4 h-4' />
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
|
||||
{languageDropdownOpen && (
|
||||
<div className='absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('zh')
|
||||
@@ -330,13 +444,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'zh' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background: language === 'zh' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
|
||||
background:
|
||||
language === 'zh'
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<span className='text-base'>🇨🇳</span>
|
||||
<span className='text-sm'>中文</span>
|
||||
<span className="text-base">🇨🇳</span>
|
||||
<span className="text-sm">中文</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -346,13 +463,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'en' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background: language === 'en' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
|
||||
background:
|
||||
language === 'en'
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<span className='text-base'>🇺🇸</span>
|
||||
<span className='text-sm'>English</span>
|
||||
<span className="text-base">🇺🇸</span>
|
||||
<span className="text-sm">English</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -363,11 +483,15 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
{/* Mobile Menu Button */}
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className='md:hidden'
|
||||
className="md:hidden"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{mobileMenuOpen ? <X className='w-6 h-6' /> : <Menu className='w-6 h-6' />}
|
||||
{mobileMenuOpen ? (
|
||||
<X className="w-6 h-6" />
|
||||
) : (
|
||||
<Menu className="w-6 h-6" />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,65 +499,81 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
{/* Mobile Menu */}
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={mobileMenuOpen ? { height: 'auto', opacity: 1 } : { height: 0, opacity: 0 }}
|
||||
animate={
|
||||
mobileMenuOpen
|
||||
? { height: 'auto', opacity: 1 }
|
||||
: { height: 0, opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3 }}
|
||||
className='md:hidden overflow-hidden'
|
||||
style={{ background: 'var(--brand-dark-gray)', borderTop: '1px solid rgba(240, 185, 11, 0.1)' }}
|
||||
className="md:hidden overflow-hidden"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div className='px-4 py-4 space-y-3'>
|
||||
<div className="px-4 py-4 space-y-3">
|
||||
{/* New Navigation Tabs */}
|
||||
{isLoggedIn ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('移动端 实时 button clicked, onPageChange:', onPageChange);
|
||||
console.log(
|
||||
'移动端 实时 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('competition')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href='/competition'
|
||||
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
|
||||
<a
|
||||
href="/competition"
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
)}
|
||||
@@ -442,107 +582,135 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('移动端 配置 button clicked, onPageChange:', onPageChange);
|
||||
console.log(
|
||||
'移动端 配置 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('traders')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'traders'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'traders' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('configNav', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('移动端 看板 button clicked, onPageChange:', onPageChange);
|
||||
console.log(
|
||||
'移动端 看板 button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('trader')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
|
||||
style={{
|
||||
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
color:
|
||||
currentPage === 'trader'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'trader' && (
|
||||
<span
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{t('dashboardNav', language)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Original Navigation Items - Only on home page */}
|
||||
{isHomePage && [
|
||||
{ key: 'features', label: t('features', language) },
|
||||
{ key: 'howItWorks', label: t('howItWorks', language) },
|
||||
{ key: 'GitHub', label: 'GitHub' },
|
||||
{ key: 'community', label: t('community', language) }
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={
|
||||
item.key === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item.key === 'community'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
|
||||
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
|
||||
className='block text-sm py-2'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{isHomePage &&
|
||||
[
|
||||
{ key: 'features', label: t('features', language) },
|
||||
{ key: 'howItWorks', label: t('howItWorks', language) },
|
||||
{ key: 'GitHub', label: 'GitHub' },
|
||||
{ key: 'community', label: t('community', language) },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={
|
||||
item.key === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item.key === 'community'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={
|
||||
item.key === 'GitHub' || item.key === 'community'
|
||||
? '_blank'
|
||||
: undefined
|
||||
}
|
||||
rel={
|
||||
item.key === 'GitHub' || item.key === 'community'
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
}
|
||||
className="block text-sm py-2"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className='py-2'>
|
||||
<div className='flex items-center gap-2 mb-2'>
|
||||
<span className='text-xs' style={{ color: 'var(--brand-light-gray)' }}>{t('language', language)}:</span>
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('language', language)}:
|
||||
</span>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('zh')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
||||
language === 'zh' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
|
||||
language === 'zh'
|
||||
? 'bg-yellow-500 text-black'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className='text-lg'>🇨🇳</span>
|
||||
<span className='text-sm'>中文</span>
|
||||
<span className="text-lg">🇨🇳</span>
|
||||
<span className="text-sm">中文</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -550,25 +718,49 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
||||
language === 'en' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
|
||||
language === 'en'
|
||||
? 'bg-yellow-500 text-black'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className='text-lg'>🇺🇸</span>
|
||||
<span className='text-sm'>English</span>
|
||||
<span className="text-lg">🇺🇸</span>
|
||||
<span className="text-sm">English</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User info and logout for mobile when logged in */}
|
||||
{isLoggedIn && user && (
|
||||
<div className='mt-4 pt-4' style={{ borderTop: '1px solid var(--panel-border)' }}>
|
||||
<div className='flex items-center gap-2 px-3 py-2 mb-2 rounded' style={{ background: 'var(--panel-bg)' }}>
|
||||
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
|
||||
<div
|
||||
className="mt-4 pt-4"
|
||||
style={{ borderTop: '1px solid var(--panel-border)' }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 mb-2 rounded"
|
||||
style={{ background: 'var(--panel-bg)' }}
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
>
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
|
||||
<div className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isAdminMode && onLogout && (
|
||||
@@ -577,8 +769,11 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
onLogout()
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center'
|
||||
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
@@ -587,29 +782,36 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
|
||||
)}
|
||||
|
||||
{/* Show login/register buttons when not logged in and not on login/register pages */}
|
||||
{!isLoggedIn && currentPage !== 'login' && currentPage !== 'register' && (
|
||||
<div className='space-y-2 mt-2'>
|
||||
<a
|
||||
href='/login'
|
||||
className='block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors'
|
||||
style={{ color: 'var(--brand-light-gray)', border: '1px solid var(--brand-light-gray)' }}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
</a>
|
||||
<a
|
||||
href='/register'
|
||||
className='block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!isLoggedIn &&
|
||||
currentPage !== 'login' &&
|
||||
currentPage !== 'register' && (
|
||||
<div className="space-y-2 mt-2">
|
||||
<a
|
||||
href="/login"
|
||||
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors"
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
border: '1px solid var(--brand-light-gray)',
|
||||
}}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
</a>
|
||||
<a
|
||||
href="/register"
|
||||
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { motion, useScroll, useTransform, useAnimation } from 'framer-motion'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
import { useGitHubStats } from '../../hooks/useGitHubStats'
|
||||
import { useCounterAnimation } from '../../hooks/useCounterAnimation'
|
||||
|
||||
interface HeroSectionProps {
|
||||
language: Language
|
||||
@@ -11,75 +13,151 @@ export default function HeroSection({ language }: HeroSectionProps) {
|
||||
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
|
||||
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
|
||||
const handControls = useAnimation()
|
||||
const { stars, daysOld, isLoading } = useGitHubStats('NoFxAiOS', 'nofx')
|
||||
|
||||
// 动画数字 - 仅对 stars 添加动画
|
||||
const animatedStars = useCounterAnimation({
|
||||
start: 0,
|
||||
end: stars,
|
||||
duration: 2000,
|
||||
})
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 60 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.6, ease: [0.6, -0.05, 0.01, 0.99] },
|
||||
}
|
||||
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
|
||||
const staggerContainer = {
|
||||
animate: { transition: { staggerChildren: 0.1 } },
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='relative pt-32 pb-20 px-4'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<div className='grid lg:grid-cols-2 gap-12 items-center'>
|
||||
<section className="relative pt-32 pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Left Content */}
|
||||
<motion.div className='space-y-6 relative z-10' style={{ opacity, scale }} initial='initial' animate='animate' variants={staggerContainer}>
|
||||
<motion.div
|
||||
className="space-y-6 relative z-10"
|
||||
style={{ opacity, scale }}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
variants={staggerContainer}
|
||||
>
|
||||
<motion.div variants={fadeInUp}>
|
||||
<motion.div
|
||||
className='inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6'
|
||||
style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
whileHover={{ scale: 1.05, boxShadow: '0 0 20px rgba(240, 185, 11, 0.2)' }}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
boxShadow: '0 0 20px rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
{t('githubStarsInDays', language)}
|
||||
<Sparkles
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
{isLoading ? (
|
||||
t('githubStarsInDays', language)
|
||||
) : language === 'zh' ? (
|
||||
<>
|
||||
{daysOld} 天内{' '}
|
||||
<span className="inline-block tabular-nums">
|
||||
{(animatedStars / 1000).toFixed(1)}
|
||||
</span>
|
||||
K+ GitHub Stars
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="inline-block tabular-nums">
|
||||
{(animatedStars / 1000).toFixed(1)}
|
||||
</span>
|
||||
K+ GitHub Stars in {daysOld} days
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<h1
|
||||
className="text-5xl lg:text-7xl font-bold leading-tight"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('heroTitle1', language)}
|
||||
<br />
|
||||
<span style={{ color: 'var(--brand-yellow)' }}>{t('heroTitle2', language)}</span>
|
||||
<span style={{ color: 'var(--brand-yellow)' }}>
|
||||
{t('heroTitle2', language)}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<motion.p className='text-xl leading-relaxed' style={{ color: 'var(--text-secondary)' }} variants={fadeInUp}>
|
||||
<motion.p
|
||||
className="text-xl leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
{t('heroDescription', language)}
|
||||
</motion.p>
|
||||
|
||||
<div className='flex items-center gap-3 flex-wrap'>
|
||||
<motion.a href='https://github.com/tinkle-community/nofx' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<motion.a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 400 }}
|
||||
>
|
||||
<img
|
||||
src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
|
||||
alt='GitHub Stars'
|
||||
className='h-7'
|
||||
src="https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
|
||||
alt="GitHub Stars"
|
||||
className="h-7"
|
||||
/>
|
||||
</motion.a>
|
||||
<motion.a href='https://github.com/tinkle-community/nofx/network/members' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
|
||||
<motion.a
|
||||
href="https://github.com/tinkle-community/nofx/network/members"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 400 }}
|
||||
>
|
||||
<img
|
||||
src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
|
||||
alt='GitHub Forks'
|
||||
className='h-7'
|
||||
src="https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
|
||||
alt="GitHub Forks"
|
||||
className="h-7"
|
||||
/>
|
||||
</motion.a>
|
||||
<motion.a href='https://github.com/tinkle-community/nofx/graphs/contributors' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
|
||||
<motion.a
|
||||
href="https://github.com/tinkle-community/nofx/graphs/contributors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 400 }}
|
||||
>
|
||||
<img
|
||||
src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
|
||||
alt='GitHub Contributors'
|
||||
className='h-7'
|
||||
src="https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
|
||||
alt="GitHub Contributors"
|
||||
className="h-7"
|
||||
/>
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
<motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}>
|
||||
{t('poweredBy', language)}
|
||||
<motion.p
|
||||
className="text-xs pt-4"
|
||||
style={{ color: 'var(--text-tertiary)' }}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
{t('poweredBy', language)}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Visual - Interactive Robot */}
|
||||
<div
|
||||
className='relative w-full cursor-pointer'
|
||||
<div
|
||||
className="relative w-full cursor-pointer"
|
||||
onMouseEnter={() => {
|
||||
handControls.start({
|
||||
y: [-8, 8, -8],
|
||||
@@ -88,9 +166,9 @@ export default function HeroSection({ language }: HeroSectionProps) {
|
||||
transition: {
|
||||
duration: 2.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.5, 1]
|
||||
}
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.5, 1],
|
||||
},
|
||||
})
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
@@ -100,32 +178,32 @@ export default function HeroSection({ language }: HeroSectionProps) {
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: "easeOut"
|
||||
}
|
||||
ease: 'easeOut',
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{/* Background Layer */}
|
||||
<motion.img
|
||||
src='/images/hand-bg.png'
|
||||
alt='NOFX Platform Background'
|
||||
className='w-full opacity-90'
|
||||
<motion.img
|
||||
src="/images/hand-bg.png"
|
||||
alt="NOFX Platform Background"
|
||||
className="w-full opacity-90"
|
||||
style={{ opacity, scale }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
/>
|
||||
|
||||
|
||||
{/* Hand Layer - Animated */}
|
||||
<motion.img
|
||||
src='/images/hand.png'
|
||||
alt='Robot Hand'
|
||||
className='absolute top-0 left-0 w-full'
|
||||
<motion.img
|
||||
src="/images/hand.png"
|
||||
alt="Robot Hand"
|
||||
className="absolute top-0 left-0 w-full"
|
||||
style={{ opacity }}
|
||||
animate={handControls}
|
||||
initial={{ y: 0, rotate: 0, x: 0 }}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
transition: { type: 'spring', stiffness: 400 }
|
||||
transition: { type: 'spring', stiffness: 400 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -134,4 +212,3 @@ export default function HeroSection({ language }: HeroSectionProps) {
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,36 @@ import { t, Language } from '../../i18n/translations'
|
||||
|
||||
function StepCard({ number, title, description, delay }: any) {
|
||||
return (
|
||||
<motion.div className='flex gap-6 items-start' initial={{ opacity: 0, x: -50 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ delay }} whileHover={{ x: 10 }}>
|
||||
<motion.div
|
||||
className="flex gap-6 items-start"
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<motion.div
|
||||
className='flex-shrink-0 w-14 h-14 rounded-full flex items-center justify-center font-bold text-2xl'
|
||||
style={{ background: 'var(--binance-yellow)', color: 'var(--brand-black)' }}
|
||||
className="flex-shrink-0 w-14 h-14 rounded-full flex items-center justify-center font-bold text-2xl"
|
||||
style={{
|
||||
background: 'var(--binance-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
whileHover={{ scale: 1.2, rotate: 360 }}
|
||||
transition={{ type: 'spring', stiffness: 260, damping: 20 }}
|
||||
>
|
||||
{number}
|
||||
</motion.div>
|
||||
<div>
|
||||
<h3 className='text-2xl font-semibold mb-2' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<h3
|
||||
className="text-2xl font-semibold mb-2"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='text-lg leading-relaxed' style={{ color: 'var(--text-secondary)' }}>
|
||||
<p
|
||||
className="text-lg leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -29,46 +45,91 @@ interface HowItWorksSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function HowItWorksSection({ language }: HowItWorksSectionProps) {
|
||||
export default function HowItWorksSection({
|
||||
language,
|
||||
}: HowItWorksSectionProps) {
|
||||
return (
|
||||
<AnimatedSection id='how-it-works' backgroundColor='var(--brand-dark-gray)'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
|
||||
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<AnimatedSection id="how-it-works" backgroundColor="var(--brand-dark-gray)">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2
|
||||
className="text-4xl font-bold mb-4"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('howToStart', language)}
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('fourSimpleSteps', language)}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className='space-y-8'>
|
||||
<div className="space-y-8">
|
||||
{[
|
||||
{ number: 1, title: t('step1Title', language), description: t('step1Desc', language) },
|
||||
{ number: 2, title: t('step2Title', language), description: t('step2Desc', language) },
|
||||
{ number: 3, title: t('step3Title', language), description: t('step3Desc', language) },
|
||||
{ number: 4, title: t('step4Title', language), description: t('step4Desc', language) },
|
||||
{
|
||||
number: 1,
|
||||
title: t('step1Title', language),
|
||||
description: t('step1Desc', language),
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: t('step2Title', language),
|
||||
description: t('step2Desc', language),
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: t('step3Title', language),
|
||||
description: t('step3Desc', language),
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: t('step4Title', language),
|
||||
description: t('step4Desc', language),
|
||||
},
|
||||
].map((step, index) => (
|
||||
<StepCard key={step.number} {...step} delay={index * 0.1} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className='mt-12 p-6 rounded-xl flex items-start gap-4'
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.3)' }}
|
||||
className="mt-12 p-6 rounded-xl flex items-start gap-4"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<div className='w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0' style={{ background: 'rgba(246, 70, 93, 0.2)', color: '#F6465D' }}>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' className='w-6 h-6' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'><path d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z'/><line x1='12' x2='12' y1='9' y2='13'/><line x1='12' x2='12.01' y1='17' y2='17'/></svg>
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.2)', color: '#F6465D' }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
|
||||
<line x1="12" x2="12" y1="9" y2="13" />
|
||||
<line x1="12" x2="12.01" y1="17" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className='font-semibold mb-2' style={{ color: '#F6465D' }}>
|
||||
<div className="font-semibold mb-2" style={{ color: '#F6465D' }}>
|
||||
{t('importantRiskWarning', language)}
|
||||
</div>
|
||||
<p className='text-sm' style={{ color: 'var(--text-secondary)' }}>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('riskWarningText', language)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ interface LoginModalProps {
|
||||
export default function LoginModal({ onClose, language }: LoginModalProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className='fixed inset-0 z-50 flex items-center justify-center p-4'
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ background: 'rgba(0, 0, 0, 0.8)' }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -18,32 +18,50 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
className='relative max-w-md w-full rounded-2xl p-8'
|
||||
style={{ background: 'var(--brand-dark-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
className="relative max-w-md w-full rounded-2xl p-8"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
initial={{ scale: 0.9, y: 50 }}
|
||||
animate={{ scale: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, y: 50 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<motion.button onClick={onClose} className='absolute top-4 right-4' style={{ color: 'var(--text-secondary)' }} whileHover={{ scale: 1.1, rotate: 90 }} whileTap={{ scale: 0.9 }}>
|
||||
<X className='w-6 h-6' />
|
||||
<motion.button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</motion.button>
|
||||
<h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
<h2
|
||||
className="text-2xl font-bold mb-6"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{t('accessNofxPlatform', language)}
|
||||
</h2>
|
||||
<p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}>
|
||||
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('loginRegisterPrompt', language)}
|
||||
</p>
|
||||
<div className='space-y-3'>
|
||||
<div className="space-y-3">
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/login')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
onClose()
|
||||
}}
|
||||
className='block w-full px-6 py-3 rounded-lg font-semibold text-center'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }}
|
||||
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
@@ -54,8 +72,12 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
onClose()
|
||||
}}
|
||||
className='block w-full px-6 py-3 rounded-lg font-semibold text-center'
|
||||
style={{ background: 'var(--brand-dark-gray)', color: 'var(--brand-light-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
color: 'var(--brand-light-gray)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
@@ -66,4 +88,3 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user