mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 13:54:41 +08:00
Merge pull request #215 from zhoujunhehe/landing-page-pr
This commit is contained in:
107
web/package-lock.json
generated
107
web/package-lock.json
generated
@@ -8,13 +8,17 @@
|
||||
"name": "nofx-web",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.552.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.2",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -834,6 +838,39 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -1504,6 +1541,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1905,6 +1954,33 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.24",
|
||||
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.23.24.tgz",
|
||||
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2212,6 +2288,21 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.23.6",
|
||||
"resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2970,6 +3061,16 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
|
||||
@@ -3096,6 +3197,12 @@
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.552.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.2",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
BIN
web/public/images/main.png
Normal file
BIN
web/public/images/main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
@@ -6,11 +6,13 @@ import { AITradersPage } from './components/AITradersPage';
|
||||
import { LoginPage } from './components/LoginPage';
|
||||
import { RegisterPage } from './components/RegisterPage';
|
||||
import { CompetitionPage } from './components/CompetitionPage';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import AILearning from './components/AILearning';
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { t, type Language } from './i18n/translations';
|
||||
import { useSystemConfig } from './hooks/useSystemConfig';
|
||||
import { Zap } from 'lucide-react';
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -169,22 +171,23 @@ function App() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center text-3xl animate-spin"
|
||||
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
⚡
|
||||
</div>
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
||||
<p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not in admin mode and not authenticated, show login/register pages
|
||||
// Show landing page for root route when not authenticated
|
||||
if (!systemConfig?.admin_mode && (!user || !token)) {
|
||||
if (route === '/login') {
|
||||
return <LoginPage />;
|
||||
}
|
||||
if (route === '/register') {
|
||||
return <RegisterPage />;
|
||||
}
|
||||
return <LoginPage />;
|
||||
// Default to landing page when not authenticated
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -258,7 +261,8 @@ function App() {
|
||||
{/* Admin Mode Indicator */}
|
||||
{systemConfig?.admin_mode && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>⚡ {t('adminMode', language)}</span>
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { t, type Language } from '../i18n/translations';
|
||||
import { getExchangeIcon } from './ExchangeIcons';
|
||||
import { getModelIcon } from './ModelIcons';
|
||||
import { TraderConfigModal } from './TraderConfigModal';
|
||||
import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users } from 'lucide-react';
|
||||
import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, AlertTriangle } from 'lucide-react';
|
||||
|
||||
// 获取友好的AI模型名称
|
||||
function getModelDisplayName(modelId: string): string {
|
||||
@@ -1427,7 +1427,7 @@ function ExchangeConfigModal({
|
||||
|
||||
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
|
||||
⚠️ {t('securityWarning', language)}
|
||||
<span className="inline-flex items-center gap-1"><AlertTriangle className="w-4 h-4" /> {t('securityWarning', language)}</span>
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>{t('exchangeConfigWarning1', language)}</div>
|
||||
@@ -1467,4 +1467,4 @@ function ExchangeConfigModal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { CompetitionTraderData } from '../types';
|
||||
import { getTraderColor } from '../utils/traderColors';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
|
||||
interface ComparisonChartProps {
|
||||
traders: CompetitionTraderData[];
|
||||
@@ -136,7 +137,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
if (combinedData.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||
<BarChart3 className="w-12 h-12 mx-auto mb-4 opacity-60" />
|
||||
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
|
||||
<div className="text-sm">{t('dataWillAppear', language)}</div>
|
||||
</div>
|
||||
@@ -338,4 +339,4 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Trophy, Medal } from 'lucide-react';
|
||||
import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import type { CompetitionData } from '../types';
|
||||
@@ -74,11 +75,8 @@ export function CompetitionPage() {
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||
}}>
|
||||
🏆
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.15)', border: '1px solid rgba(240,185,11,0.3)' }}>
|
||||
<Trophy className="w-6 h-6" style={{ color: '#F0B90B' }} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
@@ -145,8 +143,8 @@ export function CompetitionPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Rank & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-2xl w-6">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'}
|
||||
<div className="w-6 flex items-center justify-center">
|
||||
<Medal className="w-5 h-5" style={{ color: index === 0 ? '#F0B90B' : index === 1 ? '#C0C0C0' : '#CD7F32' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
||||
@@ -281,4 +279,4 @@ export function CompetitionPage() {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
115
web/src/components/CryptoFeatureCard.tsx
Normal file
115
web/src/components/CryptoFeatureCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface CryptoFeatureCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureCardProps>(
|
||||
({ icon, title, description, features, className, delay = 0 }, ref) => {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
className="relative h-full"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl",
|
||||
"bg-gradient-to-br from-[#0C0E12] to-[#1E2329]",
|
||||
"border-[#2B3139] hover:border-[#F0B90B]/50",
|
||||
isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Animated glow border effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 opacity-0 pointer-events-none"
|
||||
animate={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[#F0B90B]/20 to-transparent animate-[shimmer_2s_infinite]" />
|
||||
</motion.div>
|
||||
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 2px 2px, #F0B90B 1px, transparent 0)`,
|
||||
backgroundSize: "32px 32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 p-8 flex flex-col h-full">
|
||||
{/* Icon container */}
|
||||
<motion.div
|
||||
className={cn(
|
||||
"mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl",
|
||||
"bg-gradient-to-br from-[#F0B90B]/20 to-[#F0B90B]/5",
|
||||
"border border-[#F0B90B]/30"
|
||||
)}
|
||||
animate={{
|
||||
scale: isHovered ? 1.1 : 1,
|
||||
boxShadow: isHovered
|
||||
? "0 0 20px rgba(240, 185, 11, 0.4)"
|
||||
: "0 0 0px rgba(240, 185, 11, 0)",
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="text-[#F0B90B]">{icon}</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-2xl font-bold text-[#EAECEF] mb-3">{title}</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-[#848E9C] mb-6 flex-grow leading-relaxed">{description}</p>
|
||||
|
||||
{/* Features list */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + index * 0.1 }}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<div className="w-5 h-5 rounded-full bg-[#F0B90B]/20 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-[#F0B90B]" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-[#EAECEF]">{feature}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CryptoFeatureCard.displayName = "CryptoFeatureCard";
|
||||
@@ -15,7 +15,7 @@ export function Header({ simple = false }: HeaderProps) {
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center">
|
||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="h-10 w-auto" />
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
@@ -56,4 +56,4 @@ export function Header({ simple = false }: HeaderProps) {
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { Header } from './Header';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -52,13 +53,26 @@ export function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
|
||||
<Header simple />
|
||||
|
||||
|
||||
<div className="flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Back to Home */}
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回首页
|
||||
</button>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('loginTitle', language)}
|
||||
@@ -191,4 +205,4 @@ export function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export function RegisterPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -73,10 +74,25 @@ export function RegisterPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Back to Home */}
|
||||
{step === 'register' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回首页
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
@@ -307,4 +323,4 @@ export function RegisterPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ export function TraderConfigModal({
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label className="text-sm text-[#EAECEF]">覆盖默认提示词</label>
|
||||
<span className="text-xs text-[#F0B90B]">⚠️ 启用后将完全替换默认策略</span>
|
||||
<span className="text-xs text-[#F0B90B] inline-flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" 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> 启用后将完全替换默认策略</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
@@ -491,4 +491,4 @@ export function TraderConfigModal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
73
web/src/components/Typewriter.tsx
Normal file
73
web/src/components/Typewriter.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
interface TypewriterProps {
|
||||
lines: string[]
|
||||
typingSpeed?: number // 毫秒/字符
|
||||
lineDelay?: number // 每行结束的额外等待
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export default function Typewriter({
|
||||
lines,
|
||||
typingSpeed = 50,
|
||||
lineDelay = 600,
|
||||
className,
|
||||
style,
|
||||
}: TypewriterProps) {
|
||||
const [typedLines, setTypedLines] = useState<string[]>([''])
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const lineIndexRef = useRef(0)
|
||||
const charIndexRef = useRef(0)
|
||||
const timerRef = useRef<number | null>(null)
|
||||
const blinkRef = useRef<number | null>(null)
|
||||
const sanitizedLines = useMemo(() => lines.map((l) => String(l ?? '')), [lines])
|
||||
|
||||
useEffect(() => {
|
||||
function typeNext() {
|
||||
const currentLine = sanitizedLines[lineIndexRef.current] ?? ''
|
||||
if (charIndexRef.current < currentLine.length) {
|
||||
setTypedLines((prev) => {
|
||||
const next = [...prev]
|
||||
const ch = currentLine.charAt(charIndexRef.current)
|
||||
next[next.length - 1] = (next[next.length - 1] || '') + ch
|
||||
return next
|
||||
})
|
||||
charIndexRef.current += 1
|
||||
timerRef.current = window.setTimeout(typeNext, typingSpeed)
|
||||
} else {
|
||||
// 行结束
|
||||
if (lineIndexRef.current < sanitizedLines.length - 1) {
|
||||
lineIndexRef.current += 1
|
||||
charIndexRef.current = 0
|
||||
setTypedLines((prev) => [...prev, ''])
|
||||
timerRef.current = window.setTimeout(typeNext, lineDelay)
|
||||
} else {
|
||||
// 最后一行输入完毕
|
||||
timerRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeNext()
|
||||
|
||||
// 光标闪烁
|
||||
blinkRef.current = window.setInterval(() => {
|
||||
setShowCursor((v) => !v)
|
||||
}, 500)
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) window.clearTimeout(timerRef.current)
|
||||
if (blinkRef.current) window.clearInterval(blinkRef.current)
|
||||
}
|
||||
}, [lines, typingSpeed, lineDelay])
|
||||
|
||||
const displayText = useMemo(() => typedLines.join('\n').replace(/undefined/g, ''), [typedLines])
|
||||
|
||||
return (
|
||||
<pre className={className} style={{ whiteSpace: 'pre-wrap', ...style }}>
|
||||
{displayText}
|
||||
<span style={{ opacity: showCursor ? 1 : 0 }}> ▍</span>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
123
web/src/components/landing/AboutSection.tsx
Normal file
123
web/src/components/landing/AboutSection.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Shield, Target } from 'lucide-react'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
import Typewriter from '../Typewriter'
|
||||
|
||||
export default function AboutSection() {
|
||||
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'>
|
||||
<motion.div
|
||||
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'
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<Target
|
||||
className='w-4 h-4'
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
<span
|
||||
className='text-sm font-semibold'
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
关于 NOFX
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<h2
|
||||
className='text-4xl font-bold'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
什么是 NOFX?
|
||||
</h2>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
NOFX 不是另一个交易机器人,而是 AI 交易的 'Linux' ——
|
||||
一个透明、可信任的开源 OS,提供统一的 '决策-风险-执行'
|
||||
层,支持所有资产类别。
|
||||
</p>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
从加密市场起步(24/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI
|
||||
达尔文主义(多代理自竞争、策略进化)、CodeFi 飞轮(开发者 PR
|
||||
贡献获积分奖励)。
|
||||
</p>
|
||||
<motion.div
|
||||
className='flex items-center gap-3 pt-4'
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
<div
|
||||
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'
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className='font-semibold'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
你 100% 掌控
|
||||
</div>
|
||||
<div
|
||||
className='text-sm'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
完全掌控 AI 提示词和资金
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<div className='relative'>
|
||||
<div
|
||||
className='rounded-2xl p-8'
|
||||
style={{
|
||||
background: 'var(--brand-black)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<Typewriter
|
||||
lines={[
|
||||
'$ git clone https://github.com/tinkle-community/nofx.git',
|
||||
'$ cd nofx',
|
||||
'$ chmod +x start.sh',
|
||||
'$ ./start.sh start --build',
|
||||
' 启动自动交易系统...',
|
||||
' API服务器启动在端口 8080',
|
||||
' Web 控制台 http://localhost:3000',
|
||||
]}
|
||||
typingSpeed={70}
|
||||
lineDelay={900}
|
||||
className='text-sm font-mono'
|
||||
style={{
|
||||
color: '#00FF41',
|
||||
textShadow: '0 0 6px rgba(0,255,65,0.6)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
|
||||
30
web/src/components/landing/AnimatedSection.tsx
Normal file
30
web/src/components/landing/AnimatedSection.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useRef } from 'react'
|
||||
import { motion, useInView } from 'framer-motion'
|
||||
|
||||
export default function AnimatedSection({
|
||||
children,
|
||||
id,
|
||||
backgroundColor = 'var(--brand-black)',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
id?: string
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
id={id}
|
||||
ref={ref}
|
||||
className='py-20 px-4'
|
||||
style={{ background: backgroundColor }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{children}
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
|
||||
42
web/src/components/landing/CommunitySection.tsx
Normal file
42
web/src/components/landing/CommunitySection.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
|
||||
function TestimonialCard({ quote, author, delay }: any) {
|
||||
return (
|
||||
<motion.div
|
||||
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)' }}>
|
||||
"{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)' }}>
|
||||
{author}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CommunitySection() {
|
||||
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
|
||||
return (
|
||||
<AnimatedSection>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
<motion.div className='grid md:grid-cols-3 gap-6' variants={staggerContainer} initial='initial' whileInView='animate' viewport={{ once: true }}>
|
||||
<TestimonialCard quote='跑了一晚上 NOFX,开源的 AI 自动交易,太有意思了,一晚上赚了 6% 收益!' author='@DIYgod' delay={0} />
|
||||
<TestimonialCard quote='所有成功人士都在用 NOFX。IYKYK。' author='@SexyMichill' delay={0.1} />
|
||||
<TestimonialCard quote='NOFX 复兴了传奇 Alpha Arena,AI 驱动的加密期货战场。' author='@hqmank' delay={0.2} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
|
||||
56
web/src/components/landing/FeaturesSection.tsx
Normal file
56
web/src/components/landing/FeaturesSection.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
import { CryptoFeatureCard } from '../CryptoFeatureCard'
|
||||
import { Code, Cpu, Lock, Rocket } from 'lucide-react'
|
||||
|
||||
export default function FeaturesSection() {
|
||||
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 }}>
|
||||
<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 }}
|
||||
>
|
||||
<Rocket className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
核心功能
|
||||
</span>
|
||||
</motion.div>
|
||||
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
为什么选择 NOFX?
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
开源、透明、社区驱动的 AI 交易操作系统
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<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' />}
|
||||
title='100% 开源与自托管'
|
||||
description='你的框架,你的规则。非黑箱,支持自定义提示词和多模型。'
|
||||
features={['完全开源代码', '支持自托管部署', '自定义 AI 提示词', '多模型支持(DeepSeek、Qwen)']}
|
||||
delay={0}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Cpu className='w-8 h-8' />}
|
||||
title='多代理智能竞争'
|
||||
description='AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。'
|
||||
features={['多 AI 代理并行运行', '策略自动优化', '沙盒安全测试', '跨市场策略移植']}
|
||||
delay={0.1}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Lock className='w-8 h-8' />}
|
||||
title='安全可靠交易'
|
||||
description='企业级安全保障,完全掌控你的资金和交易策略。'
|
||||
features={['本地私钥管理', 'API 权限精细控制', '实时风险监控', '交易日志审计']}
|
||||
delay={0.2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
|
||||
169
web/src/components/landing/FooterSection.tsx
Normal file
169
web/src/components/landing/FooterSection.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
export default function FooterSection() {
|
||||
const { language } = useLanguage()
|
||||
return (
|
||||
<footer style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}>
|
||||
<div className='max-w-[1200px] mx-auto px-6 py-10'>
|
||||
{/* Brand */}
|
||||
<div className='flex items-center gap-3 mb-8'>
|
||||
<img src='/images/logo.png' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<div>
|
||||
<div className='text-lg font-bold' style={{ color: '#EAECEF' }}>
|
||||
NOFX
|
||||
</div>
|
||||
<div className='text-xs' style={{ color: '#848E9C' }}>
|
||||
AI 交易的未来标准
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multi-link columns */}
|
||||
<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'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
链接
|
||||
</h3>
|
||||
<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'
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
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'
|
||||
>
|
||||
X (Twitter)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
className='text-sm font-semibold mb-3'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
资源
|
||||
</h3>
|
||||
<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'
|
||||
>
|
||||
文档
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
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'
|
||||
>
|
||||
Pull Requests
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
className='text-sm font-semibold mb-3'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
支持方
|
||||
</h3>
|
||||
<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'
|
||||
>
|
||||
Aster DEX
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
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'
|
||||
>
|
||||
Hyperliquid
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className='hover:text-[#F0B90B]'
|
||||
href='https://amber.ac/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
Amber.ac <span className='opacity-70'>(战略投资)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom note (kept subtle) */}
|
||||
<div
|
||||
className='pt-6 mt-8 text-center text-xs'
|
||||
style={{ color: '#5E6673', borderTop: '1px solid #2B3139' }}
|
||||
>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className='mt-1'>{t('footerWarning', language)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
97
web/src/components/landing/HeaderBar.tsx
Normal file
97
web/src/components/landing/HeaderBar.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
|
||||
export default function HeaderBar({ onLoginClick }: { onLoginClick: () => void }) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
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'>
|
||||
{/* Logo */}
|
||||
<div className='flex items-center gap-3'>
|
||||
<img src='/images/logo.png' 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)' }}>
|
||||
Agentic Trading OS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className='hidden md:flex items-center gap-6'>
|
||||
{['功能', '如何运作', 'GitHub', '社区'].map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href={
|
||||
item === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item === '社区'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item === '功能' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={item === 'GitHub' || item === '社区' ? '_blank' : undefined}
|
||||
rel={item === 'GitHub' || item === '社区' ? 'noopener noreferrer' : undefined}
|
||||
className='text-sm transition-colors relative group'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item}
|
||||
<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>
|
||||
))}
|
||||
<button
|
||||
onClick={onLoginClick}
|
||||
className='px-4 py-2 rounded font-semibold text-sm'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
登录 / 注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
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' />}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<motion.div
|
||||
initial={false}
|
||||
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)' }}
|
||||
>
|
||||
<div className='px-4 py-4 space-y-3'>
|
||||
{['功能', '如何运作', 'GitHub', '社区'].map((item) => (
|
||||
<a key={item} href={`#${item}`} className='block text-sm py-2' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
<button
|
||||
onClick={() => {
|
||||
onLoginClick()
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='w-full px-4 py-2 rounded font-semibold text-sm mt-2'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
登录 / 注册
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
82
web/src/components/landing/HeroSection.tsx
Normal file
82
web/src/components/landing/HeroSection.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
|
||||
export default function HeroSection() {
|
||||
const { scrollYProgress } = useScroll()
|
||||
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
|
||||
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
|
||||
|
||||
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 } } }
|
||||
|
||||
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'>
|
||||
{/* Left Content */}
|
||||
<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)' }}
|
||||
>
|
||||
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
3 天内 2.5K+ GitHub Stars
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
Read the Market.
|
||||
<br />
|
||||
<span style={{ color: 'var(--brand-yellow)' }}>Write the Trade.</span>
|
||||
</h1>
|
||||
|
||||
<motion.p className='text-xl leading-relaxed' style={{ color: 'var(--text-secondary)' }} variants={fadeInUp}>
|
||||
NOFX 是 AI 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster DEX 等交易所,
|
||||
自托管、多代理竞争,让 AI 为你自动决策、执行和优化交易。
|
||||
</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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
alt='GitHub Contributors'
|
||||
className='h-7'
|
||||
/>
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
<motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}>
|
||||
由 Aster DEX 和 Binance 提供支持,Amber.ac 战略投资。
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Visual */}
|
||||
<motion.img src='/images/main.png' alt='NOFX Platform' className='w-full opacity-90' whileHover={{ scale: 1.05, rotate: 5 }} transition={{ type: 'spring', stiffness: 300 }} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
74
web/src/components/landing/HowItWorksSection.tsx
Normal file
74
web/src/components/landing/HowItWorksSection.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
|
||||
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-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)' }}>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='text-lg leading-relaxed' style={{ color: 'var(--text-secondary)' }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HowItWorksSection() {
|
||||
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)' }}>
|
||||
如何开始使用 NOFX
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
四个简单步骤,开启 AI 自动交易之旅
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className='space-y-8'>
|
||||
{[
|
||||
{ number: 1, title: '拉取 GitHub 仓库', description: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。' },
|
||||
{ number: 2, title: '配置环境', description: '前端设置交易所 API(如 Binance、Hyperliquid)、AI 模型和自定义提示词。' },
|
||||
{ number: 3, title: '部署与运行', description: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。' },
|
||||
{ number: 4, title: '优化与贡献', description: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。' },
|
||||
].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)' }}
|
||||
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>
|
||||
<div>
|
||||
<div className='font-semibold mb-2' style={{ color: '#F6465D' }}>
|
||||
重要风险提示
|
||||
</div>
|
||||
<p className='text-sm' style={{ color: 'var(--text-secondary)' }}>
|
||||
dev 分支不稳定,勿用无法承受损失的资金。NOFX 非托管,无官方策略。交易有风险,投资需谨慎。
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
)
|
||||
}
|
||||
63
web/src/components/landing/LoginModal.tsx
Normal file
63
web/src/components/landing/LoginModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<motion.div
|
||||
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 }}
|
||||
exit={{ opacity: 0 }}
|
||||
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)' }}
|
||||
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>
|
||||
<h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
访问 NOFX 平台
|
||||
</h2>
|
||||
<p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}>
|
||||
请选择登录或注册以访问完整的 AI 交易平台
|
||||
</p>
|
||||
<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)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
登录
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/register')
|
||||
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)' }}
|
||||
whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
注册新账号
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,22 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
/* Binance Brand Colors */
|
||||
--brand-yellow: #F0B90B;
|
||||
--brand-black: #0C0E12;
|
||||
--brand-dark-gray: #1E2329;
|
||||
--brand-light-gray: #EAECEF;
|
||||
--brand-almost-white: #FAFAFA;
|
||||
--brand-white: #FFFFFF;
|
||||
|
||||
/* Binance Theme Colors */
|
||||
--binance-yellow: #F0B90B;
|
||||
--binance-yellow-dark: #C99400;
|
||||
--binance-yellow-light: #FCD535;
|
||||
--binance-yellow-glow: rgba(240, 185, 11, 0.2);
|
||||
|
||||
--background: #0B0E11;
|
||||
--background: #181A20; /* Binance body bg */
|
||||
--header-bg: #0B0E11; /* Binance header bg */
|
||||
--background-elevated: #181A20;
|
||||
--foreground: #EAECEF;
|
||||
--panel-bg: #1E2329;
|
||||
@@ -138,10 +147,10 @@ body {
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,12 +180,9 @@ body {
|
||||
}
|
||||
|
||||
/* Glass effect - Binance header style */
|
||||
.glass {
|
||||
background: var(--background-elevated);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
.header-bar {
|
||||
background: var(--header-bg);
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Monospace numbers */
|
||||
|
||||
6
web/src/lib/utils.ts
Normal file
6
web/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
52
web/src/pages/LandingPage.tsx
Normal file
52
web/src/pages/LandingPage.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import HeaderBar from '../components/landing/HeaderBar'
|
||||
import HeroSection from '../components/landing/HeroSection'
|
||||
import AboutSection from '../components/landing/AboutSection'
|
||||
import FeaturesSection from '../components/landing/FeaturesSection'
|
||||
import HowItWorksSection from '../components/landing/HowItWorksSection'
|
||||
import CommunitySection from '../components/landing/CommunitySection'
|
||||
import AnimatedSection from '../components/landing/AnimatedSection'
|
||||
import LoginModal from '../components/landing/LoginModal'
|
||||
import FooterSection from '../components/landing/FooterSection'
|
||||
|
||||
export function LandingPage() {
|
||||
const [showLoginModal, setShowLoginModal] = useState(false)
|
||||
return (
|
||||
<div className='min-h-screen overflow-hidden' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
|
||||
<HeaderBar onLoginClick={() => setShowLoginModal(true)} />
|
||||
<HeroSection />
|
||||
<AboutSection />
|
||||
<FeaturesSection />
|
||||
<HowItWorksSection />
|
||||
<CommunitySection />
|
||||
|
||||
{/* CTA */}
|
||||
<AnimatedSection backgroundColor='var(--panel-bg)'>
|
||||
<div className='max-w-4xl mx-auto text-center'>
|
||||
<motion.h2 className='text-5xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
|
||||
准备好定义 AI 交易的未来吗?
|
||||
</motion.h2>
|
||||
<motion.p className='text-xl mb-12' style={{ color: 'var(--text-secondary)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }}>
|
||||
从加密市场起步,扩展到 TradFi。NOFX 是 AgentFi 的基础架构。
|
||||
</motion.p>
|
||||
<div className='flex flex-wrap justify-center gap-4'>
|
||||
<motion.button onClick={() => setShowLoginModal(true)} className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
立即开始
|
||||
<motion.div animate={{ x: [0, 5, 0] }} transition={{ duration: 1.5, repeat: Infinity }}>
|
||||
<ArrowRight className='w-5 h-5' />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
<motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-dark-gray)', color: 'var(--brand-light-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
查看源码
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
{showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} />}
|
||||
<FooterSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user