init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
node_modules/
.next/
.env.local
*.log
.git

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
landingpage/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

18
landingpage/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
RUN apk add --no-cache python3 make g++
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN npm ci --legacy-peer-deps
COPY . .
RUN npm run build
# ---- Run stage ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3011
CMD ["node", "server.js"]

41
landingpage/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Asset Homepage
这是资产管理平台的前台页面项目。
## 技术栈
- Next.js 15.1.4
- React 19.0.0
- TypeScript 5
- Tailwind CSS 3.4.17
## 开发
首先,安装依赖:
```bash
npm install
```
然后,运行开发服务器:
```bash
npm run dev
```
在浏览器中打开 [http://localhost:3002](http://localhost:3002) 查看结果。
注意:此项目运行在端口 3002以避免与后台项目端口 3000冲突。
## 项目结构
- `app/` - Next.js App Router 页面和布局
- `components/` - React 组件
- `public/` - 静态资源文件
## 与后台项目的关系
- 后台项目:`asset-dashboard-next` (端口 3000)
- 前台项目:`asset-homepage` (端口 3002)
两个项目使用相同的依赖版本和配置,确保开发体验的一致性。

119
landingpage/app/globals.css Normal file
View File

@@ -0,0 +1,119 @@
/* 导入设计系统 CSS 变量 */
@import '../styles/design-system.css';
/* 隐藏滚动条 */
.overflow-x-auto::-webkit-scrollbar {
display: none;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 向后兼容旧的 CSS 变量 */
:root {
--background: var(--bg-base);
--foreground: var(--text-primary);
}
[data-theme="dark"] {
--background: var(--bg-base);
--foreground: var(--text-primary);
}
body {
color: var(--foreground);
background: var(--background);
font-family: var(--font-noto-sans-sc), var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
overflow-x: hidden;
}
/* 文本选中样式 - Light Mode (毛玻璃效果) */
::selection {
background-color: rgba(156, 163, 175, 0.35);
}
::-moz-selection {
background-color: rgba(156, 163, 175, 0.35);
}
/* 文本选中样式 - Dark Mode (毛玻璃效果) */
[data-theme="dark"] ::selection {
background-color: rgba(156, 163, 175, 0.4);
}
[data-theme="dark"] ::-moz-selection {
background-color: rgba(156, 163, 175, 0.4);
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* HeroUI Navbar: prevent border-color from falling back to currentColor
when --nextui-divider CSS variable is not resolved */
nav.z-40 {
border-top: none !important;
border-left: none !important;
border-right: none !important;
box-shadow: none !important;
outline: none !important;
}
/* Font family utilities with Chinese support */
.font-inter {
font-family: var(--font-noto-sans-sc), var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
}
.font-domine {
font-family: var(--font-noto-serif-sc), var(--font-domine), Georgia, "Noto Serif", serif;
}
.font-jetbrains {
font-family: var(--font-jetbrains), "Courier New", monospace;
}
/* Calculator card wiggle animation */
@keyframes wiggle {
0% { transform: rotate(0deg); }
15% { transform: rotate(-5deg); }
35% { transform: rotate(4.5deg); }
55% { transform: rotate(-2.5deg); }
75% { transform: rotate(2deg); }
90% { transform: rotate(-0.5deg); }
100% { transform: rotate(0deg); }
}
.calculator-card-container {
cursor: pointer;
}
.calculator-card-container:hover {
animation: wiggle 2.5s ease-in-out infinite;
}
/* TrustedBy section mobile marquee */
@keyframes trusted-marquee {
from { transform: translateX(0%); }
to { transform: translateX(-50%); }
}
/* Arrow bounce animation - left and right */
@keyframes arrowBounce {
0%, 100% {
transform: translateX(0);
}
50% {
transform: translateX(8px);
}
}
.arrow-icon {
transition: transform 0.3s ease;
}
.group:hover .arrow-icon {
animation: arrowBounce 1.5s ease-in-out infinite;
}

View File

@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import "./globals.css";
import Providers from "@/components/Providers";
import localFont from "next/font/local";
const inter = localFont({
src: "../public/fonts/InterVariable.woff2",
variable: "--font-inter",
display: "swap",
});
export const metadata: Metadata = {
title: "Asset Homepage",
description: "Asset management platform homepage",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={inter.variable}>
<body className={`antialiased ${inter.className}`}>
<Providers>
{children}
</Providers>
</body>
</html>
);
}

35
landingpage/app/page.tsx Normal file
View File

@@ -0,0 +1,35 @@
'use client';
if (typeof window !== 'undefined') {
history.scrollRestoration = 'manual';
}
import Navbar from '@/components/Navbar';
import HeroSection from '@/components/HeroSection';
import StatsSection from '@/components/StatsSection';
import TrustedBySection from '@/components/TrustedBySection';
import WhyAssetXSection from '@/components/WhyAssetXSection';
import HowItWorksSection from '@/components/HowItWorksSection';
import SecuritySection from '@/components/SecuritySection';
import Footer from '@/components/Footer';
import { useTheme } from '@/contexts/ThemeContext';
export default function Home() {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`min-h-screen overflow-x-hidden ${isDark ? 'bg-[#0a0a0a]' : 'bg-white'}`}>
<Navbar />
<main style={{ paddingTop: 'var(--navbar-height, 72px)' }}>
<HeroSection />
<StatsSection />
</main>
<TrustedBySection />
<WhyAssetXSection />
<HowItWorksSection />
<SecuritySection />
<Footer />
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,181 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { Link, Button } from '@heroui/react';
import { Github, Mail } from 'lucide-react';
import { motion } from 'framer-motion';
const XIcon = ({ color }: { color: string }) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill={color}>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
);
const DiscordIcon = ({ color }: { color: string }) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill={color}>
<path d="M20.317 4.492c-1.53-.69-3.17-1.2-4.885-1.49a.075.075 0 0 0-.079.036c-.21.369-.444.85-.608 1.23a18.566 18.566 0 0 0-5.487 0 12.36 12.36 0 0 0-.617-1.23A.077.077 0 0 0 8.562 3c-1.714.29-3.354.8-4.885 1.491a.07.07 0 0 0-.032.027C.533 9.093-.32 13.555.099 17.961a.08.08 0 0 0 .031.055 20.03 20.03 0 0 0 5.993 2.98.078.078 0 0 0 .084-.026 13.83 13.83 0 0 0 1.226-1.963.074.074 0 0 0-.041-.104 13.175 13.175 0 0 1-1.872-.878.075.075 0 0 1-.008-.125c.126-.093.252-.19.372-.287a.075.075 0 0 1 .078-.01c3.927 1.764 8.18 1.764 12.061 0a.075.075 0 0 1 .079.009c.12.098.245.195.372.288a.075.075 0 0 1-.006.125c-.598.344-1.22.635-1.873.877a.075.075 0 0 0-.041.105c.36.687.772 1.341 1.225 1.962a.077.077 0 0 0 .084.028 19.963 19.963 0 0 0 6.002-2.981.076.076 0 0 0 .032-.054c.5-5.094-.838-9.52-3.549-13.442a.06.06 0 0 0-.031-.028zM8.02 15.278c-1.182 0-2.157-1.069-2.157-2.38 0-1.312.956-2.38 2.157-2.38 1.21 0 2.176 1.077 2.157 2.38 0 1.312-.956 2.38-2.157 2.38zm7.975 0c-1.183 0-2.157-1.069-2.157-2.38 0-1.312.955-2.38 2.157-2.38 1.21 0 2.176 1.077 2.157 2.38 0 1.312-.946 2.38-2.157 2.38z"/>
</svg>
);
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
export default function Footer() {
const { t } = useLanguage();
const { theme } = useTheme();
const isDark = theme === 'dark';
const [animate, setAnimate] = useState(false);
const footerRef = useRef<HTMLElement>(null);
useEffect(() => {
const currentRef = footerRef.current;
if (!currentRef) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setAnimate(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
observer.observe(currentRef);
return () => observer.disconnect();
}, []);
const socialIcons = [
{ render: (c: string) => <XIcon color={c} />, alt: 'X' },
{ render: (c: string) => <Github size={24} color={c} />, alt: 'GitHub' },
{ render: (c: string) => <DiscordIcon color={c} />, alt: 'Discord' },
{ render: (c: string) => <Mail size={24} color={c} />, alt: 'Email' },
];
return (
<footer ref={footerRef} className="w-full flex flex-col items-center border-t bg-bg-base border-border-normal">
{/* Main Footer Content */}
<div className="w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] px-4 md:px-6 2xl:px-10 3xl:px-16 py-10 md:py-20 flex flex-col md:flex-row gap-8">
{/* Left Section */}
<motion.div
className="flex flex-col gap-4 md:gap-6 md:w-[389.33px]"
initial={{ opacity: 0, x: -24 }}
animate={animate ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{/* Logo */}
<div className="flex items-center md:items-center">
<Image
src="/logo0.svg"
alt="AssetX Logo"
width={160}
height={40}
className="w-[160px] h-auto"
style={{ filter: isDark ? 'invert(1) brightness(1.2)' : 'none' }}
/>
</div>
{/* Address + Social Icons */}
<div className="flex flex-col gap-3 md:gap-6">
<div
className="text-text-tertiary text-left font-domine"
style={{ fontSize: '14px', lineHeight: '150%', fontWeight: 400 }}
>
G/F, Hong Kong Museum Of Art, 10 Salisbury
<br />
Rd, Tsim Sha Tsui, HongKong
</div>
<div className="flex flex-row gap-1 md:gap-2 items-center">
{socialIcons.map((icon, index) => (
<Button key={index} isIconOnly variant="light" className="min-w-10 w-10 h-10">
{icon.render(isDark ? '#9ca3af' : '#6b7280')}
</Button>
))}
</div>
</div>
</motion.div>
{/* Right Section */}
<div className="flex flex-col md:flex-row md:justify-between md:flex-1 gap-6 md:gap-0">
{/* Products + Resources */}
<div className="flex flex-row gap-4 md:contents">
<motion.div
className="flex flex-col gap-4 flex-1 md:flex-none md:w-[178.67px]"
initial={{ opacity: 0, y: 24 }}
animate={animate ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.15 }}
>
<h3 className="font-domine font-bold text-base text-text-primary" style={{ lineHeight: '150%' }}>
{t('footer.products')}
</h3>
<div className="flex flex-col gap-2">
{[1, 2, 3, 4, 5].map((num) => (
<Link key={num} href="#" className="font-domine text-sm text-text-tertiary hover:text-text-primary" style={{ lineHeight: '150%' }}>
{t(`footer.product${num}`)}
</Link>
))}
</div>
</motion.div>
<motion.div
className="flex flex-col gap-4 flex-1 md:flex-none md:w-[178.67px]"
initial={{ opacity: 0, y: 24 }}
animate={animate ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.25 }}
>
<h3 className="font-domine font-bold text-base text-text-primary" style={{ lineHeight: '150%' }}>
{t('footer.resources')}
</h3>
<div className="flex flex-col gap-2">
{[1, 2, 3, 4, 5].map((num) => (
<Link key={num} href="#" className="font-domine text-sm text-text-tertiary hover:text-text-primary" style={{ lineHeight: '150%' }}>
{t(`footer.resource${num}`)}
</Link>
))}
</div>
</motion.div>
</div>
{/* Company */}
<motion.div
className="flex flex-col gap-4 md:w-[178.67px]"
initial={{ opacity: 0, y: 24 }}
animate={animate ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.35 }}
>
<h3 className="font-domine font-bold text-base text-text-primary" style={{ lineHeight: '150%' }}>
{t('footer.company')}
</h3>
<div className="flex flex-col gap-2">
{[1, 2, 3, 4].map((num) => (
<Link key={num} href="#" className="font-domine text-sm text-text-tertiary hover:text-text-primary" style={{ lineHeight: '150%' }}>
{t(`footer.company${num}`)}
</Link>
))}
</div>
</motion.div>
</div>
</div>
{/* Bottom Section */}
<motion.div
className="w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] border-t border-border-subtle px-4 md:px-6 2xl:px-10 3xl:px-16 py-6 md:py-8 flex flex-col md:flex-row items-center md:items-center justify-between gap-3 md:gap-0"
initial={{ opacity: 0 }}
animate={animate ? { opacity: 1 } : {}}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.5 }}
>
<div className="text-text-tertiary font-domine text-sm text-center md:text-left" style={{ lineHeight: '150%' }}>
{t('footer.copyright')}
</div>
<div className="flex flex-row gap-6">
<Link href="#" className={`font-domine text-sm ${isDark ? 'text-[#9ca1af] hover:text-[#fafafa]' : 'text-[#9ca1af] hover:text-[#111827]'}`} style={{ lineHeight: '150%' }}>
{t('footer.privacy')}
</Link>
<Link href="#" className={`font-domine text-sm ${isDark ? 'text-[#9ca1af] hover:text-[#fafafa]' : 'text-[#9ca1af] hover:text-[#111827]'}`} style={{ lineHeight: '150%' }}>
{t('footer.terms')}
</Link>
</div>
</motion.div>
</footer>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowUpRight } from 'lucide-react';
import { Button } from '@heroui/react';
import { useLanguage } from '@/contexts/LanguageContext';
export default function HeroButtons() {
const { t } = useLanguage();
return (
<motion.div
className="flex flex-col sm:flex-row gap-4 items-stretch sm:items-start justify-start flex-shrink-0 relative w-full sm:w-auto"
initial={{ opacity: 0, scale: 0.92, y: 16 }}
whileInView={{ opacity: 1, scale: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: 'easeOut', delay: 1.0 }}
>
{/* Start Investing Button */}
<Button
size="lg"
className="bg-white text-special-black font-bold text-lg h-[60px] px-8 transition-transform duration-300 ease-out hover:scale-105 w-full sm:w-auto"
endContent={<ArrowUpRight size={20} className="flex-shrink-0" />}
onPress={() => window.open('http://152.69.205.186:3010/market', '_blank')}
>
{t('hero.startInvesting')}
</Button>
{/* Read the Whitepaper Button */}
<Button
size="lg"
variant="bordered"
className="border-white/20 bg-white/10 text-white font-bold text-lg h-[60px] px-8 backdrop-blur-[30px] hover:bg-white/20 hover:border-white/40 w-full sm:w-auto"
>
{t('hero.readWhitepaper')}
</Button>
</motion.div>
);
}

View File

@@ -0,0 +1,71 @@
'use client';
import { motion } from 'framer-motion';
import HeroTitle from './HeroTitle';
import HeroButtons from './HeroButtons';
import { useLanguage } from '@/contexts/LanguageContext';
export default function HeroSection() {
const { t } = useLanguage();
// StatsSection 桌面端高度约 182px
const statsHeight = 182;
return (
<section
className="relative w-full min-h-[calc(100vh-var(--navbar-height,72px))] md:min-h-[calc(100vh-var(--navbar-height,72px)-182px)]"
style={{ overflow: 'hidden' }}
>
{/* Video background */}
<video
autoPlay
loop
muted
playsInline
className="absolute inset-0 w-full h-full object-cover"
style={{
zIndex: 0,
objectFit: 'cover',
objectPosition: 'center center',
minWidth: '100%',
minHeight: '100%',
transform: 'scale(1.3)'
}}
>
<source src="https://pub-0b813d5d97b84a06a71fd3d4283fce59.r2.dev/hero-background.mp4" type="video/mp4" />
</video>
<div
className="relative flex flex-col items-start justify-center min-h-[calc(100vh-var(--navbar-height,72px))] md:min-h-[calc(100vh-var(--navbar-height,72px)-182px)] px-6 py-20 md:px-[85px] md:py-16"
style={{
background: 'linear-gradient(to left, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55))',
zIndex: 1
}}
>
<div className="flex flex-col gap-12 md:gap-[88px] w-full max-w-[926px] 2xl:max-w-[1200px] 3xl:max-w-[1500px]">
<div className="flex flex-col gap-5">
<HeroTitle />
<motion.p
className="text-base sm:text-xl max-w-[640px] 2xl:max-w-[800px] 3xl:max-w-[1000px]"
style={{
color: 'rgba(255,255,255,0.65)',
fontFamily: 'Inter, sans-serif',
lineHeight: '1.625',
fontWeight: 400,
minHeight: 'calc(3 * 1.625em)',
}}
initial={{ opacity: 0, y: 16, filter: 'blur(8px)' }}
whileInView={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: 'easeOut', delay: 0.75 }}
>
{t('hero.description')}
</motion.p>
</div>
<HeroButtons />
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import { motion } from 'framer-motion';
import { useLanguage } from '@/contexts/LanguageContext';
export default function HeroTitle() {
const { t } = useLanguage();
const titleStyle = {
fontSize: 'clamp(48px, 6.5vw, 140px)',
lineHeight: '100%',
letterSpacing: '-0.03em',
};
return (
<div className="flex flex-col gap-4 items-start justify-start self-stretch flex-shrink-0 relative overflow-hidden select-none">
<motion.div
className="text-special-white text-left relative self-stretch font-domine md:whitespace-nowrap"
style={{ ...titleStyle, fontWeight: 400 }}
initial={{ opacity: 0, x: -60, filter: 'blur(12px)' }}
whileInView={{ opacity: 1, x: 0, filter: 'blur(0px)' }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: 'easeOut', delay: 0.2 }}
>
{t('hero.title1')}
</motion.div>
<motion.div
className="text-special-white text-left relative w-full font-domine md:whitespace-nowrap"
style={{ ...titleStyle, fontWeight: 700 }}
initial={{ opacity: 0, x: 60, filter: 'blur(12px)' }}
whileInView={{ opacity: 1, x: 0, filter: 'blur(0px)' }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: 'easeOut', delay: 0.4 }}
>
{t('hero.title2')}
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,485 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { motion, useInView } from 'framer-motion';
import { Card, CardBody, Chip } from '@heroui/react';
import { Calculator, TrendingUp } from 'lucide-react';
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import { useCountUp } from '@/hooks/useCountUp';
export default function HowItWorksSection() {
const { t } = useLanguage();
const { theme } = useTheme();
const [activeStep, setActiveStep] = useState(1);
const sectionRef = useRef<HTMLElement>(null);
const isInView = useInView(sectionRef, { once: true, amount: 0.3 });
const investAmount = useCountUp(100000, 1500, 0.85);
const earnAmount = useCountUp(5150, 1500, 0.85);
// Step 3 inner grid 自适应缩放
const gridRef = useRef<HTMLDivElement>(null);
const [gridScale, setGridScale] = useState(1);
useEffect(() => {
const el = gridRef.current;
if (!el) return;
const observer = new ResizeObserver(([entry]) => {
setGridScale(Math.min(1, entry.contentRect.width / 466));
});
observer.observe(el);
return () => observer.disconnect();
}, []);
const isDark = theme === 'dark';
const steps = [
{
number: 1,
title: t('how.step1.title'),
description: t('how.step1.desc'),
hasLine: true
},
{
number: 2,
title: t('how.step2.title'),
description: t('how.step2.desc'),
hasLine: true
},
{
number: 3,
title: t('how.step3.title'),
description: t('how.step3.desc'),
hasLine: false
}
];
return (
<section
ref={sectionRef}
className="flex flex-col items-center justify-center flex-shrink-0 w-full relative border-y bg-bg-subtle border-border-normal overflow-x-clip py-10 md:py-20 2xl:py-28 3xl:py-36"
>
<div className="flex flex-col md:flex-row items-start justify-between flex-shrink-0 relative w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] px-4 md:px-10 2xl:px-16 3xl:px-24 gap-10 md:gap-0">
{/* 左侧:标题 + 步骤 */}
<div className="flex flex-col gap-8 md:gap-10 items-start justify-start flex-shrink-0 relative w-full md:w-[520px] 2xl:w-[640px] 3xl:w-[800px]">
<motion.div
className="flex flex-col items-start justify-start flex-shrink-0 relative"
initial={{ y: '-1.5rem', opacity: 0 }}
animate={isInView ? { y: 0, opacity: 1 } : { y: '-1.5rem', opacity: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
<h2
className="text-left relative font-domine font-bold text-text-primary"
style={{ fontSize: 'clamp(30px, 3.5vw, 80px)', lineHeight: '120%', letterSpacing: '-0.01em' }}
>
{t('how.title')}
</h2>
</motion.div>
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 relative w-full">
{steps.map((step, index) => {
const isActive = activeStep === step.number;
return (
<motion.div
key={step.number}
onClick={() => setActiveStep(step.number)}
className="flex flex-row gap-6 items-start justify-start flex-shrink-0 relative cursor-pointer"
initial={{ x: '-3rem', opacity: 0 }}
animate={isInView ? { x: 0, opacity: 1 } : { x: '-3rem', opacity: 0 }}
whileHover={{ opacity: 0.8 }}
transition={{ duration: 0.3, ease: 'easeOut', delay: index * 0.15 }}
>
<div className="pt-2 flex flex-col items-center justify-start self-stretch flex-shrink-0 relative">
{isActive ? (
<div
className={`rounded-full flex items-center justify-center flex-shrink-0 w-8 h-[21.63px] transition-all duration-300 ${
isDark ? 'bg-white' : 'bg-[#111827]'
}`}
style={{ padding: '0.31px 0px 1.32px 0px' }}
>
<span className={`text-center text-sm font-bold font-domine transition-all duration-300 ${
isDark ? 'text-[#0a0a0a]' : 'text-[#fcfcfd]'
}`}>
{step.number}
</span>
</div>
) : (
<div className={`rounded-full border-2 flex items-center justify-center flex-shrink-0 w-8 h-[24.5px] transition-all duration-300 ${
isDark ? 'bg-[#0a0a0a] border-[#3f3f46]' : 'bg-[#f9fafb] border-[#d1d5db]'
}`}>
<span className={`text-center text-sm font-bold font-domine transition-all duration-300 ${
isDark ? 'text-[#71717a]' : 'text-[#9ca1af]'
}`}>
{step.number}
</span>
</div>
)}
{step.hasLine && (
<div className="pt-6 flex flex-col items-start justify-center flex-1 w-[2px] relative">
<div className="flex-1 w-[2px] bg-border-normal" />
</div>
)}
</div>
<div
className="flex flex-col gap-2 items-start justify-start flex-1 relative"
style={{ paddingBottom: step.hasLine ? '32px' : '0px' }}
>
<h3
className="text-left font-semibold font-domine transition-all duration-300"
style={{
fontSize: 'clamp(20px, 2vw, 36px)',
lineHeight: '130%',
letterSpacing: '-0.005em',
color: isActive
? (isDark ? '#fafafa' : '#111827')
: (isDark ? '#52525b' : '#6b7280')
}}
>
{step.title}
</h3>
<p className="text-left text-sm md:text-base font-domine text-text-tertiary" style={{ lineHeight: '150%' }}>
{step.description}
</p>
</div>
</motion.div>
);
})}
</div>
</div>
{/* 右侧:卡片区 */}
<motion.div
className="calculator-card-container flex flex-col items-start justify-start flex-shrink-0 w-full md:w-[558px] 2xl:w-[680px] 3xl:w-[720px] relative h-[380px] md:h-[439px] 2xl:h-[520px] 3xl:h-[540px] md:mr-16 2xl:mr-24"
initial={{ x: '3rem', opacity: 0 }}
animate={isInView ? { x: 0, opacity: 1 } : { x: '3rem', opacity: 0 }}
transition={{ duration: 0.5, ease: 'easeOut', delay: 0.2 }}
>
<>
{/* Step 1 背景装饰卡片 — 仅桌面端显示 */}
<Card
className={`h-[162px] absolute z-0 transition-all duration-700 ease-out ${
isInView && activeStep === 1 ? 'opacity-100' : 'opacity-0'
} ${
isDark ? 'bg-white border-[#e5e7eb]' : 'bg-[#111827] border-transparent'
}`}
shadow="lg"
style={{
left: '205.43px',
top: '15.96px',
transformOrigin: '0 0',
transform: isInView && activeStep === 1
? 'rotate(6.535deg) scale(1, 1) translateX(0)'
: 'rotate(6.535deg) scale(1, 1) translateX(3rem)',
transitionDelay: activeStep === 1 ? '200ms' : '0ms',
pointerEvents: activeStep === 1 ? 'auto' : 'none'
}}
>
<div className="flex flex-row items-center gap-4" style={{ padding: '25px 25px 1px 25px' }}>
<Image src="/usd-coin-usdc-logo-10.svg" alt="USDC" width={64} height={64} className="flex-shrink-0" />
<div className="flex-shrink-0 w-[66px] h-[66px] relative">
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-green-400 to-emerald-500" />
<Image src="/image-220.png" alt="Token" width={66} height={66} className="absolute inset-0 rounded-full" />
</div>
</div>
</Card>
{/* Step 1 主卡片 */}
<Card
className={`absolute left-0 top-0 md:top-[99px] w-full z-10 transition-all duration-700 ease-out ${
isInView && activeStep === 1 ? 'opacity-100' : 'opacity-0'
} ${
isDark ? 'bg-[#18181b]/85 border-[#27272a]' : 'bg-white/85 border-[#e5e7eb]'
}`}
shadow="lg"
style={{
backdropFilter: 'blur(12px)',
transform: isInView && activeStep === 1
? 'rotate(-3deg) translateX(0)'
: 'rotate(-3deg) translateX(3rem)',
transformOrigin: 'center center',
transitionDelay: activeStep === 1 ? '200ms' : '0ms',
pointerEvents: activeStep === 1 ? 'auto' : 'none'
}}
>
<CardBody className="p-8">
<div className="flex flex-row items-center justify-between mb-6">
<div className="flex flex-row gap-3 items-center">
<div className={`rounded-xl w-10 h-10 flex items-center justify-center ${
isDark ? 'bg-white' : 'bg-[#111827]'
}`}>
<Calculator
size={24}
color={isDark ? '#000000' : '#ffffff'}
/>
</div>
<span
className={`font-domine text-base font-medium ${
isDark ? 'text-[#fafafa]' : 'text-[#111827]'
}`}
>
{t('how.simulator.title')}
</span>
</div>
<Chip className="bg-green-50 text-green-600 font-bold" size="sm">
+5.2% APY
</Chip>
</div>
<div className="mb-4">
<div className="flex flex-col gap-2">
<span
className={`font-domine text-sm ${
isDark ? 'text-[#71717a]' : 'text-[#9ca1af]'
}`}
>
{t('how.simulator.invest')}
</span>
<span
ref={investAmount.elementRef}
className={`font-domine text-3xl font-bold ${
isDark ? 'text-[#fafafa]' : 'text-[#111827]'
}`}
style={{
lineHeight: '120%',
letterSpacing: '-0.01em'
}}
>
${investAmount.count.toLocaleString()}
</span>
</div>
<div className={`mt-4 h-2 rounded-full relative ${
isDark ? 'bg-[#27272a]' : 'bg-[#e5e7eb]'
}`}>
<div className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 rounded-full ${
isDark ? 'bg-white' : 'bg-[#111827]'
}`} />
</div>
</div>
<div className={`mt-6 pt-6 border-t ${
isDark ? 'border-[#27272a]' : 'border-[#e5e7eb]'
}`}>
<span
className={`font-domine text-sm block mb-2 ${
isDark ? 'text-[#71717a]' : 'text-[#9ca1af]'
}`}
>
{t('how.simulator.earn')}
</span>
<div className="flex flex-row gap-2 items-center">
<TrendingUp size={24} color="#059669" />
<span
ref={earnAmount.elementRef}
className="text-green-600 font-domine text-3xl font-bold"
style={{
lineHeight: '120%',
letterSpacing: '-0.01em'
}}
>
+${earnAmount.count.toLocaleString()}
</span>
</div>
</div>
</CardBody>
</Card>
</>
<Card
className={`absolute left-0 top-0 md:top-[99px] w-full transition-all duration-700 ease-out ${
isInView && activeStep === 2 ? 'opacity-100' : 'opacity-0'
} ${
isDark ? 'bg-[#18181b]/85 border-[#27272a]' : 'bg-white/85 border-[#f3f4f6]'
}`}
shadow="lg"
style={{
backdropFilter: 'blur(12px)',
transform: isInView && activeStep === 2
? 'rotate(3deg) translateX(0)'
: 'rotate(3deg) translateX(3rem)',
transformOrigin: '0 0',
transitionDelay: activeStep === 2 ? '200ms' : '0ms',
pointerEvents: activeStep === 2 ? 'auto' : 'none'
}}
>
<CardBody className="p-6 flex flex-col gap-6">
<div
className={`border-b pb-4 flex flex-row items-center justify-between w-full ${
isDark ? 'border-[#27272a]' : 'border-[#f3f4f6]'
}`}
>
<span className={`font-domine text-lg font-bold ${
isDark ? 'text-[#fafafa]' : 'text-[#000000]'
}`}>
Fund Market
</span>
<span className="text-[#059669] font-domine text-base font-bold">
Connect Wallet
</span>
</div>
<div className="flex flex-col gap-4 w-full">
<div className={`rounded-xl border p-4 flex flex-row gap-4 items-center h-24 ${
isDark ? 'bg-[#27272a] border-[#3f3f46]' : 'bg-[#f9fafb] border-[#f3f4f6]'
}`}>
<div className={`rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-white' : 'bg-[#000000]'
}`}>
<span className={`font-domine text-base font-bold ${
isDark ? 'text-[#000000]' : 'text-white'
}`}>O</span>
</div>
<div className="flex flex-col gap-2 flex-1">
<div className={`rounded h-4 w-24 ${
isDark ? 'bg-[#3f3f46]' : 'bg-[#e5e7eb]'
}`} />
<div className={`rounded h-3 w-16 ${
isDark ? 'bg-[#52525b]' : 'bg-[#f3f4f6]'
}`} />
</div>
<div className="flex flex-col gap-2 items-end flex-shrink-0">
<div className={`rounded h-4 w-20 ${
isDark ? 'bg-[#3f3f46]' : 'bg-[#e5e7eb]'
}`} />
<span className="text-[#059669] font-domine text-base font-bold">
APY 30.2%
</span>
</div>
</div>
<div className={`rounded-xl border p-4 flex flex-row gap-4 items-center h-24 opacity-60 ${
isDark ? 'bg-[#27272a] border-[#3f3f46]' : 'bg-[#f9fafb] border-[#f3f4f6]'
}`}>
<div className={`rounded-full w-12 h-12 flex-shrink-0 ${
isDark ? 'bg-[#52525b]' : 'bg-[#d1d5db]'
}`} />
<div className="flex flex-col gap-2 flex-1">
<div className={`rounded h-4 w-32 ${
isDark ? 'bg-[#3f3f46]' : 'bg-[#e5e7eb]'
}`} />
<div className={`rounded h-3 w-20 ${
isDark ? 'bg-[#52525b]' : 'bg-[#f3f4f6]'
}`} />
</div>
<div className="flex flex-col gap-2 items-end flex-shrink-0">
<div className={`rounded h-4 w-20 ${
isDark ? 'bg-[#3f3f46]' : 'bg-[#e5e7eb]'
}`} />
<span className="text-[#059669] font-domine text-base font-bold">
APY 15.2%
</span>
</div>
</div>
</div>
</CardBody>
</Card>
<Card
className={`absolute left-0 top-0 md:top-[99px] w-full transition-all duration-700 ease-out ${
isInView && activeStep === 3 ? 'opacity-100' : 'opacity-0'
} ${
isDark ? 'bg-[#18181b]/85 border-[#27272a]' : 'bg-white/85 border-[#f3f4f6]'
}`}
shadow="lg"
style={{
backdropFilter: 'blur(12px)',
transform: isInView && activeStep === 3
? 'rotate(3deg) translateX(0)'
: 'rotate(3deg) translateX(3rem)',
transformOrigin: '0 0',
transitionDelay: activeStep === 3 ? '200ms' : '0ms',
overflow: 'hidden',
pointerEvents: activeStep === 3 ? 'auto' : 'none'
}}
>
<CardBody className="p-6 flex flex-col gap-6">
<div className={`border-b pb-[17px] flex flex-row items-center justify-between ${
isDark ? 'border-[#27272a]' : 'border-[#f3f4f6]'
}`} style={{ height: '45px' }}>
<span className={`font-domine text-lg font-bold ${
isDark ? 'text-[#fafafa]' : 'text-[#000000]'
}`}>
Defi
</span>
<span className="text-[#059669] font-domine text-base font-bold">
+5.2% APY
</span>
</div>
{/* Cards Grid Container — 按比例缩放 */}
<div
ref={gridRef}
className="w-full overflow-hidden"
style={{ height: `${Math.round(195 * gridScale)}px` }}
>
<div
className="relative"
style={{
width: '466px',
height: '195px',
transform: `scale(${gridScale}) rotate(-3deg)`,
transformOrigin: '0 0',
overflow: 'hidden'
}}
>
{[
{ left: '8.58px', top: '0.46px', label: '+10% APY', button: 'Boost' },
{ left: '160.18px', top: '11.66px', label: '+10% APY', button: 'Boost' },
{ left: '312.18px', top: '19.66px', label: '+10% APY', button: 'Boost' },
{ left: '160.18px', top: '11.66px', label: 'Get USDC', button: 'SWAP' },
{ left: '312.18px', top: '19.66px', label: '10% APY', button: 'LP' },
].map((item, i) => (
<div
key={i}
className="absolute"
style={{
width: '135.53px',
height: '165.15px',
left: item.left,
top: item.top,
transform: 'rotate(3deg)',
transformOrigin: '0 0'
}}
>
<div className="bg-bg-elevated rounded-[12px] border border-border-subtle p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
<div className="bg-text-primary rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
<span className="text-text-inverse font-domine text-[16px] font-bold leading-6">O</span>
</div>
<span
className="text-text-primary font-domine text-[16px] font-bold text-center"
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
{item.label}
</span>
<div
className="bg-text-primary rounded-lg px-4 py-2 flex items-center justify-center"
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
>
<span className="text-text-inverse font-domine text-[12px] font-bold leading-4">
{item.button}
</span>
</div>
</div>
</div>
))}
</div>
</div>
</CardBody>
</Card>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,726 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import {
Navbar as HeroNavbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Button,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem
} from '@heroui/react';
import { ArrowRight, ChevronDown, Menu, X, Sun, Moon, Globe, Layers, Rocket, ArrowLeftRight, Coins, LayoutDashboard, Radio, BookOpen, ShieldCheck, GraduationCap, MessageCircle, Headphones, Users, Briefcase, Mail, Newspaper, Rainbow } from 'lucide-react';
import ProductMenu from './ProductMenu';
import ResourceMenu from './ResourceMenu';
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const { language, setLanguage, t } = useLanguage();
const { theme, toggleTheme } = useTheme();
const [showProductMenu, setShowProductMenu] = useState(false);
const [showResourceMenu, setShowResourceMenu] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileProductOpen, setMobileProductOpen] = useState(false);
const [mobileResourceOpen, setMobileResourceOpen] = useState(false);
const [navBottom, setNavBottom] = useState(64);
const navRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => { document.body.style.overflow = ''; };
}, [mobileMenuOpen]);
useEffect(() => {
if (!showProductMenu && !showResourceMenu) return;
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Element;
if (showProductMenu && !target.closest('.product-menu-container')) {
setShowProductMenu(false);
}
if (showResourceMenu && !target.closest('.resource-menu-container')) {
setShowResourceMenu(false);
}
};
// 延迟注册,避免当前 click 事件冒泡立即触发
const timer = requestAnimationFrame(() => {
document.addEventListener('click', handleClickOutside);
});
return () => {
cancelAnimationFrame(timer);
document.removeEventListener('click', handleClickOutside);
};
}, [showProductMenu, showResourceMenu]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
setMobileMenuOpen(false);
setMobileProductOpen(false);
setMobileResourceOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const measure = () => {
const el = navRef.current ?? (document.querySelector('header, nav') as HTMLElement);
if (el) {
navRef.current = el;
const h = Math.ceil(el.getBoundingClientRect().height);
setNavBottom(h);
document.documentElement.style.setProperty('--navbar-height', `${h}px`);
}
};
measure();
const ro = new ResizeObserver(measure);
const el = document.querySelector('header, nav') as HTMLElement;
if (el) ro.observe(el);
return () => ro.disconnect();
}, []);
const isDark = theme === 'dark';
return (
<>
<HeroNavbar
maxWidth="full"
position="static"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 50,
backgroundColor: isDark
? scrolled ? 'rgba(10, 10, 10, 0.7)' : 'rgba(10, 10, 10, 0.9)'
: scrolled ? 'rgba(255, 255, 255, 0.7)' : 'rgba(255, 255, 255, 0.9)',
backdropFilter: 'blur(50px)',
borderTop: 'none',
borderLeft: 'none',
borderRight: 'none',
borderBottom: isDark
? scrolled ? '1px solid #27272a' : '1px solid #18181b'
: '1px solid #f3f4f6',
boxShadow: 'none',
transition: 'background-color 0.3s ease-out, border-color 0.3s ease-out'
}}
classNames={{
wrapper: "px-4 lg:px-10 2xl:px-20 3xl:px-32 py-5"
}}
>
{/* Logo */}
<NavbarBrand>
<motion.div
initial={{ y: '-3rem', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 1, ease: 'easeOut' }}
>
<Image
src="/logo0.svg"
alt="Logo"
width={160}
height={40}
priority
style={{
filter: isDark ? 'invert(1) brightness(1.2)' : 'none',
transition: 'filter 0.3s ease-out'
}}
/>
</motion.div>
</NavbarBrand>
{/* Center Menu */}
<NavbarContent className="hidden lg:flex gap-6" justify="center">
{/* Product */}
<NavbarItem className="product-menu-container">
<Button
variant="light"
className="font-bold text-sm select-none bg-transparent"
style={{ color: showProductMenu ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563') }}
endContent={
<ChevronDown
size={14}
color={showProductMenu ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563')}
style={{
transform: showProductMenu ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
}
onPress={() => {
setShowProductMenu(!showProductMenu);
setShowResourceMenu(false);
}}
>
{t('nav.product')}
</Button>
</NavbarItem>
{/* Ecosystem */}
<NavbarItem>
<Button
variant="light"
className="font-bold text-sm select-none bg-transparent"
style={{ color: isDark ? '#a1a1aa' : '#4b5563' }}
>
{t('nav.ecosystem')}
</Button>
</NavbarItem>
{/* Resource */}
<NavbarItem className="resource-menu-container">
<Button
variant="light"
className="font-bold text-sm select-none bg-transparent"
style={{ color: showResourceMenu ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563') }}
endContent={
<ChevronDown
size={14}
color={showResourceMenu ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563')}
style={{
transform: showResourceMenu ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
}
onPress={() => {
setShowResourceMenu(!showResourceMenu);
setShowProductMenu(false);
}}
>
{t('nav.resource')}
</Button>
</NavbarItem>
</NavbarContent>
{/* Right Content */}
<NavbarContent justify="end" className="gap-2 lg:gap-4">
{/* Launch App Button */}
<NavbarItem>
<motion.div
initial={{ y: '-3rem', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 1, ease: 'easeOut', delay: 0.2 }}
>
<Button
className="font-bold text-sm hover:scale-105 transition-transform duration-200 ease-out"
style={{
backgroundColor: isDark ? '#ffffff' : '#111827',
color: isDark ? '#0a0a0a' : '#fafafa',
borderRadius: '9999px',
padding: '10px 20px',
transition: 'background-color 0.3s ease-out, color 0.3s ease-out, transform 0.2s ease-out',
boxShadow: '0px 4px 6px -4px rgba(229,231,235,1), 0px 10px 15px -3px rgba(229,231,235,1)'
}}
endContent={
<ArrowRight size={16} color={isDark ? '#0a0a0a' : '#ffffff'} />
}
onPress={() => window.open('http://152.69.205.186:3010/market', '_blank')}
>
{t('nav.launchApp')}
</Button>
</motion.div>
</NavbarItem>
{/* Theme Toggle - desktop only */}
<NavbarItem className="hidden lg:flex">
<Button
isIconOnly
variant="light"
onPress={toggleTheme}
className="transition-colors"
>
<div className="relative w-5 h-5">
<AnimatePresence mode="wait" initial={false}>
{isDark ? (
<motion.div key="moon" className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, rotate: -90, scale: 0.5 }}
animate={{ opacity: 1, rotate: 0, scale: 1 }}
exit={{ opacity: 0, rotate: 90, scale: 0.5 }}
transition={{ duration: 0.3 }}
>
<Moon size={18} color="#a1a1aa" />
</motion.div>
) : (
<motion.div key="sun" className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, rotate: 90, scale: 0.5 }}
animate={{ opacity: 1, rotate: 0, scale: 1 }}
exit={{ opacity: 0, rotate: -90, scale: 0.5 }}
transition={{ duration: 0.3 }}
>
<Sun size={18} color="#4B5563" />
</motion.div>
)}
</AnimatePresence>
</div>
</Button>
</NavbarItem>
{/* Language Selector - desktop only */}
<NavbarItem className="hidden lg:flex">
<Dropdown
classNames={{
content: 'bg-bg-surface border-none shadow-lg min-w-[120px] w-[120px]'
}}
>
<DropdownTrigger>
<Button
variant="light"
className="font-bold text-sm text-text-secondary"
startContent={<Globe size={16} color={isDark ? '#a1a1aa' : '#4B5563'} />}
endContent={<ChevronDown size={12} color={isDark ? '#a1a1aa' : '#4B5563'} />}
>
{language === 'zh' ? '中文' : 'EN'}
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Language selection"
onAction={(key) => setLanguage(key as 'zh' | 'en')}
selectedKeys={new Set([language])}
classNames={{
list: "py-2"
}}
itemClasses={{
base: "py-3 flex items-center justify-center"
}}
>
<DropdownItem
key="zh"
className="text-text-primary hover:bg-bg-elevated text-center"
>
</DropdownItem>
<DropdownItem
key="en"
className="text-text-primary hover:bg-bg-elevated text-center"
>
English
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarItem>
{/* Hamburger - mobile only */}
<NavbarItem className="lg:hidden">
<Button
isIconOnly
variant="light"
onPress={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<AnimatePresence mode="wait" initial={false}>
{mobileMenuOpen ? (
<motion.div
key="close"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.15 }}
>
<X size={20} color={isDark ? '#a1a1aa' : '#4b5563'} />
</motion.div>
) : (
<motion.div
key="menu"
initial={{ opacity: 0, rotate: 90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: -90 }}
transition={{ duration: 0.15 }}
>
<Menu size={20} color={isDark ? '#a1a1aa' : '#4b5563'} />
</motion.div>
)}
</AnimatePresence>
</Button>
</NavbarItem>
</NavbarContent>
</HeroNavbar>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<>
{/* Backdrop — click outside to close */}
<motion.div
key="mobile-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="lg:hidden fixed inset-0 z-[45]"
onClick={() => {
setMobileMenuOpen(false);
setMobileProductOpen(false);
setMobileResourceOpen(false);
}}
/>
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="lg:hidden fixed left-0 right-0 z-[50]"
style={{
top: navBottom,
backgroundColor: isDark ? 'rgba(10, 10, 10, 0.97)' : 'rgba(255, 255, 255, 0.97)',
backdropFilter: 'blur(50px)',
outline: 'none',
border: 'none',
borderBottom: isDark ? '1px solid #27272a' : '1px solid #e5e7eb',
maxHeight: `calc(100vh - ${navBottom}px)`,
overflowY: 'auto'
}}
>
<div className="flex flex-col px-4 py-3">
{/* Product */}
<button
className="flex items-center justify-between w-full px-3 py-4 text-sm font-bold rounded-lg transition-colors"
style={{ color: mobileProductOpen ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563') }}
onClick={() => {
setMobileProductOpen(!mobileProductOpen);
setMobileResourceOpen(false);
}}
>
{t('nav.product')}
<ChevronDown
size={14}
color={mobileProductOpen ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563')}
style={{ transform: mobileProductOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s ease' }}
/>
</button>
{/* Product Accordion */}
<AnimatePresence>
{mobileProductOpen && (
<motion.div
key="product-accordion"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
style={{ overflow: 'hidden' }}
>
<div className="px-2 pb-3 flex flex-col gap-4">
{/* Core Yield Assets */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '核心收益资产' : 'Core Yield Assets'}
</span>
<div className="flex flex-col gap-2 mt-2">
<div className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer ${isDark ? 'bg-[#27272a]' : 'bg-[#F9FAFB]'}`}>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ background: isDark ? '#ffffff' : '#000000' }}>
<Rainbow size={16} color={isDark ? '#000000' : '#ffffff'} />
</div>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>AX-Fund</span>
<div className="px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#D1FAE5' }}>
<span className="text-[9px] font-bold uppercase font-domine" style={{ color: '#047857' }}>
{language === 'zh' ? '最高22% APY' : 'up to 22% APY'}
</span>
</div>
</div>
</div>
<div className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer ${isDark ? 'bg-[#27272a]' : 'bg-[#F9FAFB]'}`}>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ background: isDark ? '#27272a' : '#ffffff', outline: `1px solid ${isDark ? '#3f3f46' : '#E5E7EB'}`, outlineOffset: '-1px' }}>
<Layers size={16} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>AX-Pool</span>
<div className="px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: isDark ? '#3f3f46' : '#E5E7EB' }}>
<span className={`text-[9px] font-bold uppercase font-domine ${isDark ? 'text-[#a1a1aa]' : 'text-[#4B5563]'}`}>
{language === 'zh' ? '多元化' : 'Diversified'}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Platforms & Protocols */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '平台与协议' : 'Platforms & Protocols'}
</span>
<div className="flex flex-col gap-0.5 mt-2">
{[
{ Icon: Rocket, titleZh: 'Launchpad', titleEn: 'Launchpad' },
{ Icon: ArrowLeftRight, titleZh: 'DeFi市场', titleEn: 'DeFi Market' },
{ Icon: Coins, titleZh: 'Token Factory', titleEn: 'Token Factory' },
].map(({ Icon, titleZh, titleEn }) => (
<div key={titleEn} className={`flex items-center gap-3 px-2 py-2.5 rounded-lg cursor-pointer transition-colors ${isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'}`}>
<div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'}`} style={{ outlineOffset: '-1px' }}>
<Icon size={14} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{language === 'zh' ? titleZh : titleEn}
</span>
</div>
))}
</div>
</div>
{/* Infrastructure */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '基础设施' : 'Infrastructure'}
</span>
<div className="flex flex-col gap-0.5 mt-2">
{[
{ Icon: LayoutDashboard, titleZh: 'Asset Cockpit', titleEn: 'Asset Cockpit' },
{ Icon: Radio, titleZh: 'Oracle Network', titleEn: 'Oracle Network' },
].map(({ Icon, titleZh, titleEn }) => (
<div key={titleEn} className={`flex items-center gap-3 px-2 py-2.5 rounded-lg cursor-pointer transition-colors ${isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'}`}>
<div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'}`} style={{ outlineOffset: '-1px' }}>
<Icon size={14} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{language === 'zh' ? titleZh : titleEn}
</span>
</div>
))}
</div>
</div>
{/* Bottom */}
<div className={`pt-2 border-t flex items-center justify-between ${isDark ? 'border-[#27272a]' : 'border-[#F3F4F6]'}`}>
<span className="text-xs font-domine text-[#9CA3AF]">
{language === 'zh' ? '最新审计:' : 'Latest Audit: '}
<span className={`font-medium ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>Oct 2025 (Certik)</span>
</span>
<span className="text-xs font-bold uppercase font-domine cursor-pointer hover:opacity-70 tracking-[0.3px]" style={{ color: '#059669' }}>
{language === 'zh' ? '查看文档 →' : 'View Docs →'}
</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div style={{ height: '1px', backgroundColor: isDark ? '#27272a' : '#f3f4f6' }} />
{/* Ecosystem */}
<button
className="flex items-center w-full px-3 py-4 text-sm font-bold rounded-lg transition-colors"
style={{ color: isDark ? '#a1a1aa' : '#4b5563' }}
onClick={() => setMobileMenuOpen(false)}
>
{t('nav.ecosystem')}
</button>
<div style={{ height: '1px', backgroundColor: isDark ? '#27272a' : '#f3f4f6' }} />
{/* Resource */}
<button
className="flex items-center justify-between w-full px-3 py-4 text-sm font-bold rounded-lg transition-colors"
style={{ color: mobileResourceOpen ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563') }}
onClick={() => {
setMobileResourceOpen(!mobileResourceOpen);
setMobileProductOpen(false);
}}
>
{t('nav.resource')}
<ChevronDown
size={14}
color={mobileResourceOpen ? '#059669' : (isDark ? '#a1a1aa' : '#4b5563')}
style={{ transform: mobileResourceOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s ease' }}
/>
</button>
{/* Resource Accordion */}
<AnimatePresence>
{mobileResourceOpen && (
<motion.div
key="resource-accordion"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
style={{ overflow: 'hidden' }}
>
<div className="px-2 pb-3 flex flex-col gap-4">
{/* Documentation & Learning */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '文档与学习' : 'Documentation & Learning'}
</span>
<div className="flex flex-col gap-2 mt-2">
<div className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer ${isDark ? 'bg-[#27272a]' : 'bg-[#F9FAFB]'}`}>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ background: isDark ? '#ffffff' : '#000000' }}>
<BookOpen size={16} color={isDark ? '#000000' : '#ffffff'} />
</div>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{language === 'zh' ? '文档' : 'Docs'}
</span>
<div className="px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#D1FAE5' }}>
<span className="text-[9px] font-bold uppercase font-domine" style={{ color: '#047857' }}>
{language === 'zh' ? '已更新' : 'Updated'}
</span>
</div>
</div>
</div>
<div className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer ${isDark ? 'bg-[#27272a]' : 'bg-[#F9FAFB]'}`}>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ background: isDark ? '#052e16' : '#ffffff', outline: '2px solid #10B981', outlineOffset: '-2px' }}>
<ShieldCheck size={16} color="#059669" />
</div>
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{language === 'zh' ? '信任与安全' : 'Trust & Security'}
</span>
</div>
</div>
</div>
{/* Help & Support */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '帮助与支持' : 'Help & Support'}
</span>
<div className="flex flex-col gap-0.5 mt-2">
{[
{ Icon: GraduationCap, titleZh: '学习中心', titleEn: 'Learning Center' },
{ Icon: MessageCircle, titleZh: '社区论坛', titleEn: 'Community Forum' },
{ Icon: Headphones, titleZh: '联系支持', titleEn: 'Contact Support' },
].map(({ Icon, titleZh, titleEn }) => (
<div key={titleEn} className={`flex items-center gap-3 px-2 py-2.5 rounded-lg cursor-pointer transition-colors ${isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'}`}>
<div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'}`} style={{ outlineOffset: '-1px' }}>
<Icon size={14} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{language === 'zh' ? titleZh : titleEn}
</span>
</div>
))}
</div>
</div>
{/* Company */}
<div>
<span className={`text-[10px] font-bold uppercase tracking-[1.2px] font-domine px-1 ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{language === 'zh' ? '公司' : 'Company'}
</span>
<div className="flex flex-col gap-0.5 mt-2">
{[
{ Icon: Users, titleZh: '关于团队', titleEn: 'About Team' },
{ Icon: Briefcase, titleZh: '招聘', titleEn: 'Careers' },
{ Icon: Mail, titleZh: '联系我们', titleEn: 'Contact Us' },
{ Icon: Newspaper, titleZh: '新闻媒体', titleEn: 'Press & Media' },
].map(({ Icon, titleZh, titleEn }) => (
<div key={titleEn} className={`flex items-center gap-3 px-2 py-2.5 rounded-lg cursor-pointer transition-colors ${isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'}`}>
<div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'}`} style={{ outlineOffset: '-1px' }}>
<Icon size={14} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<span className={`text-sm font-bold font-domine ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{language === 'zh' ? titleZh : titleEn}
</span>
</div>
))}
</div>
</div>
{/* Bottom */}
<div className={`pt-2 border-t flex items-center justify-between ${isDark ? 'border-[#27272a]' : 'border-[#F3F4F6]'}`}>
<span className="text-xs font-domine text-[#9CA3AF]">
{language === 'zh' ? '最新更新:' : 'Latest Update: '}
<span className={`font-medium ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>December 2025</span>
</span>
<div className="flex gap-4">
<span className={`text-xs font-bold font-domine cursor-pointer ${isDark ? 'text-[#6b7280]' : 'text-[#6B7280]'}`}>
{language === 'zh' ? '隐私政策' : 'Privacy Policy'}
</span>
<span className={`text-xs font-bold font-domine cursor-pointer ${isDark ? 'text-[#6b7280]' : 'text-[#6B7280]'}`}>
{language === 'zh' ? '服务条款' : 'Terms of Service'}
</span>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Theme + Language */}
<div style={{ height: '1px', backgroundColor: isDark ? '#27272a' : '#f3f4f6', margin: '4px 0' }} />
<div className="flex items-center gap-2 px-3 py-3">
{/* Theme Toggle */}
<Button
isIconOnly
variant="light"
onPress={toggleTheme}
>
<div className="relative w-5 h-5">
<AnimatePresence mode="wait" initial={false}>
{isDark ? (
<motion.div key="moon" className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, rotate: -90, scale: 0.5 }} animate={{ opacity: 1, rotate: 0, scale: 1 }} exit={{ opacity: 0, rotate: 90, scale: 0.5 }} transition={{ duration: 0.3 }}>
<Moon size={18} color="#a1a1aa" />
</motion.div>
) : (
<motion.div key="sun" className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, rotate: 90, scale: 0.5 }} animate={{ opacity: 1, rotate: 0, scale: 1 }} exit={{ opacity: 0, rotate: -90, scale: 0.5 }} transition={{ duration: 0.3 }}>
<Sun size={18} color="#4B5563" />
</motion.div>
)}
</AnimatePresence>
</div>
</Button>
{/* Language */}
<Dropdown classNames={{ content: 'bg-bg-surface border-none shadow-lg min-w-[120px] w-[120px]' }}>
<DropdownTrigger>
<Button variant="light" className="font-bold text-sm text-text-secondary"
startContent={<Globe size={16} color={isDark ? '#a1a1aa' : '#4B5563'} />}
endContent={<ChevronDown size={12} color={isDark ? '#a1a1aa' : '#4B5563'} />}
>
{language === 'zh' ? '中文' : 'EN'}
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Language selection" onAction={(key) => setLanguage(key as 'zh' | 'en')} selectedKeys={new Set([language])} classNames={{ list: "py-2" }} itemClasses={{ base: "py-3 flex items-center justify-center" }}>
<DropdownItem key="zh" className="text-text-primary hover:bg-bg-elevated text-center"></DropdownItem>
<DropdownItem key="en" className="text-text-primary hover:bg-bg-elevated text-center">English</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
{/* Custom Menus */}
<ProductMenu
isOpen={showProductMenu}
onClose={() => setShowProductMenu(false)}
language={language}
top={navBottom}
/>
<ResourceMenu
isOpen={showResourceMenu}
onClose={() => setShowResourceMenu(false)}
language={language}
top={navBottom}
/>
</>
);
}

View File

@@ -0,0 +1,274 @@
'use client';
import Image from 'next/image';
import { AnimatePresence, motion } from 'framer-motion';
import { useTheme } from '@/contexts/ThemeContext';
import { Layers, Rocket, ArrowLeftRight, Coins, LayoutDashboard, Radio, Rainbow } from 'lucide-react';
interface ProductMenuProps {
isOpen: boolean;
onClose: () => void;
language: 'zh' | 'en';
top?: number;
}
export default function ProductMenu({ isOpen, onClose, language, top = 64 }: ProductMenuProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const content = {
zh: {
coreYieldAssets: '核心收益资产',
axFund: 'AX-Fund',
axFundDesc: '购买AX-Fund收益代币获得市场中性收益策略的敞口。收益随时间在代币价格中累积。',
axFundBadge: '最高22% APY',
axPool: 'AX-Pool',
axPoolDesc: '以ALP提供流动性支持收益代币市场并赚取交易费',
axPoolBadge: '多元化',
platformsProtocols: '平台与协议',
launchpad: 'Launchpad',
launchpadDesc: '发行与代币化新RWA',
defiMarket: 'DeFi市场',
defiMarketDesc: '资产代币的交换与借贷',
tokenFactory: 'Token Factory',
tokenFactoryDesc: '标准化铸造协议',
infrastructure: '基础设施',
assetCockpit: 'Asset Cockpit',
assetCockpitDesc: '投资组合分析与管理',
oracleNetwork: 'Oracle Network',
oracleNetworkDesc: '实时链下数据源',
latestAudit: '最新审计:',
auditInfo: 'Oct 2025 (Certik)',
viewDocs: '查看文档 →',
},
en: {
coreYieldAssets: 'Core Yield Assets',
axFund: 'AX-Fund',
axFundDesc: 'Buy the AX-Fund Yield Token to gain exposure to market-neutral yield strategies. Yield accrues in token price over time.',
axFundBadge: 'up to 22% APY',
axPool: 'AX-Pool',
axPoolDesc: 'Provide liquidity as ALP to support Yield Token markets and earn trading fees',
axPoolBadge: 'Diversified',
platformsProtocols: 'Platforms & Protocols',
launchpad: 'Launchpad',
launchpadDesc: 'Issue & tokenize new RWAs.',
defiMarket: 'Defi Market',
defiMarketDesc: 'Swap & Lending for assets tokens.',
tokenFactory: 'Token Factory',
tokenFactoryDesc: 'Standardized minting protocol.',
infrastructure: 'Infrastructure',
assetCockpit: 'Asset Cockpit',
assetCockpitDesc: 'Portfolio analytics & mgmt.',
oracleNetwork: 'Oracle Network',
oracleNetworkDesc: 'Real-time off-chain data feeds.',
latestAudit: 'Latest Audit:',
auditInfo: 'Oct 2025 (Certik)',
viewDocs: 'View Documentation →',
},
};
const t = content[language];
const platformItems = [
{ Icon: Rocket, title: t.launchpad, desc: t.launchpadDesc },
{ Icon: ArrowLeftRight, title: t.defiMarket, desc: t.defiMarketDesc },
{ Icon: Coins, title: t.tokenFactory, desc: t.tokenFactoryDesc },
];
const infraItems = [
{ Icon: LayoutDashboard, title: t.assetCockpit, desc: t.assetCockpitDesc },
{ Icon: Radio, title: t.oracleNetwork, desc: t.oracleNetworkDesc },
];
return (
<AnimatePresence>
{isOpen && (
<>
{/* Transparent backdrop for click-outside */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40"
onClick={onClose}
/>
{/* Menu — centered card */}
<motion.div
initial={{ opacity: 0, y: -8, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: -8, x: '-50%' }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className={`fixed left-1/2 z-50 rounded-2xl border transition-colors ${
isDark ? 'bg-[#18181b] border-[#27272a]' : 'bg-white border-[#E5E7EB]'
}`}
style={{ top: top, width: '960px', boxShadow: '0px 20px 50px rgba(0,0,0,0.10)' }}
>
<div style={{ padding: '32px' }}>
{/* Three-column layout */}
<div className="flex gap-12">
{/* Left — Core Yield Assets (large cards) */}
<div className="flex flex-col gap-6 flex-shrink-0" style={{ width: '345px' }}>
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.coreYieldAssets}
</span>
{/* AX-Fund card */}
<div
className={`p-5 rounded-2xl cursor-pointer transition-opacity hover:opacity-75 ${
isDark
? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]'
: 'bg-[#F9FAFB] outline outline-1 outline-[#E5E7EB]'
}`}
style={{ outlineOffset: '-1px' }}
>
<div className="flex items-start gap-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"
style={{ background: isDark ? '#ffffff' : '#000000' }}
>
<Rainbow size={24} color={isDark ? '#000000' : '#ffffff'} />
</div>
<div className="flex flex-col gap-1 flex-1">
<div className="flex items-center gap-2">
<span className={`font-bold text-lg font-domine leading-7 ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{t.axFund}
</span>
<div className="px-2 py-0.5 rounded flex items-center" style={{ background: '#D1FAE5' }}>
<span className="text-[10px] font-bold uppercase font-domine leading-none" style={{ color: '#047857' }}>
{t.axFundBadge}
</span>
</div>
</div>
<p className="text-sm font-domine text-[#6B7280] leading-relaxed">{t.axFundDesc}</p>
</div>
</div>
</div>
{/* AX-Pool card */}
<div
className={`p-5 rounded-2xl cursor-pointer transition-opacity hover:opacity-75 ${
isDark
? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]'
: 'bg-[#F9FAFB] outline outline-1 outline-[#E5E7EB]'
}`}
style={{ outlineOffset: '-1px' }}
>
<div className="flex items-start gap-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"
style={{
background: isDark ? '#27272a' : '#ffffff',
outline: `1px solid ${isDark ? '#3f3f46' : '#E5E7EB'}`,
outlineOffset: '-1px',
}}
>
<Layers size={24} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex flex-col gap-1 flex-1">
<div className="flex items-center gap-2">
<span className={`font-bold text-lg font-domine leading-7 ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{t.axPool}
</span>
<div className="px-2 py-0.5 rounded flex items-center" style={{ background: isDark ? '#3f3f46' : '#E5E7EB' }}>
<span className={`text-[10px] font-bold uppercase font-domine leading-none ${isDark ? 'text-[#a1a1aa]' : 'text-[#4B5563]'}`}>
{t.axPoolBadge}
</span>
</div>
</div>
<p className="text-sm font-domine text-[#6B7280] leading-relaxed">{t.axPoolDesc}</p>
</div>
</div>
</div>
</div>
{/* Middle — Platforms & Protocols */}
<div className="flex flex-col gap-6 flex-shrink-0" style={{ width: '227px' }}>
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.platformsProtocols}
</span>
<div className="flex flex-col gap-6">
{platformItems.map(({ Icon, title, desc }) => (
<div
key={title}
className={`flex items-start gap-4 cursor-pointer rounded-lg p-2 -mx-2 transition-colors ${
isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'
}`}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'
}`}
style={{ outlineOffset: '-1px' }}
>
<Icon size={20} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex flex-col gap-0.5">
<span className={`text-sm font-bold font-domine leading-5 ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{title}
</span>
<span className="text-xs font-domine text-[#6B7280] leading-4">{desc}</span>
</div>
</div>
))}
</div>
</div>
{/* Right — Infrastructure */}
<div className="flex flex-col gap-6 flex-1">
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.infrastructure}
</span>
<div className="flex flex-col gap-6">
{infraItems.map(({ Icon, title, desc }) => (
<div
key={title}
className={`flex items-start gap-4 cursor-pointer rounded-lg p-2 -mx-2 transition-colors ${
isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'
}`}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'
}`}
style={{ outlineOffset: '-1px' }}
>
<Icon size={20} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex flex-col gap-0.5">
<span className={`text-sm font-bold font-domine leading-5 ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{title}
</span>
<span className="text-xs font-domine text-[#6B7280] leading-4">{desc}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bottom bar */}
<div className={`mt-8 pt-6 border-t flex items-center justify-between ${isDark ? 'border-[#27272a]' : 'border-[#F3F4F6]'}`}>
<div className="text-xs font-domine">
<span className="text-[#9CA3AF]">{t.latestAudit} </span>
<span className={`font-medium ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>{t.auditInfo}</span>
</div>
<span
className="text-xs font-bold uppercase font-domine cursor-pointer hover:opacity-70 transition-opacity tracking-[0.3px]"
style={{ color: '#059669' }}
>
{t.viewDocs}
</span>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { HeroUIProvider } from '@heroui/react';
import { LanguageProvider } from '@/contexts/LanguageContext';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { ReactNode } from 'react';
export default function Providers({ children }: { children: ReactNode }) {
return (
<HeroUIProvider>
<ThemeProvider>
<LanguageProvider>
{children}
</LanguageProvider>
</ThemeProvider>
</HeroUIProvider>
);
}

View File

@@ -0,0 +1,280 @@
'use client';
import { AnimatePresence, motion } from 'framer-motion';
import { useTheme } from '@/contexts/ThemeContext';
import { BookOpen, ShieldCheck, GraduationCap, MessageCircle, Headphones, Users, Briefcase, Mail, Newspaper } from 'lucide-react';
interface ResourceMenuProps {
isOpen: boolean;
onClose: () => void;
language: 'zh' | 'en';
top?: number;
}
export default function ResourceMenu({ isOpen, onClose, language, top = 64 }: ResourceMenuProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const content = {
zh: {
docLearning: '文档与学习',
docs: '文档',
docsDesc: '阅读技术文档、智能合约参考和集成指南。',
docsBadge: '已更新',
trustSecurity: '信任与安全',
trustSecurityDesc: '查看审计、合规认证和我们的安全实践。',
helpSupport: '帮助与支持',
learningCenter: '学习中心',
learningCenterDesc: '教程与视频指南。',
communityForum: '社区论坛',
communityForumDesc: '提问与交流。',
contactSupport: '联系支持',
contactSupportDesc: '从我们的团队获得帮助。',
company: '公司',
aboutTeam: '关于团队',
aboutTeamDesc: '认识AssetX背后的团队。',
careers: '招聘',
careersDesc: '加入我们不断成长的团队。',
contactUs: '联系我们',
contactUsDesc: '合作与媒体咨询。',
pressMedia: '新闻媒体',
pressMediaDesc: '最新消息与品牌资产。',
latestUpdate: '最新更新:',
updateInfo: 'December 2025',
privacyPolicy: '隐私政策',
termsOfService: '服务条款',
},
en: {
docLearning: 'Documentation & Learning',
docs: 'Docs',
docsDesc: 'Read technical docs, smart contract references, and integration guides.',
docsBadge: 'Updated',
trustSecurity: 'Trust & Security',
trustSecurityDesc: 'Review audits, compliance attestations, and our security practices.',
helpSupport: 'Help & Support',
learningCenter: 'Learning Center',
learningCenterDesc: 'Tutorials & video guides.',
communityForum: 'Community Forum',
communityForumDesc: 'Ask questions & connect.',
contactSupport: 'Contact Support',
contactSupportDesc: 'Get help from our team.',
company: 'Company',
aboutTeam: 'About Team',
aboutTeamDesc: 'Meet the people behind AssetX.',
careers: 'Careers',
careersDesc: 'Join our growing team.',
contactUs: 'Contact Us',
contactUsDesc: 'Partnerships & media inquiries.',
pressMedia: 'Press & Media',
pressMediaDesc: 'Latest news & brand assets.',
latestUpdate: 'Latest Update:',
updateInfo: 'December 2025',
privacyPolicy: 'Privacy Policy',
termsOfService: 'Terms of Service',
},
};
const t = content[language];
const helpItems = [
{ Icon: GraduationCap, title: t.learningCenter, desc: t.learningCenterDesc },
{ Icon: MessageCircle, title: t.communityForum, desc: t.communityForumDesc },
{ Icon: Headphones, title: t.contactSupport, desc: t.contactSupportDesc },
];
const companyItems = [
{ Icon: Users, title: t.aboutTeam, desc: t.aboutTeamDesc },
{ Icon: Briefcase, title: t.careers, desc: t.careersDesc },
{ Icon: Mail, title: t.contactUs, desc: t.contactUsDesc },
{ Icon: Newspaper, title: t.pressMedia, desc: t.pressMediaDesc },
];
return (
<AnimatePresence>
{isOpen && (
<>
{/* Transparent backdrop for click-outside */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40"
onClick={onClose}
/>
{/* Menu — centered card */}
<motion.div
initial={{ opacity: 0, y: -8, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: -8, x: '-50%' }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className={`fixed left-1/2 z-50 rounded-2xl border transition-colors ${
isDark ? 'bg-[#18181b] border-[#27272a]' : 'bg-white border-[#E5E7EB]'
}`}
style={{ top: top, width: '960px', boxShadow: '0px 20px 50px rgba(0,0,0,0.10)' }}
>
<div>
<div style={{ padding: '32px' }}>
{/* Three-column layout */}
<div className="flex gap-12">
{/* Left — Documentation & Learning (large cards) */}
<div className="flex flex-col gap-6 flex-shrink-0" style={{ width: '345px' }}>
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.docLearning}
</span>
{/* Docs card */}
<div
className={`p-5 rounded-2xl cursor-pointer transition-opacity hover:opacity-75 ${
isDark
? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]'
: 'bg-[#F9FAFB] outline outline-1 outline-[#E5E7EB]'
}`}
style={{ outlineOffset: '-1px' }}
>
<div className="flex items-start gap-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"
style={{ background: isDark ? '#ffffff' : '#000000' }}
>
<BookOpen size={24} color={isDark ? '#000000' : '#ffffff'} />
</div>
<div className="flex flex-col gap-1 flex-1">
<div className="flex items-center gap-2">
<span className={`font-bold text-lg font-domine leading-7 ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{t.docs}
</span>
<div className="px-2 py-0.5 rounded flex items-center" style={{ background: '#D1FAE5' }}>
<span className="text-[10px] font-bold uppercase font-domine leading-none" style={{ color: '#047857' }}>
{t.docsBadge}
</span>
</div>
</div>
<p className="text-sm font-domine text-[#6B7280] leading-relaxed">{t.docsDesc}</p>
</div>
</div>
</div>
{/* Trust & Security card */}
<div
className={`p-5 rounded-2xl cursor-pointer transition-opacity hover:opacity-75 ${
isDark
? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]'
: 'bg-[#F9FAFB] outline outline-1 outline-[#E5E7EB]'
}`}
style={{ outlineOffset: '-1px' }}
>
<div className="flex items-start gap-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"
style={{
background: isDark ? '#052e16' : '#ffffff',
outline: '2px solid #10B981',
outlineOffset: '-2px',
}}
>
<ShieldCheck size={24} color="#059669" />
</div>
<div className="flex flex-col gap-1 flex-1">
<span className={`font-bold text-lg font-domine leading-7 ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}>
{t.trustSecurity}
</span>
<p className="text-sm font-domine text-[#6B7280] leading-relaxed">{t.trustSecurityDesc}</p>
</div>
</div>
</div>
</div>
{/* Middle — Help & Support */}
<div className="flex flex-col gap-6 flex-shrink-0" style={{ width: '227px' }}>
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.helpSupport}
</span>
<div className="flex flex-col gap-6">
{helpItems.map(({ Icon, title, desc }) => (
<div
key={title}
className={`flex items-start gap-4 cursor-pointer rounded-lg p-2 -mx-2 transition-colors ${
isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'
}`}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'
}`}
style={{ outlineOffset: '-1px' }}
>
<Icon size={20} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex flex-col gap-0.5">
<span className={`text-sm font-bold font-domine leading-5 ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{title}
</span>
<span className="text-xs font-domine text-[#6B7280] leading-4">{desc}</span>
</div>
</div>
))}
</div>
</div>
{/* Right — Company */}
<div className="flex flex-col gap-6 flex-1">
<span className={`text-xs font-bold uppercase tracking-[1.2px] font-domine ${isDark ? 'text-[#52525b]' : 'text-[#9CA3AF]'}`}>
{t.company}
</span>
<div className="flex flex-col gap-6">
{companyItems.map(({ Icon, title, desc }) => (
<div
key={title}
className={`flex items-start gap-4 cursor-pointer rounded-lg p-2 -mx-2 transition-colors ${
isDark ? 'hover:bg-[#27272a]' : 'hover:bg-[#F9FAFB]'
}`}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-[#27272a] outline outline-1 outline-[#3f3f46]' : 'bg-[#F9FAFB] outline outline-1 outline-[#F3F4F6]'
}`}
style={{ outlineOffset: '-1px' }}
>
<Icon size={20} color={isDark ? '#d1d5db' : '#111827'} />
</div>
<div className="flex flex-col gap-0.5">
<span className={`text-sm font-bold font-domine leading-5 ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>
{title}
</span>
<span className="text-xs font-domine text-[#6B7280] leading-4">{desc}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bottom bar */}
<div className={`mt-8 pt-6 border-t flex items-center justify-between ${isDark ? 'border-[#27272a]' : 'border-[#F3F4F6]'}`}>
<div className="text-xs font-domine">
<span className="text-[#9CA3AF]">{t.latestUpdate} </span>
<span className={`font-medium ${isDark ? 'text-[#fafafa]' : 'text-black'}`}>{t.updateInfo}</span>
</div>
<div className="flex gap-6">
<span className={`text-xs font-bold font-domine cursor-pointer transition-colors ${isDark ? 'text-[#6b7280] hover:text-[#fafafa]' : 'text-[#6B7280] hover:text-[#111827]'}`}>
{t.privacyPolicy}
</span>
<span className={`text-xs font-bold font-domine cursor-pointer transition-colors ${isDark ? 'text-[#6b7280] hover:text-[#fafafa]' : 'text-[#6B7280] hover:text-[#111827]'}`}>
{t.termsOfService}
</span>
</div>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,247 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { Card, CardBody } from '@heroui/react';
import { SearchCheck, Landmark, BarChart2, ArrowRight, LucideIcon } from 'lucide-react';
import { useLanguage, TranslationKey } from '@/contexts/LanguageContext';
const features: { Icon: LucideIcon; titleKey: TranslationKey; descKey: TranslationKey | null; position: string; special?: boolean }[] = [
{
Icon: SearchCheck,
titleKey: 'security.audited.title',
descKey: 'security.audited.desc',
position: 'top-left'
},
{
Icon: Landmark,
titleKey: 'security.segregated.title',
descKey: 'security.segregated.desc',
position: 'top-right'
},
{
Icon: BarChart2,
titleKey: 'security.transparency.title',
descKey: 'security.transparency.desc',
position: 'bottom-left'
},
{
Icon: ArrowRight,
titleKey: 'security.partners.title',
descKey: null,
position: 'bottom-right',
special: true
}
];
export default function SecuritySection() {
const { t } = useLanguage();
const [animate, setAnimate] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
const [cardHovers, setCardHovers] = useState<{[key: number]: {x: number, y: number} | null}>({});
useEffect(() => {
const currentRef = sectionRef.current;
if (!currentRef) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setAnimate(true);
observer.disconnect();
}
},
{ threshold: 0.3 }
);
observer.observe(currentRef);
return () => observer.disconnect();
}, []);
const handleCardMouseMove = (e: React.MouseEvent<HTMLDivElement>, index: number) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setCardHovers(prev => ({...prev, [index]: {x, y}}));
};
const handleCardMouseLeave = (index: number) => {
setCardHovers(prev => ({...prev, [index]: null}));
};
const getBorderGradients = (index: number) => {
const hover = cardHovers[index];
if (!hover) return null;
const { x, y } = hover;
const spreadRange = 20;
const horizontalGradient = `linear-gradient(to right,
transparent 0%,
rgba(136,136,136,0.3) ${Math.max(0, x - spreadRange)}%,
rgba(136,136,136,1) ${x}%,
rgba(136,136,136,0.3) ${Math.min(100, x + spreadRange)}%,
transparent 100%)`;
const verticalGradient = `linear-gradient(to bottom,
transparent 0%,
rgba(136,136,136,0.3) ${Math.max(0, y - spreadRange)}%,
rgba(136,136,136,1) ${y}%,
rgba(136,136,136,0.3) ${Math.min(100, y + spreadRange)}%,
transparent 100%)`;
// 根据鼠标位置判断显示哪些边,使用 opacity 平滑过渡
const topOpacity = y < 50 ? 1 : 0;
const leftOpacity = x < 50 ? 1 : 0;
const rightOpacity = x >= 50 ? 1 : 0;
const bottomOpacity = y >= 50 ? 1 : 0;
return { horizontalGradient, verticalGradient, topOpacity, leftOpacity, rightOpacity, bottomOpacity };
};
return (
<section
ref={sectionRef}
className="bg-black flex flex-col items-center justify-start self-stretch flex-shrink-0 w-full relative"
style={{
padding: '1px 0px 0px 0px',
gap: '40px'
}}
>
<div className="flex flex-col md:flex-row items-stretch justify-start flex-shrink-0 w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] relative">
{/* 标题区域 */}
<div
className={`flex flex-row items-center justify-center flex-shrink-0 w-full md:w-[459px] 2xl:w-[560px] 3xl:w-[700px] relative transition-all duration-700 ease-out border-b border-[#222222] px-6 py-12 md:py-0 md:pr-6 md:pl-[120px] 2xl:pl-[160px] 3xl:pl-[200px] ${
animate ? 'translate-x-0 opacity-100' : '-translate-x-12 opacity-0'
}`}
>
<div className="flex flex-col gap-6 items-start justify-start flex-1 relative">
<h2
className="text-[#fcfcfd] text-left relative self-stretch font-domine select-none"
style={{
fontSize: 'clamp(36px, 3.5vw, 80px)',
lineHeight: '120%',
letterSpacing: '-0.01em'
}}
>
{t('security.title')}
</h2>
<p
className="text-[#9ca1af] text-left relative flex items-center justify-start font-domine text-base select-none"
style={{
lineHeight: '150%'
}}
>
{t('security.subtitle')}
</p>
</div>
</div>
{/* 卡片区域移动端单列桌面端2x2 */}
<div className="w-full md:flex-1 grid grid-cols-1 md:grid-cols-2 gap-0 relative overflow-visible">
{features.map((feature, index) => {
const borders = getBorderGradients(index);
return (
<div
key={feature.titleKey}
className="relative group h-full overflow-visible"
style={{ zIndex: 0 }}
>
{/* 顶部边框光效 */}
<div
className="absolute left-0 right-0 h-[1px] z-10 transition-opacity duration-300"
style={{ top: '-1px', background: borders?.horizontalGradient, opacity: borders?.topOpacity ?? 0, pointerEvents: 'none' }}
/>
{/* 右边边框光效 */}
<div
className="absolute top-0 right-0 bottom-0 w-[1px] z-10 transition-opacity duration-300"
style={{ background: borders?.verticalGradient, opacity: borders?.rightOpacity ?? 0, pointerEvents: 'none' }}
/>
{/* 底部边框光效 */}
<div
className="absolute bottom-0 left-0 right-0 h-[1px] z-10 transition-opacity duration-300"
style={{ background: borders?.horizontalGradient, opacity: borders?.bottomOpacity ?? 0, pointerEvents: 'none' }}
/>
{/* 左边边框光效 */}
<div
className="absolute top-0 left-0 bottom-0 w-[1px] z-10 transition-opacity duration-300"
style={{ background: borders?.verticalGradient, opacity: borders?.leftOpacity ?? 0, pointerEvents: 'none' }}
/>
<Card
className={`rounded-none bg-black border-[#222222] transition-all duration-300 ${
animate ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
shadow="none"
style={{
borderStyle: 'solid',
borderWidth: '0px 0px 1px 0px',
height: '100%',
transitionDelay: animate ? `${index * 150}ms` : '0ms'
}}
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => handleCardMouseMove(e, index)}
onMouseLeave={() => handleCardMouseLeave(index)}
>
<CardBody className="p-8 md:p-[72px_48px] flex items-start justify-center overflow-hidden">
{feature.special ? (
<div className="flex flex-col gap-6 items-start justify-start self-stretch">
<div className="flex flex-col gap-4 items-start justify-start self-stretch">
<h3
className="text-[#fcfcfd] text-left relative self-stretch font-domine"
style={{ fontSize: 'clamp(18px, 1.5vw, 28px)', lineHeight: '140%' }}
>
{t(feature.titleKey)}
</h3>
<feature.Icon
size={24}
className="flex-shrink-0 text-[#fcfcfd] transition-transform duration-300 ease-out"
style={{
transform: cardHovers[index] ? 'translateX(6px)' : 'translateX(0px)'
}}
/>
</div>
</div>
) : (
<div className="flex flex-col gap-6 items-start justify-start self-stretch">
<feature.Icon size={32} className="flex-shrink-0 text-[#fcfcfd]" />
<div className="flex flex-col gap-2 items-start justify-start self-stretch">
<h3
className="text-[#fcfcfd] text-left relative self-stretch font-domine font-bold"
style={{
fontSize: 'clamp(18px, 1.5vw, 28px)',
lineHeight: '130%',
letterSpacing: '-0.005em'
}}
>
{t(feature.titleKey)}
</h3>
<p
className="text-[#9ca1af] text-left relative self-stretch font-domine text-sm"
style={{ lineHeight: '150%' }}
>
{feature.descKey && t(feature.descKey)}
</p>
</div>
</div>
)}
</CardBody>
</Card>
</div>
);
})}
</div>
</div>
<div className="flex-shrink-0 w-full max-w-[1200px] px-6 md:px-0" style={{ height: 'auto' }}>
<Image
src="/logo1.svg"
alt="Logo"
width={1200}
height={187}
className="w-full h-auto object-contain"
/>
</div>
</section>
);
}

View File

@@ -0,0 +1,139 @@
'use client';
import { motion } from 'framer-motion';
import { Card, CardBody, Avatar, AvatarGroup } from '@heroui/react';
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import { useCountUp } from '@/hooks/useCountUp';
// 格式化数字 - 确保始终显示正确的单位
function formatNumber(num: number, prefix: string = '', suffix: string = '', forceUnit: 'M' | 'K' | '' = '') {
if (forceUnit === 'M') {
return `${prefix}${Math.floor(num)}${suffix}`;
} else if (forceUnit === 'K') {
return `${prefix}${Math.floor(num)}${suffix}`;
} else if (num >= 1000000) {
return `${prefix}${Math.floor(num / 1000000)}M${suffix}`;
} else if (num >= 1000) {
return `${prefix}${Math.floor(num / 1000)}K${suffix}`;
}
return `${prefix}${num.toLocaleString()}${suffix}`;
}
export default function StatsSection() {
const { t } = useLanguage();
const { theme } = useTheme();
const tvl = useCountUp(485, 1500, 0.80);
const apy = useCountUp(515, 1500, 0.85);
const yield_ = useCountUp(45, 1500, 0.75);
const users = useCountUp(23928, 1800, 0.80);
const isDark = theme === 'dark';
return (
<section
className="w-full overflow-x-clip border-b transition-colors bg-bg-base border-border-normal flex-shrink-0 flex justify-center"
style={{ paddingLeft: 'clamp(16px, 4vw, 80px)', paddingRight: 'clamp(16px, 4vw, 80px)' }}
>
<div className="w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] grid grid-cols-2 md:grid-cols-4">
{/* TVL */}
<Card
className="rounded-none border-r border-b md:border-b-0 transition-colors bg-bg-base border-border-normal"
shadow="none"
>
<CardBody className="flex flex-col gap-2 items-start justify-center py-4 md:py-0 md:h-[182px] px-4 md:px-0">
<p className="font-domine transition-colors text-text-secondary" style={{ fontSize: 'clamp(14px, 1vw, 20px)' }}>
{t('stats.tvl')}
</p>
<div
ref={tvl.elementRef}
className="font-extrabold font-domine transition-colors text-text-primary"
style={{ fontSize: 'clamp(20px, 2.5vw, 48px)', lineHeight: '110%', letterSpacing: '-0.01em' }}
>
{formatNumber(tvl.count, '$', 'M+', 'M')}
</div>
</CardBody>
</Card>
{/* APY */}
<Card
className={`rounded-none border-b md:border-b-0 md:border-r transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
>
<CardBody className="flex flex-col gap-2 items-start justify-center py-4 md:py-0 md:h-[182px] px-4 md:px-8">
<p className="font-domine transition-colors text-text-secondary" style={{ fontSize: 'clamp(14px, 1vw, 20px)' }}>
{t('stats.apy')}
</p>
<div
ref={apy.elementRef}
className="font-extrabold font-domine transition-colors"
style={{ fontSize: 'clamp(20px, 2.5vw, 48px)', lineHeight: '110%', letterSpacing: '-0.01em', color: '#059669' }}
>
{(apy.count / 100).toFixed(2)}%
</div>
</CardBody>
</Card>
{/* Yield */}
<Card
className={`rounded-none border-r transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
>
<CardBody className="flex flex-col gap-2 items-start justify-center py-4 md:py-0 md:h-[182px] px-4 md:px-8">
<p className="font-domine transition-colors text-text-secondary" style={{ fontSize: 'clamp(14px, 1vw, 20px)' }}>
{t('stats.yield')}
</p>
<div
ref={yield_.elementRef}
className="font-extrabold font-domine transition-colors text-text-primary"
style={{ fontSize: 'clamp(20px, 2.5vw, 48px)', lineHeight: '110%', letterSpacing: '-0.01em' }}
>
{formatNumber(yield_.count, '$', 'M', 'M')}
</div>
</CardBody>
</Card>
{/* Users */}
<Card
className="rounded-none transition-colors bg-bg-base overflow-hidden"
shadow="none"
>
<CardBody className="flex flex-col gap-2 items-start justify-center py-4 md:py-0 md:h-[182px] px-4 md:pl-8 overflow-hidden">
<p className="font-domine transition-colors text-text-secondary" style={{ fontSize: 'clamp(14px, 1vw, 20px)' }}>
{t('stats.users')}
</p>
<div className="flex flex-row items-center gap-4">
<div
ref={users.elementRef}
className="text-xl md:text-2xl xl:text-4xl font-extrabold font-domine transition-colors text-text-primary"
style={{ lineHeight: '110%', letterSpacing: '-0.01em' }}
>
{users.count.toLocaleString()}+
</div>
<motion.div
className="hidden lg:block"
initial={{ y: '3rem', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.7, ease: 'easeOut', delay: 0.5 }}
>
<AvatarGroup max={4}>
<Avatar src="/image-230.png" size="sm" />
<Avatar src="/image-240.png" size="sm" />
<Avatar src="/image-250.png" size="sm" />
<Avatar src="/image-251.png" size="sm" />
</AvatarGroup>
</motion.div>
</div>
</CardBody>
</Card>
</div>
</section>
);
}

View File

@@ -0,0 +1,175 @@
'use client';
import { useRef, useEffect, useState } from 'react';
import Image from 'next/image';
import { motion, Variants } from 'framer-motion';
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import ConsenSysLogo from './ConsenSysLogo';
const itemVariants: Variants = {
hidden: { y: '3rem', opacity: 0 },
visible: (i: number) => ({
y: 0,
opacity: 1,
transition: { duration: 0.7, ease: 'easeOut' as const, delay: i * 0.1 }
})
};
export default function TrustedBySection() {
const { t } = useLanguage();
const { theme } = useTheme();
const isDark = theme === 'dark';
const scrollRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number>(0);
const isPaused = useRef(false);
const [isMounted, setIsMounted] = useState(false);
// 确保组件挂载后才启动滚动
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (!isMounted) return;
const el = scrollRef.current;
if (!el) return;
const speed = 1.2;
let lastTime = performance.now();
const tick = (currentTime: number) => {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
if (!isPaused.current && el) {
el.scrollLeft += speed * (deltaTime / 16); // 标准化到60fps
if (el.scrollLeft >= el.scrollWidth / 2) {
el.scrollLeft = 0;
}
}
rafRef.current = requestAnimationFrame(tick);
};
// 延迟启动,确保 DOM 完全渲染
const startTimer = setTimeout(() => {
rafRef.current = requestAnimationFrame(tick);
}, 100);
return () => {
clearTimeout(startTimer);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [isMounted]);
const logoClass = isDark
? 'brightness-0 invert opacity-50 group-hover:opacity-100'
: 'brightness-0 opacity-50 group-hover:opacity-100';
const partners = [
{ name: 'BlackRock', src: '/nav-ireland0.svg', width: 200, height: 40 },
{ name: 'Coinbase', src: '/coinbase-10.svg', width: 180, height: 40 },
{ name: 'Wintermute', src: '/wintermute0.svg', width: 247, height: 40 },
{ name: 'Circle', src: '/group0.svg', width: 156, height: 40 },
{ name: 'ConsenSys', src: '', width: 220, height: 50 },
];
const renderLogo = (partner: typeof partners[number], key: string) => (
<div
key={key}
className="relative flex items-center justify-center transition-all duration-300 group"
style={{ width: `${partner.width}px`, height: `${partner.height}px` }}
>
{partner.name === 'ConsenSys' ? (
<div className={`w-full h-full flex items-center justify-center transition-all duration-300 ${logoClass}`}>
<ConsenSysLogo width={partner.width} height={partner.height} />
</div>
) : (
<Image
src={partner.src}
alt={partner.name}
width={partner.width}
height={partner.height}
className={`w-full h-full object-contain transition-all duration-300 ${logoClass}`}
/>
)}
</div>
);
return (
<section
className="flex flex-col items-center justify-center w-full overflow-hidden border-b bg-bg-subtle border-border-normal py-10 gap-8"
style={{ paddingLeft: 'clamp(16px, 4vw, 80px)', paddingRight: 'clamp(16px, 4vw, 80px)' }}
>
<div className="text-lg font-medium text-center font-domine text-text-primary px-4">
{t('trusted.title')}
</div>
{/* ── 移动端:自动滚动 + 手动滑动 ── */}
<div
ref={scrollRef}
className="md:hidden w-full overflow-x-auto cursor-grab active:cursor-grabbing"
style={{
WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
onTouchStart={() => { isPaused.current = true; }}
onTouchEnd={() => { isPaused.current = false; }}
onTouchCancel={() => { isPaused.current = false; }}
>
<div className="flex items-center gap-10" style={{ width: 'max-content', padding: '0 24px' }}>
{[...partners, ...partners].map((partner, i) => (
<div
key={`${partner.name}-${i}`}
className="flex-shrink-0 flex items-center justify-center h-[60px]"
style={{ width: `${partner.width}px` }}
>
{renderLogo(partner, `m-${partner.name}-${i}`)}
</div>
))}
</div>
</div>
{/* ── 桌面端:单行不换行 ── */}
<div className="hidden md:flex w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] flex-row flex-nowrap items-center justify-between gap-x-6">
{partners.map((partner, index) => (
<motion.div
key={partner.name}
custom={index}
variants={itemVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative cursor-pointer group flex items-center justify-center"
style={{
maxWidth: `${partner.width}px`,
width: '100%',
height: `${partner.height}px`,
flexShrink: 1,
flexBasis: `${partner.width}px`,
minWidth: 60,
}}
>
{partner.name === 'ConsenSys' ? (
<div className={`w-full h-full flex items-center justify-center transition-all duration-300 ${logoClass}`}>
<ConsenSysLogo width={partner.width} height={partner.height} />
</div>
) : (
<Image
src={partner.src}
alt={partner.name}
width={partner.width}
height={partner.height}
className={`w-full h-full object-contain transition-all duration-300 ${logoClass}`}
/>
)}
</motion.div>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,256 @@
'use client';
import { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { Card, CardBody, Chip } from '@heroui/react';
import { TrendingUp, ShieldCheck, ArrowLeftRight, Lock } from 'lucide-react';
import { useLanguage } from '@/contexts/LanguageContext';
export default function WhyAssetXSection() {
const { t } = useLanguage();
const sectionRef = useRef<HTMLElement>(null);
const isInView = useInView(sectionRef, { once: true, amount: 0.2 });
return (
<section
ref={sectionRef}
className="flex flex-row items-center justify-center flex-wrap flex-shrink-0 w-full relative bg-bg-base"
style={{
padding: 'clamp(40px, 5vw, 100px) clamp(24px, 4vw, 80px)',
gap: 'clamp(24px, 3vw, 64px)',
alignContent: 'center'
}}
>
<div className="flex flex-col items-center justify-center w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px]">
<div className="flex flex-col gap-2 items-start justify-start flex-shrink-0 w-full relative mb-10">
<motion.h2
className="text-left relative font-domine font-bold text-text-primary"
style={{
fontSize: 'clamp(36px, 3.5vw, 80px)',
lineHeight: '120%',
letterSpacing: '-0.01em'
}}
initial={{ opacity: 0, y: 24 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{t('why.title')}
</motion.h2>
<motion.p
className="text-left relative font-domine text-text-tertiary"
style={{ fontSize: 'clamp(16px, 1.2vw, 22px)', lineHeight: '140%' }}
initial={{ opacity: 0, y: 24 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.15 }}
>
{t('why.subtitle')}
</motion.p>
</div>
<div className="flex flex-col md:flex-row gap-6 items-stretch justify-start w-full relative">
<motion.div
initial={{ opacity: 0, y: 32 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 32 }}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.2 }}
className="flex-1"
>
<Card
className="self-stretch hover:-translate-y-2 bg-bg-surface border-border-normal h-full"
shadow="sm"
style={{
transition: 'transform 0.3s ease-out, box-shadow 0.3s ease-out',
willChange: 'transform'
}}
>
<CardBody className="flex flex-col items-start justify-between p-6 md:p-10 gap-6">
<div className="flex flex-col gap-6 items-start justify-start self-stretch">
<div className="rounded-2xl flex items-center justify-center w-12 h-12 bg-bg-elevated">
<TrendingUp size={24} className="text-text-primary" />
</div>
<div className="flex flex-col gap-4 items-start justify-start self-stretch">
<h3 className="text-2xl font-bold font-domine text-text-primary"
style={{
lineHeight: '130%',
letterSpacing: '-0.005em'
}}
>
{t('why.sustainable.title')}
</h3>
<p className="text-base font-domine text-text-tertiary"
style={{
lineHeight: '150%'
}}
>
{t('why.sustainable.desc')}
</p>
</div>
</div>
<Image
src="/frame-110.svg"
alt="Chart"
width={305}
height={162}
className="w-full h-auto"
/>
</CardBody>
</Card>
</motion.div>
<div className="flex flex-col gap-6 items-start justify-start w-full md:flex-1 relative self-stretch">
<motion.div
initial={{ opacity: 0, y: 32 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 32 }}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.35 }}
className="w-full flex-1 flex flex-col"
>
<Card
className="self-stretch hover:-translate-y-2 bg-bg-surface border-border-normal flex-1"
shadow="sm"
style={{
transition: 'transform 0.3s ease-out, box-shadow 0.3s ease-out',
willChange: 'transform'
}}
>
<CardBody className="flex flex-col md:flex-row items-start md:items-center justify-between p-6 md:p-10 gap-6 md:gap-0">
<div className="flex flex-col gap-6 items-start justify-start w-full md:w-[414px]">
<div className="rounded-2xl flex items-center justify-center w-12 h-12 bg-bg-elevated">
<ShieldCheck size={24} className="text-text-primary" />
</div>
<div className="flex flex-col gap-4 items-start justify-start self-stretch">
<h3 className="text-2xl font-bold font-domine text-text-primary"
style={{
lineHeight: '130%',
letterSpacing: '-0.005em'
}}
>
{t('why.reliability.title')}
</h3>
<p className="text-base font-domine text-text-tertiary"
style={{
lineHeight: '150%'
}}
>
{t('why.reliability.desc')}
</p>
</div>
</div>
<div
className="relative bg-bg-elevated border border-border-strong rounded-2xl mx-auto md:mx-0"
style={{
width: '196px',
height: '180px',
flexShrink: 0
}}
>
{/* 外层圆环 */}
<div
className="absolute rounded-full border-2 border-border-strong"
style={{
width: '132px',
height: '132px',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
>
{/* 内层圆 */}
<div
className="absolute rounded-full bg-border-strong"
style={{
width: '88.59px',
height: '88.59px',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
>
{/* 图标 */}
<Lock
size={44}
className="text-white"
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
</div>
</div>
</div>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 32 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 32 }}
transition={{ duration: 0.6, ease: 'easeOut', delay: 0.5 }}
className="w-full flex-1 flex flex-col"
>
<Card
className="self-stretch hover:-translate-y-2 bg-bg-surface border-border-normal flex-1"
shadow="sm"
style={{
transition: 'transform 0.3s ease-out, box-shadow 0.3s ease-out',
willChange: 'transform'
}}
>
<CardBody className="flex flex-col md:flex-row items-start justify-between p-6 md:p-10 gap-6 md:gap-0">
<div className="flex flex-col gap-6 items-start justify-start w-full md:w-[550px]">
<div className="rounded-2xl flex items-center justify-center w-12 h-12 bg-bg-elevated">
<ArrowLeftRight size={24} className="text-text-primary" />
</div>
<div className="flex flex-col gap-4 items-start justify-start">
<h3 className="text-2xl font-bold font-domine text-text-primary"
style={{
lineHeight: '130%',
letterSpacing: '-0.005em'
}}
>
{t('why.liquidity.title')}
</h3>
<p className="text-base font-domine text-text-tertiary"
style={{
lineHeight: '150%'
}}
>
{t('why.liquidity.desc')}
</p>
</div>
</div>
<div className="flex flex-row gap-3 items-center justify-end">
<Chip
className="bg-accent-green-bg text-accent-green font-bold"
size="sm"
>
{t('why.liquidity.badge1')}
</Chip>
<Chip
className="bg-accent-blue-bg text-accent-blue font-bold"
size="sm"
>
{t('why.liquidity.badge2')}
</Chip>
</div>
</CardBody>
</Card>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,226 @@
'use client';
import { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';
type Language = 'zh' | 'en';
// 翻译内容
const zhTranslations = {
// Navbar
'nav.product': '产品',
'nav.resource': '资源',
'nav.community': '社区',
'nav.ecosystem': '生态系统',
'nav.launchApp': '启动应用',
// Hero Section
'hero.badge': '由纳斯达克及港交所上市合作伙伴支持',
'hero.title1': '链上收益资产',
'hero.title2': '触手可及。',
'hero.description': '低门槛投资股票、房地产和商业贷款等收益资产。享受真实资产支持的10%-30%年化收益。',
'hero.startInvesting': '开始投资',
'hero.readWhitepaper': '阅读白皮书',
'hero.readDocs': '阅读文档',
// Stats Section
'stats.tvl': '总锁仓价值',
'stats.apy': '平均年化收益',
'stats.yield': '累计收益',
'stats.users': '连接用户',
// Trusted By Section
'trusted.title': '受到行业领导者信赖',
// Why AssetX Section
'why.title': '为什么选择ASSETX?',
'why.subtitle': '机构级真实世界收益兼具DeFi原生可组合性。',
'why.sustainable.title': '可持续真实收益',
'why.sustainable.desc': '从Delta中性套利策略和商业信贷中获得15%-30%的回报。没有通胀代币发行,只有真实利润。',
'why.reliability.title': '经过验证的可靠性',
'why.reliability.desc': '由持有香港证监会1/2/4/5/9号牌照的合作伙伴和纳斯达克上市实体支持。经过审计、合规且透明。',
'why.liquidity.title': '灵活流动性',
'why.liquidity.desc': '解决传统金融的流动性不足问题。通过二级市场即时退出或通过杠杆头寸享受高达40%的年化收益。',
'why.liquidity.badge1': '40%+年化',
'why.liquidity.badge2': '即时退出',
// How It Works Section
'how.title': '如何运作',
'how.step1.title': '连接并探索',
'how.step1.desc': '浏览收益代币,比较收益/风险,查看关键指标。',
'how.step2.title': '用USDC投资',
'how.step2.desc': '用USDC购买收益代币赚取现实世界收益。',
'how.step3.title': '在DeFi中使用',
'how.step3.desc': '通过二级市场提前退出,以抵押品借款,或提供流动性赚取手续费。',
'how.simulator.title': '投资组合概览模拟器',
'how.simulator.invest': '如果您投资',
'how.simulator.earn': '您每年赚取',
// Security Section
'security.title': '安全优先架构',
'security.subtitle': '实时数据透明度与隔离资产管理。',
'security.audited.title': '已审计',
'security.audited.desc': '智能合约由顶级公司审计。底层资产经过严格的尽职调查和财务审计。',
'security.segregated.title': '隔离管理',
'security.segregated.desc': 'SPV设置。用户资产保存在隔离的特殊目的实体中与平台风险隔离。',
'security.transparency.title': '透明度',
'security.transparency.desc': '实时净值。基金表现数据和资产估值公开可查并在链上更新。',
'security.partners.title': '由纳斯达克和港交所上市合作伙伴提供支持',
// Footer
'footer.products': '产品',
'footer.product1': 'AX-基金',
'footer.product2': 'AX-矩阵',
'footer.product3': '启动平台',
'footer.product4': '流动性市场',
'footer.product5': '代币工厂',
'footer.resources': '资源',
'footer.resource1': '文档',
'footer.resource2': '常见问题与支持',
'footer.resource3': '信任与安全',
'footer.resource4': '学习中心',
'footer.resource5': '社区论坛',
'footer.company': '公司',
'footer.company1': '关于团队',
'footer.company2': '职业机会',
'footer.company3': '联系我们',
'footer.company4': '新闻媒体',
'footer.privacy': '隐私政策',
'footer.terms': '服务条款',
'footer.copyright': `© ${new Date().getFullYear()} ASSETX协议。保留所有权利。`,
} as const;
const enTranslations = {
// Navbar
'nav.product': 'Product',
'nav.resource': 'Resource',
'nav.community': 'Community',
'nav.ecosystem': 'Ecosystem',
'nav.launchApp': 'Launch App',
// Hero Section
'hero.badge': 'Powered By Nasdaq & HKEX Listed Partners',
'hero.title1': 'Yield-Bearing Assets',
'hero.title2': 'On Chain.',
'hero.description': 'Access Yield-Bearing Asset like Equities, Real Estate, and Commercial Loans with low barriers. Enjoy 10%-30% APY backed by real assets.',
'hero.startInvesting': 'Start Investing',
'hero.readWhitepaper': 'Read Whitepaper',
'hero.readDocs': 'Read Docs',
// Stats Section
'stats.tvl': 'Total Value Locked',
'stats.apy': 'Avg. APY',
'stats.yield': 'Yield Captured',
'stats.users': 'Connected Users',
// Trusted By Section
'trusted.title': 'Trusted by Industry Leaders',
// Why AssetX Section
'why.title': 'Why ASSETX?',
'why.subtitle': 'Institutional-grade access to real-world yield, with DeFi-native composability.',
'why.sustainable.title': 'Sustainable Real Yield',
'why.sustainable.desc': 'Access 15%-30% returns from Delta-neutral arbitrage strategies and commercial credit. No inflationary token emissions, just real profits.',
'why.reliability.title': 'Proven Reliability',
'why.reliability.desc': 'Backed by partners holding HK SFC Licenses (1/2/4/5/9) and NASDAQ-listed entities. Audited, compliant, and transparent.',
'why.liquidity.title': 'Flexible Liquidity',
'why.liquidity.desc': 'Solve the illiquidity of traditional finance. Enjoy instant exit via secondary markets or leverage your positions for up to 40% APY.',
'why.liquidity.badge1': '40%+ APR',
'why.liquidity.badge2': 'Instant Exit',
// How It Works Section
'how.title': 'How it works',
'how.step1.title': 'Connect & Explore',
'how.step1.desc': 'Browse Yield Tokens, compare yield/risk, and view key metrics.',
'how.step2.title': 'Invest with USDC',
'how.step2.desc': 'Buy Yield Tokens with USDC and earn real-world yield.',
'how.step3.title': 'Use in DeFi',
'how.step3.desc': 'Exit early via secondary markets, borrow against collateral, or provide liquidity to earn fees.',
'how.simulator.title': 'Portfolio Overview Simulator',
'how.simulator.invest': 'If you invest',
'how.simulator.earn': 'You earn per year',
// Security Section
'security.title': 'Security First Architecture',
'security.subtitle': 'Real-time data transparency with segregated asset management.',
'security.audited.title': 'Audited',
'security.audited.desc': 'Smart contracts audited by top-tier firms. Underlying assets undergo strict due diligence and financial auditing.',
'security.segregated.title': 'Segregated',
'security.segregated.desc': 'SPV Setup. User assets are held in segregated Special Purpose Vehicles, isolated from platform risks.',
'security.transparency.title': 'Transparency',
'security.transparency.desc': 'Real-Time NAV. Fund performance data and asset valuations are publicly available and updated on-chain.',
'security.partners.title': 'Powered By NASDAQ & HKEX Listed Partners',
// Footer
'footer.products': 'Products',
'footer.product1': 'AX-Fund',
'footer.product2': 'AX-Matrix',
'footer.product3': 'Launchpad',
'footer.product4': 'Liquid Market',
'footer.product5': 'Token Factory',
'footer.resources': 'Resources',
'footer.resource1': 'Docs',
'footer.resource2': 'FAQ & Support',
'footer.resource3': 'Trust & Security',
'footer.resource4': 'Learning Center',
'footer.resource5': 'Community Forum',
'footer.company': 'Company',
'footer.company1': 'About Team',
'footer.company2': 'Careers',
'footer.company3': 'Contact Us',
'footer.company4': 'Press & Media',
'footer.privacy': 'Privacy Policy',
'footer.terms': 'Terms of Service',
'footer.copyright': `© ${new Date().getFullYear()} ASSETX Protocol. All rights reserved.`,
} as const;
export type TranslationKey = keyof typeof enTranslations;
const translationMap: Record<Language, Record<TranslationKey, string>> = {
zh: zhTranslations,
en: enTranslations,
};
interface LanguageContextType {
language: Language;
setLanguage: (lang: Language) => void;
t: (key: TranslationKey) => string;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguageState] = useState<Language>('en');
const handleSetLanguage = useCallback((lang: Language) => {
setLanguageState(prev => {
if (lang === prev) return prev;
return lang;
});
}, []);
const translations = useMemo(() => translationMap[language], [language]);
const t = useCallback(
(key: TranslationKey): string => translations[key] || key,
[translations]
);
const value = useMemo(
() => ({ language, setLanguage: handleSetLanguage, t }),
[language, handleSetLanguage, t]
);
return (
<LanguageContext.Provider value={value}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
}

View File

@@ -0,0 +1,108 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
isSystemTheme: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// 纯函数,放在组件外避免闭包问题
function getSystemTheme(): Theme {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function isNightTime(): boolean {
if (typeof window === 'undefined') return false;
const hour = new Date().getHours();
return hour >= 18 || hour < 6;
}
function getAutoTheme(): Theme {
if (getSystemTheme() === 'dark') return 'dark';
if (isNightTime()) return 'dark';
return 'light';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>('light');
const [isSystemTheme, setIsSystemTheme] = useState(true);
const [mounted, setMounted] = useState(false);
// 设置主题(仅当前会话,不持久化)
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
setIsSystemTheme(false);
}, []);
// 切换主题
const toggleTheme = useCallback(() => {
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
setIsSystemTheme(false);
}, []);
// 初始化:始终跟随系统主题 + 时间
useEffect(() => {
setThemeState(getAutoTheme());
setIsSystemTheme(true);
setMounted(true);
}, []);
// 监听系统主题变化
useEffect(() => {
if (!isSystemTheme) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
setThemeState(getAutoTheme());
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [isSystemTheme]);
// 每分钟检查时间变化
useEffect(() => {
if (!isSystemTheme) return;
const interval = setInterval(() => {
setThemeState(getAutoTheme());
}, 60000);
return () => clearInterval(interval);
}, [isSystemTheme]);
// 在 DOM 上设置主题属性
useEffect(() => {
if (mounted) {
document.documentElement.setAttribute('data-theme', theme);
}
}, [theme, mounted]);
const value = useMemo(
() => ({ theme, toggleTheme, setTheme, isSystemTheme }),
[theme, toggleTheme, setTheme, isSystemTheme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -0,0 +1,44 @@
module.exports = {
apps: [
{
name: 'landingpage',
script: 'node_modules/.bin/next',
args: 'start -H 0.0.0.0 -p 3011',
cwd: '/home/coder/myprojects/assetx/landingpage',
interpreter: 'node',
instances: 1,
exec_mode: 'fork',
// 环境变量
env: {
NODE_ENV: 'production',
PORT: 3011,
},
// 自动重启配置
autorestart: true,
watch: false,
max_memory_restart: '500M',
// 日志配置
error_file: '~/.pm2/logs/landingpage-error.log',
out_file: '~/.pm2/logs/landingpage-out.log',
log_file: '~/.pm2/logs/landingpage-combined.log',
time: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
// 启动延迟(避免端口冲突)
listen_timeout: 10000,
kill_timeout: 5000,
// 最大重启次数(防止无限重启)
max_restarts: 10,
min_uptime: '10s',
// 其他配置
shutdown_with_message: true,
wait_ready: false,
}
]
};

View File

@@ -0,0 +1,83 @@
'use client';
import { useState, useEffect, useRef } from 'react';
/**
* 数字增长动画 Hook - 进入视口触发,离开视口重置
* 使用 requestAnimationFrame 保证与浏览器刷新同步
*/
export function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
const [mounted, setMounted] = useState(false);
const [count, setCount] = useState(end); // 初始值设为目标值,避免 hydration 错误
const elementRef = useRef<HTMLDivElement>(null);
const startValueRef = useRef<number>(end);
const rafRef = useRef<number | null>(null);
// 客户端挂载后设置随机起始值
useEffect(() => {
setMounted(true);
startValueRef.current = Math.floor(end * (startRangePercent + Math.random() * 0.15));
setCount(startValueRef.current);
}, [end, startRangePercent]);
useEffect(() => {
if (!mounted) return;
const cancelAnimation = () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
const startAnimation = () => {
cancelAnimation();
const start = startValueRef.current;
const startTime = performance.now();
const tick = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// easeOutCubic 缓动
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
const currentCount = Math.floor(start + (end - start) * easeOutCubic);
setCount(currentCount);
if (progress < 1) {
rafRef.current = requestAnimationFrame(tick);
} else {
setCount(end); // 确保最终值准确
rafRef.current = null;
}
};
rafRef.current = requestAnimationFrame(tick);
};
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
startAnimation();
} else {
cancelAnimation();
setCount(startValueRef.current);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
observer.disconnect();
cancelAnimation();
};
}, [end, duration, mounted]);
return { count, elementRef };
}

View File

@@ -0,0 +1,9 @@
import type { NextConfig } from "next";
import path from "path";
const nextConfig: NextConfig = {
outputFileTracingRoot: path.join(__dirname),
output: 'standalone',
};
export default nextConfig;

10893
landingpage/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
landingpage/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "asset-homepage",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npx next dev -H 0.0.0.0 -p 3011",
"dev:kill": "pkill -f 'next dev' || pkill -f 'next-server' || true",
"build": "next build",
"start": "next start -H 0.0.0.0 -p 3011",
"lint": "next lint",
"validate:colors": "node scripts/validate-colors.js",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop homepage",
"pm2:restart": "pm2 restart homepage",
"pm2:reload": "pm2 reload ecosystem.config.js",
"pm2:delete": "pm2 delete homepage",
"pm2:logs": "pm2 logs homepage",
"pm2:status": "pm2 status",
"pm2:monit": "pm2 monit"
},
"dependencies": {
"@heroui/react": "^2.6.14",
"framer-motion": "^12.29.2",
"lucide-react": "^0.577.0",
"next": "^15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@heroui/theme": "^2.4.15",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.23",
"eslint": "^8",
"eslint-config-next": "^15.1.4",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "5.9.3"
}
}

View File

@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View File

@@ -0,0 +1,10 @@
<svg width="179" height="32" viewBox="0 0 179 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_10487)">
<path d="M36.1269 8.93379C29.6243 8.93379 24.5434 13.8739 24.5434 20.4872C24.5434 27.1006 29.4959 31.9984 36.1269 31.9984C42.7579 31.9984 47.7966 27.016 47.7966 20.4449C47.7966 13.9162 42.844 8.93379 36.1269 8.93379ZM36.1708 27.2323C32.4678 27.2323 29.7543 24.3541 29.7543 20.4888C29.7543 16.5797 32.4239 13.7031 36.1269 13.7031C39.8738 13.7031 42.5857 16.6236 42.5857 20.4888C42.5857 24.3541 39.8738 27.2323 36.1708 27.2323ZM49.2167 13.9601H52.4469V31.5707H57.6139V9.36308H49.2167V13.9601ZM11.5397 13.7015C14.2532 13.7015 16.4061 15.3764 17.2234 17.8676H22.6927C21.7015 12.5421 17.3095 8.93379 11.5836 8.93379C5.08091 8.93379 0 13.8739 0 20.4888C0 27.1038 4.95254 32 11.5836 32C17.1812 32 21.6593 28.3917 22.6504 23.0223H17.2234C16.4484 25.5135 14.2954 27.2323 11.5819 27.2323C7.83503 27.2323 5.20927 24.3541 5.20927 20.4888C5.21089 16.5797 7.79441 13.7015 11.5397 13.7015ZM147.393 18.3408L143.604 17.783C141.796 17.5261 140.504 16.9244 140.504 15.5065C140.504 13.9601 142.184 13.1877 144.465 13.1877C146.963 13.1877 148.557 14.2609 148.901 16.022H153.896C153.335 11.5551 149.891 8.93541 144.595 8.93541C139.126 8.93541 135.509 11.7274 135.509 15.6788C135.509 19.4579 137.878 21.6499 142.657 22.3361L146.446 22.8938C148.298 23.1508 149.332 23.8825 149.332 25.2566C149.332 27.0176 147.523 27.7478 145.026 27.7478C141.968 27.7478 140.246 26.5022 139.987 24.6126H134.906C135.381 28.9511 138.782 32 144.982 32C150.624 32 154.369 29.4226 154.369 24.998C154.369 21.0466 151.657 18.9847 147.393 18.3408ZM55.0304 0.214645C53.1358 0.214645 51.7141 1.5887 51.7141 3.47823C51.7141 5.36775 53.1342 6.74181 55.0304 6.74181C56.925 6.74181 58.3467 5.36775 58.3467 3.47823C58.3467 1.5887 56.925 0.214645 55.0304 0.214645ZM130.859 16.9667C130.859 12.1567 127.931 8.93541 121.731 8.93541C115.875 8.93541 112.602 11.8998 111.955 16.4529H117.08C117.339 14.6918 118.716 13.2316 121.644 13.2316C124.272 13.2316 125.564 14.391 125.564 15.8089C125.564 17.6562 123.195 18.1278 120.267 18.4286C116.305 18.8579 111.396 20.2319 111.396 25.3867C111.396 29.382 114.368 31.9577 119.105 31.9577C122.808 31.9577 125.133 30.4113 126.296 27.9624C126.469 30.1528 128.105 31.5707 130.388 31.5707H133.402V26.9754H130.861V16.9667H130.859ZM125.778 22.5507C125.778 25.5151 123.195 27.7055 120.05 27.7055C118.112 27.7055 116.476 26.8892 116.476 25.172C116.476 22.9817 119.103 22.38 121.514 22.1231C123.84 21.9084 125.131 21.393 125.778 20.4043V22.5507ZM98.3489 8.93379C95.4632 8.93379 93.0519 10.1371 91.3296 12.1551V0H86.1626V31.5707H91.2435V28.6502C92.9658 30.7544 95.421 32 98.3489 32C104.549 32 109.244 27.1038 109.244 20.4888C109.244 13.8739 104.463 8.93379 98.3489 8.93379ZM97.5739 27.2323C93.8708 27.2323 91.1573 24.3541 91.1573 20.4888C91.1573 16.6236 93.9131 13.7031 97.6161 13.7031C101.363 13.7031 103.989 16.5813 103.989 20.4888C103.989 24.3541 101.277 27.2323 97.5739 27.2323ZM73.8039 8.93379C70.4454 8.93379 68.2486 10.3078 66.9568 12.2413V9.36308H61.8321V31.5691H66.9991V19.5002C66.9991 16.1065 69.152 13.7015 72.3383 13.7015C75.3102 13.7015 77.1609 15.8057 77.1609 18.8562V31.5707H82.3279V18.4709C82.3295 12.8852 79.4454 8.93379 73.8039 8.93379ZM179 19.7587C179 13.4023 174.35 8.93541 168.105 8.93541C161.474 8.93541 156.608 13.9178 156.608 20.4888C156.608 27.4046 161.819 32 168.191 32C173.575 32 177.794 28.821 178.87 24.3118H173.487C172.712 26.2875 170.817 27.4046 168.276 27.4046C164.96 27.4046 162.462 25.3428 161.903 21.7344H178.998V19.7587H179ZM162.206 18.0399C163.024 14.9471 165.35 13.4446 168.019 13.4446C170.947 13.4446 173.186 15.1195 173.703 18.0399H162.206Z" fill="#9CA1AF"/>
</g>
<defs>
<clipPath id="clip0_4_10487">
<rect width="179" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

View File

@@ -0,0 +1,16 @@
<svg width="305" height="162" viewBox="0 0 305 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_10643)">
<path d="M-1 161H304" stroke="#D1D5DB" stroke-width="0.521677" stroke-dasharray="3.13 3.13"/>
<path d="M34.5112 137.394C21.7066 134.128 7.83516 138.756 2.5 141.478V162H302.5V4L268.308 16.2505L238.583 45.5156L174.366 76.7499L150.552 74.1002L112.825 85.6701C102.916 95.1983 81.0419 116.569 72.8104 125.824C62.5211 140.117 50.5169 141.478 34.5112 137.394Z" fill="url(#paint0_linear_4_10643)"/>
<path d="M2.5 142C7.85368 139.267 21.7733 134.622 34.6223 137.901C50.6835 142 62.7294 140.634 73.0545 126.287C79.4259 119.12 93.9428 104.718 104.811 94.1177C110.317 88.7477 117.038 84.805 124.391 82.5494L141.319 77.3564C147.728 75.3903 154.508 74.952 161.117 76.0764C170.233 77.6274 179.607 76.1943 187.843 71.9902L232.671 49.109C237.132 46.8319 241.218 43.8849 244.787 40.3704L261.715 23.6982C266.669 18.8198 272.603 15.0505 279.124 12.6402L302.5 4" stroke="#10B981" stroke-width="2.08671" stroke-linecap="round"/>
</g>
<defs>
<linearGradient id="paint0_linear_4_10643" x1="134.431" y1="10.2653" x2="162.471" y2="160.036" gradientUnits="userSpaceOnUse">
<stop stop-color="#10B981"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_4_10643">
<rect width="305" height="162" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,10 @@
<svg width="156" height="40" viewBox="0 0 156 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.292 10.0724L36.4259 8.57843C36.1853 8.14469 35.656 8.00012 35.2229 8.24108C35.1748 8.28927 35.0786 8.33747 35.0305 8.38566L33.0576 10.3616C32.7689 10.6507 32.7208 11.1326 32.9614 11.47C33.6831 12.5784 34.2606 13.7832 34.6936 15.0363C37.4364 23.1808 33.0576 32.0001 24.9256 34.7471C23.3377 35.2772 21.6535 35.5664 19.9694 35.5664C17.4191 35.5664 14.917 34.9399 12.7035 33.7351L16.0718 30.3616C21.7979 32.5302 28.1976 29.6387 30.363 23.9037C31.4216 21.0604 31.2772 17.8796 29.978 15.1326C29.7856 14.6989 29.2563 14.5061 28.7751 14.6989C28.6788 14.7471 28.5826 14.7953 28.5345 14.8917L26.5135 16.8676C26.2729 17.1086 26.2248 17.3977 26.2729 17.6869L26.4654 18.4098C27.2834 21.976 25.0699 25.5905 21.5092 26.4098C19.8732 26.7953 18.1409 26.5543 16.6973 25.6869L15.8312 25.1567C15.4944 24.964 15.0613 25.0122 14.7726 25.3013L6.59247 33.4941C6.25564 33.8314 6.25564 34.4098 6.59247 34.7471C6.64058 34.7953 6.64058 34.7953 6.6887 34.8435L7.89166 35.7592C11.3081 38.5543 15.5906 40.0001 19.9694 40.0001C30.9885 40.0001 39.9385 31.0363 39.9385 19.9519C39.9385 16.482 39.0243 13.1086 37.292 10.0724Z" fill="#9CA1AF"/>
<path d="M32.095 4.14458C28.6305 1.44578 24.3479 0 19.9692 0C8.95003 0 0 8.96385 0 20.0482C0 23.5181 0.91425 26.9398 2.5984 29.9277L3.46453 31.4217C3.70512 31.8554 4.23442 32 4.66749 31.759C4.71561 31.7108 4.81184 31.6627 4.85996 31.6145L6.83282 29.6386C7.12153 29.3494 7.16965 28.8675 6.92906 28.5301C6.20728 27.4217 5.62986 26.2169 5.19679 24.9639C2.45404 16.8193 6.83282 8 14.9648 5.25301C16.5527 4.72289 18.2369 4.43373 19.921 4.43373C22.4713 4.43373 24.9735 5.06024 27.1869 6.26506L23.8186 9.63855C18.0925 7.46988 11.6928 10.3614 9.52745 16.0964C9.04627 17.3494 8.80567 18.6988 8.80567 20.0482C8.80567 20.241 8.85379 21.0602 8.85379 21.2048C8.99815 22.4578 9.33498 23.7108 9.9124 24.8675C10.1049 25.3012 10.6342 25.494 11.1154 25.3012C11.2116 25.253 11.3078 25.2048 11.356 25.1084L13.3769 23.0843C13.6175 22.8434 13.6656 22.5542 13.6175 22.2651L13.4732 21.5422C12.6552 17.9759 14.8686 14.3614 18.4294 13.5422C20.0654 13.1566 21.7977 13.3976 23.2412 14.2651L24.1073 14.7952C24.4442 14.988 24.8772 14.9398 25.1659 14.6506L33.3461 6.45783C33.6829 6.12048 33.6829 5.54217 33.3461 5.20482C33.298 5.15663 33.298 5.15663 33.2498 5.10843L32.095 4.14458Z" fill="#9CA1AF"/>
<path d="M65.7302 25.5423C65.4415 25.3014 64.9603 25.3014 64.6716 25.5905C63.3724 26.6026 61.8807 27.5182 59.7154 27.5182C55.7216 27.5182 52.4976 24.1447 52.4976 20.0484C52.4976 15.952 55.7216 12.5303 59.7154 12.5303C61.4477 12.5303 63.2762 13.2532 64.6716 14.458C64.816 14.6508 65.0566 14.7471 65.2972 14.7471C65.4896 14.7471 65.6821 14.6026 65.8265 14.458L67.1257 13.1086C67.27 12.964 67.3662 12.723 67.3662 12.4821C67.3662 12.2411 67.27 12.0484 67.0775 11.8556C64.816 9.87968 62.5544 9.01221 59.7154 9.01221C53.6525 9.01221 48.6963 13.9761 48.6963 20.0965C48.6963 26.1688 53.6044 31.1327 59.7154 31.1327C62.5063 31.1809 65.1528 30.0724 67.1257 28.1447C67.3181 27.952 67.4144 27.711 67.3662 27.47C67.3662 27.2773 67.27 27.0845 67.1257 26.9399L65.7302 25.5423Z" fill="#9CA1AF"/>
<path d="M75.161 9.25293H73.1881C72.7551 9.25293 72.3701 9.63847 72.3701 10.0722V29.9276C72.3701 30.3614 72.7551 30.7469 73.1881 30.7469H75.161C75.5941 30.7469 75.979 30.3614 75.979 29.9276V10.0722C75.979 9.63847 75.6422 9.25293 75.161 9.25293Z" fill="#9CA1AF"/>
<path d="M98.4022 15.9517C98.4022 12.2891 95.3707 9.25293 91.6656 9.25293H83.5817C83.1005 9.25293 82.7637 9.63847 82.7637 10.0722V29.9276C82.7637 30.4096 83.1486 30.7469 83.5817 30.7469H85.5064C85.9395 30.7469 86.3244 30.3614 86.3244 29.9276V22.5541H90.3664L94.264 30.3614C94.4083 30.6023 94.697 30.7951 94.9858 30.7951H97.2954C97.5842 30.7951 97.8729 30.6505 98.0172 30.4096C98.1616 30.1204 98.1616 29.8312 98.0172 29.5421L94.0715 22.1686C96.7661 20.9638 98.4022 18.6023 98.4022 15.9517ZM94.7452 15.9999C94.7452 17.8794 93.2054 19.4698 91.3769 19.4698H86.3244V12.7228H91.425C93.2054 12.6746 94.7452 14.2168 94.7452 15.9999Z" fill="#9CA1AF"/>
<path d="M119.526 25.5423C119.238 25.3014 118.756 25.3014 118.468 25.5905C117.169 26.6026 115.677 27.5182 113.512 27.5182C109.518 27.5182 106.294 24.1447 106.294 20.0484C106.294 15.952 109.518 12.5303 113.463 12.5303C115.196 12.5303 117.024 13.2532 118.42 14.458C118.564 14.6508 118.805 14.7471 119.045 14.7471C119.238 14.7471 119.43 14.6026 119.575 14.458L120.874 13.1086C121.018 12.964 121.114 12.723 121.114 12.4821C121.114 12.2411 121.018 12.0484 120.826 11.8556C118.564 9.87968 116.302 9.01221 113.463 9.01221C107.401 9.01221 102.444 13.9761 102.444 20.0965C102.444 26.1688 107.352 31.1327 113.463 31.1327C116.254 31.1809 118.901 30.0724 120.874 28.1447C121.066 27.952 121.162 27.711 121.114 27.47C121.114 27.2773 121.018 27.0845 120.874 26.9399L119.526 25.5423Z" fill="#9CA1AF"/>
<path d="M137.571 27.4216H129.776V10.0722C129.776 9.63847 129.391 9.25293 128.958 9.25293H126.985C126.504 9.25293 126.167 9.63847 126.167 10.0722V29.9276C126.167 30.4096 126.552 30.7469 126.985 30.7469H137.571C138.052 30.7469 138.389 30.3614 138.389 29.9276V28.2891C138.389 27.8071 138.052 27.4216 137.571 27.4216Z" fill="#9CA1AF"/>
<path d="M155.183 12.6266C155.664 12.6266 156.001 12.241 156.001 11.8073V10.1205C156.001 9.63862 155.616 9.30127 155.183 9.30127H143.297C142.816 9.30127 142.479 9.68681 142.479 10.1205V29.976C142.479 30.4579 142.864 30.7952 143.297 30.7952H155.183C155.664 30.7952 156.001 30.4097 156.001 29.976V28.3374C156.001 27.8555 155.616 27.5181 155.183 27.5181H146.04V21.5422H153.691C154.172 21.5422 154.509 21.1567 154.509 20.723V19.0362C154.509 18.6025 154.124 18.2169 153.691 18.2169H146.04V12.6266H155.183Z" fill="#9CA1AF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,9 @@
<svg width="160" height="40" viewBox="0 0 160 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M64.5039 27.4229H61.3193L55.4033 16.8848L49.4717 27.4229H46.2861L55.4053 11.2412L64.5039 27.4229ZM82.6846 14.7344H71.8057C71.2937 14.7345 70.8758 14.9071 70.5107 15.2715C70.1459 15.6358 69.9727 16.0526 69.9727 16.5635C69.9727 17.0728 70.1443 17.4891 70.5068 17.8525C70.87 18.2058 71.289 18.3759 71.8057 18.376H78.1533C79.3949 18.3762 80.4742 18.8246 81.3457 19.7041C82.2326 20.5757 82.6844 21.6559 82.6846 22.8994C82.6846 24.1427 82.2325 25.2259 81.3496 26.1074C80.4756 26.9799 79.3951 27.4228 78.1533 27.4229H67.2734V24.7295H78.1533C78.6651 24.7294 79.0823 24.5566 79.4473 24.1924C79.8124 23.8279 79.9863 23.4106 79.9863 22.8994C79.9862 22.3889 79.8133 21.9813 79.4541 21.6309L79.4473 21.624C79.0823 21.26 78.665 21.088 78.1533 21.0879H71.8057C70.5641 21.0879 69.4796 20.6456 68.5957 19.7773L68.5918 19.7715L68.5869 19.7686C67.7168 18.8861 67.2734 17.803 67.2734 16.5635C67.2735 15.324 67.7178 14.2454 68.5918 13.373C69.4749 12.4917 70.5602 12.0401 71.8057 12.04H82.6846V14.7344ZM101.412 14.7344H90.5332C90.0215 14.7345 89.6042 14.9074 89.2393 15.2715C88.8743 15.6357 88.7003 16.0526 88.7002 16.5635C88.7002 17.0728 88.873 17.4891 89.2354 17.8525C89.5986 18.2055 90.017 18.3758 90.5332 18.376H96.8809C98.1225 18.3761 99.2018 18.8247 100.073 19.7041C100.96 20.5757 101.412 21.6559 101.412 22.8994C101.412 24.1426 100.961 25.226 100.078 26.1074C99.2041 26.9799 98.1227 27.4228 96.8809 27.4229H86.001V24.7295H96.8809C97.3928 24.7294 97.8107 24.5568 98.1758 24.1924C98.541 23.8279 98.7139 23.4106 98.7139 22.8994C98.7137 22.3888 98.5411 21.9814 98.1816 21.6309L98.1758 21.624C97.8107 21.2598 97.3928 21.088 96.8809 21.0879H90.5332C89.2919 21.0877 88.2081 20.6454 87.3242 19.7773L87.3154 19.7686C86.4452 18.8861 86.001 17.803 86.001 16.5635C86.001 15.3241 86.4456 14.2454 87.3193 13.373C88.2024 12.4917 89.2879 12.0402 90.5332 12.04H101.412V14.7344ZM120.143 14.7344H112.438C111.139 14.7344 110.028 15.1657 109.071 16.04C108.343 16.7054 107.861 17.4793 107.617 18.376H120.143V21.0879H107.617C107.861 21.9868 108.343 22.7676 109.072 23.4424C110.027 24.3037 111.138 24.7295 112.438 24.7295H120.143V27.4229H112.438C110.316 27.4229 108.484 26.6672 106.986 25.1729C105.489 23.6783 104.732 21.8496 104.732 19.7314C104.733 17.6133 105.489 15.7854 106.986 14.291C108.484 12.7964 110.316 12.04 112.438 12.04H120.143V14.7344ZM138.874 14.7344H132.525V27.4229H129.81V14.7344H123.463V12.04H138.874V14.7344ZM149.896 17.6377L154.839 12.04H158.538L151.737 19.7314L158.538 27.4229H154.839L149.896 21.8271L144.955 27.4229H141.257L148.058 19.7314L141.257 12.04H144.955L149.896 17.6377Z" fill="#111827"/>
<path d="M22.7595 21.1203C18.5742 26.4404 19.4882 25.3226 15.8872 29.3075C12.2862 33.2924 10.8695 34.122 7.84473 35.6161C10.6589 38.3532 17.6613 39.5414 20.5777 39.6614C31.9067 39.6614 37.8658 29.0974 39.4293 23.8154L34.6487 28.5872C26.8272 34.5505 27.5173 27.1545 27.8281 21.4701C28.139 15.7856 26.9448 15.8001 22.7595 21.1203Z" fill="#111827"/>
<path opacity="0.1" d="M21.21 26.2439C25.3141 20.8326 24.7568 22.5015 24.8075 24.7774C24.8524 26.7963 26.5319 36.6559 33.1012 34.301C30.0474 37.3255 25.9075 39.662 20.5798 39.662C18.3127 39.5687 13.8582 38.9891 10.665 37.45C13.0175 35.3088 18.0426 30.4203 21.21 26.2439Z" fill="#111827"/>
<path d="M25.6931 9.41385C31.7064 4.99711 33.3042 12.2451 33.3513 16.4212V23.1454C33.3513 27.4346 39.8398 20.9512 39.8398 18.7215C39.8398 17.7209 37.2516 0 21.084 0C4.91616 0 -1.11093 13.0945 0.165453 22.2252C1.44185 31.356 4.92273 29.6112 7.25637 28.1708C10.731 26.9675 18.1765 14.9347 25.6931 9.41385Z" fill="#111827"/>
<path d="M21.0861 0C30.6554 4.75569e-06 35.4673 6.20837 37.8012 11.4831C34.1121 7.97614 30.7616 7.54956 28.4579 8.08875C27.6616 8.22689 26.7457 8.64219 25.6951 9.41381C18.1786 14.9347 10.2903 26.9881 7.36901 28.3181C4.44771 29.6479 0.986264 31.1565 0.167397 22.2252C-0.674318 13.0444 4.91822 0 21.0861 0Z" fill="#111827"/>
<path opacity="0.1" d="M18.7263 33.5267C22.0196 29.4302 22.2611 26.9637 23.337 30.9474C24.2896 34.4746 26.7081 36.2162 29.1686 37.3452C26.7487 38.7572 23.8936 39.6621 20.5787 39.6621C18.8808 39.5922 15.9559 39.2493 13.2529 38.4351C15.031 37.2144 17.1298 35.5124 18.7263 33.5267Z" fill="#111827"/>
<path d="M21.088 0C21.3164 0 21.5422 0.00350863 21.7652 0.0104881C23.9447 0.0786729 25.8683 0.475476 27.565 1.11005C27.7725 1.18767 27.9766 1.26895 28.1773 1.35352C28.2003 1.36323 28.2235 1.37276 28.2464 1.38255C28.3196 1.41379 28.3923 1.44574 28.4646 1.47788C28.5163 1.50086 28.5677 1.52413 28.619 1.54755C28.7651 1.61432 28.9095 1.68267 29.052 1.75301C29.1196 1.7864 29.1867 1.82035 29.2534 1.85452C29.3449 1.90133 29.4356 1.9488 29.5257 1.99705C30.6339 2.5913 31.6275 3.29283 32.5172 4.06639C32.6232 4.15858 32.7278 4.25182 32.831 4.346C32.9697 4.4727 33.1057 4.60121 33.2392 4.73125C33.2839 4.77477 33.3283 4.81848 33.3725 4.86235C33.434 4.92356 33.4949 4.98514 33.5554 5.04702C33.5739 5.06596 33.5924 5.08494 33.6107 5.10395C33.6736 5.16883 33.7359 5.23394 33.7974 5.29948C34.0164 5.53248 34.2277 5.76965 34.4318 6.01023C34.5281 6.12388 34.6225 6.23863 34.7157 6.35372C34.8215 6.48444 34.9255 6.61579 35.0271 6.74814C36.1938 8.2668 37.0918 9.88565 37.7782 11.4271C37.7864 11.4456 37.7946 11.4641 37.8028 11.4826C34.7535 8.5841 31.9355 7.79003 29.749 7.90634C27.2573 7.13785 23.2106 7.96321 17.9463 13.2178C5.91446 25.2275 -1.8284 26.8444 2.06022 12.1741C2.44117 10.7369 2.97053 9.45623 3.62937 8.31499C4.6146 6.90895 5.80945 5.60385 7.22473 4.46399C7.35114 4.36217 7.47942 4.26178 7.60937 4.16264C9.16188 2.97822 10.9663 1.98826 13.036 1.27149C13.3319 1.169 13.6333 1.07212 13.9402 0.98101C16.0584 0.352059 18.4368 2.0529e-05 21.088 0Z" fill="#111827"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="1200" height="187" viewBox="0 0 1200 187" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M200.242 186.762H166.632L104.208 75.5576L41.6104 186.762H8L104.222 16L200.242 186.762ZM392.1 52.8594H277.295C271.892 52.8607 267.478 54.6792 263.626 58.5244C259.774 62.3696 257.951 66.7736 257.951 72.167C257.951 77.5427 259.761 81.9338 263.587 85.7695C267.42 89.4977 271.843 91.2881 277.295 91.2881H344.274C357.378 91.2895 368.767 96.0215 377.964 105.304C387.324 114.501 392.098 125.902 392.1 139.025C392.1 152.146 387.323 163.577 378.005 172.88C368.781 182.087 357.38 186.762 344.274 186.762H229.47V158.332H344.274C349.676 158.332 354.077 156.512 357.93 152.668C361.783 148.821 363.617 144.42 363.617 139.025C363.616 133.638 361.799 129.334 358.009 125.636L357.969 125.608L357.93 125.568C354.077 121.726 349.675 119.904 344.274 119.904H277.295C264.191 119.904 252.746 115.239 243.418 106.074L243.377 106.021L243.324 105.981C234.142 96.6693 229.47 85.2468 229.47 72.167C229.47 59.0858 234.153 47.705 243.377 38.499C252.697 29.1967 264.15 24.4309 277.295 24.4307H392.1V52.8594ZM589.724 52.8594H474.919C469.518 52.8607 465.115 54.6819 461.264 58.5244C457.41 62.3696 455.576 66.7736 455.576 72.167C455.576 77.5426 457.398 81.9339 461.223 85.7695C465.056 89.4949 469.471 91.2867 474.919 91.2881H541.898C555.002 91.2881 566.391 96.0229 575.588 105.304C584.948 114.501 589.722 125.902 589.724 139.025C589.724 152.145 584.958 163.579 575.642 172.88C566.417 182.087 555.005 186.762 541.898 186.762H427.094V158.332H541.898C547.303 158.332 551.714 156.515 555.567 152.668C559.421 148.821 561.242 144.42 561.242 139.025C561.241 133.636 559.426 129.335 555.633 125.636L555.567 125.568C551.714 121.723 547.303 119.904 541.898 119.904H474.919C461.818 119.903 450.383 115.237 441.055 106.074L440.962 105.981C431.779 96.6693 427.094 85.2467 427.094 72.167C427.094 59.0872 431.78 47.705 441.001 38.499C450.321 29.1969 461.776 24.4316 474.919 24.4307H589.724V52.8594ZM787.373 52.8594H706.073C692.359 52.8594 680.637 57.4096 670.545 66.6357C662.855 73.6576 657.768 81.8249 655.198 91.2881H787.373V119.904H655.198C657.767 129.393 662.857 137.634 670.559 144.757C680.637 153.846 692.35 158.332 706.073 158.332H787.373V186.762H706.073C683.677 186.762 664.338 178.785 648.537 163.014C632.739 147.242 624.758 127.949 624.758 105.597C624.759 83.2443 632.738 63.95 648.537 48.1797C664.338 32.4074 683.677 24.4307 706.073 24.4307H787.373V52.8594ZM985.042 52.8594H918.051V186.762H889.391V52.8594H822.409V24.4307H985.042V52.8594ZM1049.21 24.4307L1101.36 83.4961L1153.51 24.4307H1192.55L1120.79 105.597L1192.55 186.762H1153.51L1101.36 127.709L1049.21 186.762H1010.18L1081.95 105.597L1010.18 24.4307H1049.21Z" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="193" height="28" viewBox="0 0 193 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5535 26.8894V0H30.6639V26.8894H24.5535ZM143.334 16.8432C143.334 10.345 138.304 6.31155 132.417 6.31155C126.493 6.31155 121.501 10.3823 121.501 16.8432C121.501 23.3415 126.531 27.4123 132.417 27.4123C138.304 27.4123 143.334 23.3042 143.334 16.8432ZM137.298 16.8432C137.298 20.2418 135.025 22.1464 132.38 22.1464C129.735 22.1464 127.462 20.2418 127.462 16.8432C127.462 13.4074 129.735 11.5774 132.38 11.5774C135.025 11.5774 137.298 13.4074 137.298 16.8432ZM22.0944 19.1587C22.0944 16.1337 20.5295 13.8182 17.7351 12.7725C20.3805 11.316 21.312 9.4113 21.312 7.17052C21.312 3.24914 18.4803 0 12.1463 0.0373464H0V26.9268H12.668C19.0392 26.8894 22.0944 23.715 22.0944 19.1587ZM14.829 7.95479C14.829 9.78477 13.6739 10.8305 11.4384 10.8305H6.37124V5.2285H11.4384C13.4876 5.2285 14.829 6.12482 14.829 7.95479ZM15.3878 18.7479C15.3878 20.5032 14.1583 21.6609 11.8855 21.6609H6.37124V15.8349H11.8855C14.0838 15.8349 15.3878 16.9553 15.3878 18.7479ZM75.2998 11.8015C73.6232 8.29091 70.4934 6.34889 65.7616 6.34889C60.1728 6.34889 55.1801 9.85946 55.1801 16.8432C55.1801 23.6403 59.5766 27.4123 65.6871 27.4123C69.9718 27.4123 73.5859 25.6943 75.2998 21.9971L70.3817 19.4575C69.4874 21.1007 68.1089 22.1464 65.9106 22.1464C63.079 22.1464 61.2533 20.1297 61.2533 16.8432C61.2533 13.5568 63.3398 11.5774 65.8734 11.5774C67.8481 11.5774 69.3384 12.4737 70.2699 14.4157L75.2998 11.8015ZM165.354 11.8015C163.677 8.29091 160.548 6.34889 155.816 6.34889C150.227 6.34889 145.234 9.85946 145.234 16.8432C145.234 23.6403 149.631 27.4123 155.741 27.4123C160.026 27.4123 163.64 25.6943 165.354 21.9971L160.436 19.4575C159.542 21.1007 158.163 22.1464 155.965 22.1464C153.133 22.1464 151.308 20.1297 151.308 16.8432C151.308 13.5568 153.394 11.5774 155.928 11.5774C157.902 11.5774 159.393 12.4737 160.324 14.4157L165.354 11.8015ZM114.123 26.8894L109.019 17.8516H105.107V26.8894H98.81V0H110.174C116.322 0 120.606 2.95037 120.606 8.73907C120.606 12.4364 118.781 15.0506 115.539 16.6565L121.426 26.8894H114.123ZM105.144 12.5111H109.839C112.745 12.5111 114.161 10.8678 114.161 8.9258C114.161 6.61032 112.819 5.34054 109.839 5.34054H105.144V12.5111ZM54.1741 26.5533V22.3332C53.9133 22.4079 53.5407 22.4826 53.1309 22.4826C52.3485 22.4826 51.9386 22.0717 51.9386 21.3622V14.3784C51.9386 9.14988 48.4363 6.38624 42.6612 6.38624C38.8981 6.38624 35.6938 7.76806 33.6819 9.56069L37.0724 13.37C38.4137 12.1749 40.2021 11.428 41.9533 11.428C44.6359 11.428 45.9027 12.7725 45.9027 14.9759V15.6108C44.6359 15.312 43.0338 15.0133 41.0963 15.0133C36.2154 15.0133 33.0485 17.3661 33.0485 21.2501C33.0485 25.2462 35.6938 27.3376 39.7923 27.3376C42.5867 27.3376 45.083 26.1799 46.3871 24.686C47.1695 26.6654 48.8834 27.1882 50.56 27.1882C51.7151 27.2256 53.0564 27.0388 54.1741 26.5533ZM45.94 20.2418C45.94 22.0717 44.1888 23.2668 42.1768 23.2668C40.2766 23.2668 39.3452 22.2958 39.3452 20.9514C39.3452 19.5322 40.3884 18.4865 42.5122 18.4865C43.8162 18.4865 44.9712 18.7106 45.94 18.9346V20.2418ZM187.188 26.8894L179.997 14.3784L186.741 7.02113H179.959L172.843 14.9386V0.0373464H166.77V26.9268H172.843V22.0717L175.749 18.972L180.295 26.9268H187.188V26.8894ZM97.2079 26.8894L90.017 14.3784L96.7608 7.02113H89.9052L82.8633 14.9386V0.0373464H76.7902V26.9268H82.8633V22.0717L85.7695 18.972L90.3151 26.9268H97.2079V26.8894ZM190.914 23.1548C192.18 23.1548 192.925 23.9391 192.925 25.1342C192.925 26.3292 192.18 27.1135 190.914 27.1135C189.647 27.1135 188.902 26.3292 188.902 25.1342C188.902 23.9391 189.647 23.1548 190.914 23.1548ZM190.914 26.8521C191.919 26.8521 192.59 26.2545 192.59 25.1342C192.59 24.0138 191.882 23.4162 190.914 23.4162C189.908 23.4162 189.237 24.0138 189.237 25.1342C189.2 26.2919 189.908 26.8521 190.914 26.8521ZM190.131 24.1258H190.951C191.435 24.1258 191.696 24.3872 191.696 24.7607C191.696 25.0968 191.547 25.2835 191.286 25.3956L191.659 26.1052H191.286L190.951 25.4703H190.504V26.1052H190.131V24.1258ZM190.504 24.4246V25.1342H190.914C191.174 25.1342 191.323 25.0595 191.323 24.798C191.323 24.574 191.212 24.4619 190.951 24.4619H190.504V24.4246Z" fill="#9CA1AF"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,12 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_10710)">
<path d="M32 64C49.7334 64 64 49.7334 64 32C64 14.2666 49.7334 0 32 0C14.2666 0 0 14.2666 0 32C0 49.7334 14.2666 64 32 64Z" fill="#2775CA"/>
<path d="M40.8002 37.0666C40.8002 32.4 38.0002 30.8 32.4002 30.1335C28.4002 29.6 27.6002 28.5335 27.6002 26.6666C27.6002 24.7997 28.9336 23.6 31.6002 23.6C34.0002 23.6 35.3336 24.4 36.0002 26.4C36.1336 26.8 36.5336 27.0666 36.9336 27.0666H39.0667C39.6002 27.0666 40.0002 26.6666 40.0002 26.1335V26C39.4667 23.0666 37.0667 20.8 34.0002 20.5335V17.3335C34.0002 16.8 33.6002 16.4 32.9336 16.2666H30.9336C30.4002 16.2666 30.0002 16.6666 29.8667 17.3335V20.4C25.8667 20.9335 23.3336 23.6 23.3336 26.9335C23.3336 31.3335 26.0002 33.0666 31.6002 33.7335C35.3336 34.4 36.5336 35.2 36.5336 37.3335C36.5336 39.4669 34.6667 40.9335 32.1336 40.9335C28.6667 40.9335 27.4667 39.4666 27.0667 37.4666C26.9336 36.9335 26.5336 36.6666 26.1336 36.6666H23.8667C23.3336 36.6666 22.9336 37.0666 22.9336 37.6V37.7335C23.4667 41.0666 25.6002 43.4666 30.0002 44.1335V47.3335C30.0002 47.8666 30.4002 48.2666 31.0667 48.4H33.0667C33.6002 48.4 34.0002 48 34.1336 47.3335V44.1335C38.1336 43.4666 40.8002 40.6666 40.8002 37.0666Z" fill="white"/>
<path d="M25.1997 51.0666C14.7997 47.3335 9.46623 35.7335 13.3331 25.4666C15.3331 19.8666 19.7331 15.6001 25.1997 13.6001C25.7331 13.3335 25.9997 12.9335 25.9997 12.2666V10.4001C25.9997 9.86662 25.7331 9.46662 25.1997 9.3335C25.0662 9.3335 24.7997 9.3335 24.6662 9.46662C11.9997 13.4666 5.06623 26.9335 9.06623 39.6001C11.4662 47.0666 17.1997 52.8001 24.6662 55.2001C25.1997 55.4666 25.7331 55.2001 25.8662 54.6666C25.9997 54.5335 25.9997 54.4001 25.9997 54.1335V52.2666C25.9997 51.8666 25.5997 51.3335 25.1997 51.0666ZM39.3331 9.46662C38.7997 9.20006 38.2662 9.46662 38.1331 10.0001C37.9997 10.1335 37.9997 10.2666 37.9997 10.5335V12.4001C37.9997 12.9335 38.3997 13.4666 38.7997 13.7335C49.1997 17.4666 54.5331 29.0666 50.6662 39.3335C48.6662 44.9335 44.2662 49.2001 38.7997 51.2001C38.2662 51.4666 37.9997 51.8666 37.9997 52.5335V54.4001C37.9997 54.9335 38.2662 55.3335 38.7997 55.4666C38.9331 55.4666 39.1997 55.4666 39.3331 55.3335C51.9997 51.3335 58.9331 37.8666 54.9331 25.2001C52.5331 17.6001 46.6662 11.8666 39.3331 9.46662Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_4_10710">
<rect width="64" height="64" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

36
landingpage/scripts/dev.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# 尝试的端口列表
PORTS=(3002 3003 3004 3005 3006)
# 检查端口是否被占用
check_port() {
local port=$1
if nc -z localhost $port 2>/dev/null || curl -s http://localhost:$port >/dev/null 2>&1; then
return 1 # 端口被占用
else
return 0 # 端口可用
fi
}
# 查找可用端口
find_available_port() {
for port in "${PORTS[@]}"; do
if check_port $port; then
echo $port
return 0
fi
done
# 如果预定义端口都被占用,随机选择一个
echo $((3000 + RANDOM % 1000))
return 0
}
# 查找可用端口
AVAILABLE_PORT=$(find_available_port)
echo "🚀 Starting dev server on port $AVAILABLE_PORT..."
# 启动开发服务器
npx next dev -H 0.0.0.0 -p $AVAILABLE_PORT

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# 批量更新组件中的图片路径以使用 getAssetUrl 函数
echo "🔧 修复静态资源路径..."
# 需要更新的组件列表
components=(
"components/HowItWorksSection.tsx"
"components/WhyAssetXSection.tsx"
"components/Navbar.tsx"
"components/Footer.tsx"
"components/ResourceMenu.tsx"
"components/ProductMenu.tsx"
"components/SecuritySection.tsx"
)
for component in "${components[@]}"; do
if [ -f "$component" ]; then
echo " 处理: $component"
# 添加 import 语句(如果不存在)
if ! grep -q "getAssetUrl" "$component"; then
# 在 'use client' 之后或第一个 import 之前添加
if grep -q "'use client'" "$component"; then
sed -i "/'use client';/a import { getAssetUrl } from '@/lib/assetUrl';" "$component"
else
sed -i "1i import { getAssetUrl } from '@/lib/assetUrl';" "$component"
fi
fi
# 替换 src="/xxx.svg" 为 src={getAssetUrl("/xxx.svg")}
sed -i 's/src="\/\([^"]*\.svg\)"/src={getAssetUrl("\/\1")}/g' "$component"
# 替换 icon: '/xxx.svg' 为 icon: getAssetUrl('/xxx.svg')
sed -i "s/icon: '\/\([^']*\.svg\)'/icon: getAssetUrl('\/\1')/g" "$component"
# 替换 icon: "/xxx.svg" 为 icon: getAssetUrl("/xxx.svg")
sed -i 's/icon: "\/\([^"]*\.svg\)"/icon: getAssetUrl("\/\1")/g' "$component"
fi
done
echo "✅ 完成!"

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* 颜色验证脚本
*
* 检查组件文件中是否还有硬编码的十六进制颜色值
* 用法: node scripts/validate-colors.js
*/
const fs = require('fs');
const path = require('path');
// 硬编码颜色的正则模式
const hardcodedColorPatterns = [
// Tailwind 类名中的十六进制颜色: bg-[#hex], text-[#hex], border-[#hex]
/(bg|text|border|from|to|via)-\[#[0-9a-fA-F]{3,8}\]/g,
// 内联样式中的十六进制颜色
/(?:background|color|border)(?:Color)?:\s*['"]?#[0-9a-fA-F]{3,8}/g,
// 对象样式中的十六进制颜色
/['"](?:background|color|border)(?:Color)?['"]\s*:\s*['"]#[0-9a-fA-F]{3,8}/g,
];
// 允许的例外情况(特殊设计需求)
const allowedExceptions = [
// rgba/hsla 透明度值(毛玻璃效果等)
/rgba?\(/,
/hsla?\(/,
// 渐变色gradient-to-br from-green-400 to-emerald-500 等非硬编码)
// 注意:这里不排除硬编码的渐变,会被上面的模式捕获
];
// 需要检查的目录
const componentsDir = path.join(__dirname, '../components');
// 收集结果
const results = {
files: [],
totalViolations: 0,
};
/**
* 检查单个文件
*/
function checkFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const violations = [];
// 按行分析
const lines = content.split('\n');
lines.forEach((line, index) => {
hardcodedColorPatterns.forEach(pattern => {
const matches = line.match(pattern);
if (matches) {
// 检查是否是允许的例外
const isException = allowedExceptions.some(exceptionPattern =>
exceptionPattern.test(line)
);
if (!isException) {
matches.forEach(match => {
violations.push({
line: index + 1,
content: line.trim(),
match: match,
});
});
}
}
});
});
if (violations.length > 0) {
results.files.push({
path: path.relative(process.cwd(), filePath),
violations: violations,
count: violations.length,
});
results.totalViolations += violations.length;
}
return violations.length === 0;
}
/**
* 递归扫描目录
*/
function scanDirectory(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (entry.isFile() && /\.(tsx|ts|jsx|js)$/.test(entry.name)) {
checkFile(fullPath);
}
});
}
/**
* 打印结果
*/
function printResults() {
console.log('\n='.repeat(80));
console.log('🎨 设计系统颜色验证报告');
console.log('='.repeat(80));
console.log();
if (results.totalViolations === 0) {
console.log('✅ 太棒了!没有发现硬编码颜色值。');
console.log();
console.log('所有组件都已成功迁移到设计系统!');
} else {
console.log(`⚠️ 发现 ${results.totalViolations} 处硬编码颜色值,分布在 ${results.files.length} 个文件中:`);
console.log();
results.files.forEach(file => {
console.log(`📄 ${file.path} (${file.count} 处问题)`);
console.log('─'.repeat(80));
// 按行号分组相同的问题
const lineMap = new Map();
file.violations.forEach(v => {
if (!lineMap.has(v.line)) {
lineMap.set(v.line, []);
}
lineMap.get(v.line).push(v.match);
});
lineMap.forEach((matches, lineNum) => {
const firstViolation = file.violations.find(v => v.line === lineNum);
console.log(`${lineNum}: ${firstViolation.content.substring(0, 100)}${firstViolation.content.length > 100 ? '...' : ''}`);
matches.forEach(match => {
console.log(`${match}`);
});
});
console.log();
});
console.log('='.repeat(80));
console.log('💡 建议:');
console.log(' • 将硬编码颜色替换为设计系统的 token (如 text-text-primary, bg-bg-base 等)');
console.log(' • 检查是否有特殊设计需求需要保留硬编码');
console.log(' • 参考 /styles/design-system.css 中定义的颜色变量');
}
console.log('='.repeat(80));
console.log();
// 退出码
process.exit(results.totalViolations > 0 ? 1 : 0);
}
// 执行扫描
console.log('🔍 正在扫描组件文件...');
scanDirectory(componentsDir);
printResults();

View File

@@ -0,0 +1,174 @@
/**
* Design System - CSS Variables
*
* 定义 Light 和 Dark 模式的 CSS 变量
* 使用 [data-theme] 属性选择器来切换主题
*/
/* ========================================
Light Mode (Default)
======================================== */
:root {
/* Text Colors */
--text-primary: #111827;
--text-secondary: #4b5563;
--text-tertiary: #9ca1af;
--text-inverse: #ffffff;
--text-disabled: #d1d5db;
/* Background Colors */
--bg-base: #ffffff;
--bg-subtle: #f9fafb;
--bg-surface: #ffffff;
--bg-elevated: #f3f4f6;
--bg-overlay: rgba(0, 0, 0, 0.5);
/* Border Colors */
--border-normal: #e5e7eb;
--border-subtle: #f3f4f6;
--border-strong: #d1d5db;
/* Accent Colors */
--accent-green: #10b981;
--accent-green-bg: #e1f8ec;
--accent-blue: #1447e6;
--accent-blue-bg: #ebf2ff;
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
/* Special Colors */
--special-black: #000000;
--special-white: #ffffff;
}
/* ========================================
Dark Mode
======================================== */
[data-theme="dark"] {
/* Text Colors */
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-inverse: #111827;
--text-disabled: #71717a;
/* Background Colors */
--bg-base: #0a0a0a;
--bg-subtle: #0a0a0a;
--bg-surface: #18181b;
--bg-elevated: #27272a;
--bg-overlay: rgba(0, 0, 0, 0.7);
/* Border Colors */
--border-normal: #27272a;
--border-subtle: #18181b;
--border-strong: #3f3f46;
/* Accent Colors */
--accent-green: #10b981;
--accent-green-bg: rgba(16, 185, 129, 0.15);
--accent-blue: #1447e6;
--accent-blue-bg: rgba(20, 71, 230, 0.15);
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
/* Special Colors */
--special-black: #000000;
--special-white: #ffffff;
}
/* ========================================
Typography - Font Sizes
======================================== */
:root {
/* Serif Display */
--serif--display--giant--bold: 100px;
--serif--display--giant--regular: 100px;
--serif--display--hero--bold: 64px;
--serif--display--hero--regular: 64px;
--serif--display--title-1--bold: 48px;
--serif--display--title-1--regular: 48px;
--serif--display--title-2--bold: 32px;
--serif--display--title-2--regular: 32px;
--serif--display--quote--bold: 24px;
--serif--display--quote--regular: 24px;
--serif--display--text--bold: 16px;
--serif--display--text--regular: 16px;
/* Sans Heading */
--sans--heading--h1--bold: 48px;
--sans--heading--h1--medium: 48px;
--sans--heading--h1--regular: 48px;
--sans--heading--h2--bold: 32px;
--sans--heading--h2--medium: 32px;
--sans--heading--h2--regular: 32px;
--sans--heading--h3--bold: 24px;
--sans--heading--h3--medium: 24px;
--sans--heading--h3--regular: 24px;
--sans--heading--h4--bold: 20px;
--sans--heading--h4--medium: 20px;
--sans--heading--h4--regular: 20px;
/* Sans Body */
--sans--body--large--bold: 18px;
--sans--body--large--medium: 18px;
--sans--body--large--regular: 18px;
--sans--body--default--bold: 16px;
--sans--body--default--medium: 16px;
--sans--body--default--regular: 16px;
--sans--body--small--bold: 14px;
--sans--body--small--medium: 14px;
--sans--body--small--regular: 14px;
/* Sans Caption */
--sans--caption--tiny--bold: 12px;
--sans--caption--tiny--medium: 12px;
--sans--caption--tiny--regular: 12px;
--sans--caption--minuscule--bold: 10px;
--sans--caption--minuscule--medium: 10px;
--sans--caption--minuscule--regular: 10px;
/* Mono Numeric */
--mono--numeric--giant--extra-bold: 48px;
--mono--numeric--giant--bold: 48px;
--mono--numeric--giant--medium: 48px;
--mono--numeric--giant--regular: 48px;
--mono--numeric--huge--extra-bold: 32px;
--mono--numeric--huge--bold: 32px;
--mono--numeric--huge--medium: 32px;
--mono--numeric--huge--regular: 32px;
--mono--numeric--large--extra-bold: 24px;
--mono--numeric--large--bold: 24px;
--mono--numeric--large--medium: 24px;
--mono--numeric--large--regular: 24px;
--mono--numeric--default--extra-bold: 16px;
--mono--numeric--default--bold: 16px;
--mono--numeric--default--medium: 16px;
--mono--numeric--default--regular: 16px;
/* Mono Code */
--mono--code--small--extra-bold: 14px;
--mono--code--small--bold: 14px;
--mono--code--small--medium: 14px;
--mono--code--small--regular: 14px;
--mono--code--tiny--extra-bold: 12px;
--mono--code--tiny--bold: 12px;
--mono--code--tiny--medium: 12px;
--mono--code--tiny--regular: 12px;
--mono--code--minuscule--extra-bold: 10px;
--mono--code--minuscule--bold: 10px;
--mono--code--minuscule--medium: 10px;
--mono--code--minuscule--regular: 10px;
}
/* ========================================
Utility Classes (Optional)
======================================== */
/*
* 如果需要,可以定义一些通用的 utility classes
* 但主要还是通过 Tailwind 的扩展配置来使用这些变量
*/

View File

@@ -0,0 +1,17 @@
/**
* Design System - Tokens Index
*
* 统一导出所有设计 tokens
*/
export { primitives } from './primitives';
export { semanticTokens, lightTokens, darkTokens } from './semantic-tokens';
export { radius } from './radius';
export { spacing } from './spacing';
export { typography } from './typography';
export type { PrimitiveColors } from './primitives';
export type { SemanticTokens, LightTokens, DarkTokens } from './semantic-tokens';
export type { Radius } from './radius';
export type { Spacing } from './spacing';
export type { Typography } from './typography';

View File

@@ -0,0 +1,75 @@
/**
* Design System - Primitive Colors
*
* 原始色板定义,包含所有基础颜色值
* 这些值直接映射到设计规范中的颜色
*/
export const primitives = {
// 基础色
base: {
1: '#ffffff',
10: '#222222',
12: '#000000',
},
// 品牌色系(灰度)
brand: {
1: '#fcfcfd',
2: '#f9fafb',
3: '#f3f4f6',
4: '#e5e7eb',
5: '#d1d5db',
6: '#9ca1af',
7: '#6b7280',
8: '#4b5563',
9: '#374151',
10: '#1f2937',
11: '#111827',
12: '#030712',
},
// 绿色系(成功/强调)
green: {
1: '#e1f8ec',
9: '#10b981',
10: '#00ad76',
11: '#008352',
},
// 蓝色系(链接/强调)
blue: {
1: '#ebf2ff',
9: '#1447e6',
10: '#0d3cc7',
11: '#0831a8',
},
// 红色系(警告/错误)
red: {
9: '#ef4444',
10: '#dc2626',
11: '#b91c1c',
},
// 黄色系(警告)
yellow: {
9: '#f59e0b',
10: '#d97706',
11: '#b45309',
},
// Dark Mode 专用色
dark: {
zinc: {
50: '#fafafa',
400: '#a1a1aa',
500: '#71717a',
800: '#27272a',
900: '#18181b',
950: '#0a0a0a',
},
},
} as const;
export type PrimitiveColors = typeof primitives;

View File

@@ -0,0 +1,19 @@
/**
* Design System - Border Radius
*
* 圆角规范定义
*/
export const radius = {
none: '0',
sm: '0.125rem', // 2px
base: '0.25rem', // 4px
md: '0.375rem', // 6px
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
'2xl': '1rem', // 16px
'3xl': '1.5rem', // 24px
full: '9999px', // 圆形
} as const;
export type Radius = typeof radius;

View File

@@ -0,0 +1,102 @@
/**
* Design System - Semantic Tokens
*
* 语义化颜色 tokens定义 Light 和 Dark 模式下的颜色用途
* 组件应该使用这些语义化的名称,而不是直接使用原始色板
*/
import { primitives } from './primitives';
/**
* Light Mode Semantic Tokens
*/
export const lightTokens = {
text: {
primary: primitives.brand[11], // #111827 - 主要文本
secondary: primitives.brand[8], // #4b5563 - 次要文本
tertiary: primitives.brand[6], // #9ca1af - 辅助文本
inverse: primitives.base[1], // #ffffff - 反色文本(深色背景上)
disabled: primitives.brand[5], // #d1d5db - 禁用文本
},
bg: {
base: primitives.base[1], // #ffffff - 主背景
subtle: primitives.brand[2], // #f9fafb - 次级背景
surface: primitives.base[1], // #ffffff - 卡片/表面背景
elevated: primitives.brand[3], // #f3f4f6 - 提升背景
overlay: 'rgba(0, 0, 0, 0.5)', // 遮罩层
},
border: {
normal: primitives.brand[4], // #e5e7eb - 标准边框
subtle: primitives.brand[3], // #f3f4f6 - 微妙边框
strong: primitives.brand[5], // #d1d5db - 强调边框
},
accent: {
green: primitives.green[9], // #10b981 - 绿色强调
greenBg: primitives.green[1], // #e1f8ec - 绿色背景
blue: primitives.blue[9], // #1447e6 - 蓝色强调
blueBg: primitives.blue[1], // #ebf2ff - 蓝色背景
red: primitives.red[9], // #ef4444 - 红色强调
yellow: primitives.yellow[9], // #f59e0b - 黄色强调
},
special: {
black: primitives.base[12], // #000000 - 纯黑(特殊用途)
white: primitives.base[1], // #ffffff - 纯白(特殊用途)
},
} as const;
/**
* Dark Mode Semantic Tokens
*/
export const darkTokens = {
text: {
primary: primitives.dark.zinc[50], // #fafafa - 主要文本
secondary: primitives.dark.zinc[400], // #a1a1aa - 次要文本
tertiary: primitives.dark.zinc[500], // #71717a - 辅助文本
inverse: primitives.brand[11], // #111827 - 反色文本(浅色背景上)
disabled: primitives.dark.zinc[500], // #71717a - 禁用文本
},
bg: {
base: primitives.dark.zinc[950], // #0a0a0a - 主背景
subtle: primitives.dark.zinc[950], // #0a0a0a - 次级背景
surface: primitives.dark.zinc[900], // #18181b - 卡片/表面背景
elevated: primitives.dark.zinc[800], // #27272a - 提升背景
overlay: 'rgba(0, 0, 0, 0.7)', // 遮罩层
},
border: {
normal: primitives.dark.zinc[800], // #27272a - 标准边框
subtle: primitives.dark.zinc[900], // #18181b - 微妙边框
strong: '#3f3f46', // 强调边框
},
accent: {
green: primitives.green[9], // #10b981 - 绿色强调(保持一致)
greenBg: 'rgba(16, 185, 129, 0.15)', // 绿色背景(透明)
blue: primitives.blue[9], // #1447e6 - 蓝色强调(保持一致)
blueBg: 'rgba(20, 71, 230, 0.15)', // 蓝色背景(透明)
red: primitives.red[9], // #ef4444 - 红色强调
yellow: primitives.yellow[9], // #f59e0b - 黄色强调
},
special: {
black: primitives.base[12], // #000000 - 纯黑(特殊用途)
white: primitives.base[1], // #ffffff - 纯白(特殊用途)
},
} as const;
/**
* 导出统一的 semantic tokens 结构
*/
export const semanticTokens = {
light: lightTokens,
dark: darkTokens,
} as const;
export type SemanticTokens = typeof semanticTokens;
export type LightTokens = typeof lightTokens;
export type DarkTokens = typeof darkTokens;

View File

@@ -0,0 +1,24 @@
/**
* Design System - Spacing
*
* 间距规范定义
*/
export const spacing = {
0: '0',
1: '0.25rem', // 4px
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem', // 24px
8: '2rem', // 32px
10: '2.5rem', // 40px
12: '3rem', // 48px
16: '4rem', // 64px
20: '5rem', // 80px
24: '6rem', // 96px
32: '8rem', // 128px
} as const;
export type Spacing = typeof spacing;

View File

@@ -0,0 +1,178 @@
/**
* Design System - Typography Tokens
*
* 字体大小和样式规范定义
*/
/**
* Serif Display 系列 (Domine/Noto Serif)
* 用于大标题、引用等展示性文本
*/
export const serifDisplay = {
giant: {
bold: { fontSize: '100px', lineHeight: '100%', fontWeight: '700', letterSpacing: '-0.03em' },
regular: { fontSize: '100px', lineHeight: '100%', fontWeight: '400', letterSpacing: '-0.03em' },
},
hero: {
bold: { fontSize: '64px', lineHeight: '110%', fontWeight: '700', letterSpacing: '-0.02em' },
regular: { fontSize: '64px', lineHeight: '110%', fontWeight: '400', letterSpacing: '-0.02em' },
},
title1: {
bold: { fontSize: '48px', lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' },
regular: { fontSize: '48px', lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' },
},
title2: {
bold: { fontSize: '32px', lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' },
regular: { fontSize: '32px', lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' },
},
quote: {
bold: { fontSize: '24px', lineHeight: '130%', fontWeight: '700', letterSpacing: '-0.005em' },
regular: { fontSize: '24px', lineHeight: '130%', fontWeight: '400', letterSpacing: '-0.005em' },
},
text: {
bold: { fontSize: '16px', lineHeight: '150%', fontWeight: '700' },
regular: { fontSize: '16px', lineHeight: '150%', fontWeight: '400' },
},
} as const;
/**
* Sans Heading 系列 (Inter/Noto Sans)
* 用于标题和子标题
*/
export const sansHeading = {
h1: {
bold: { fontSize: '48px', lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' },
medium: { fontSize: '48px', lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.01em' },
regular: { fontSize: '48px', lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' },
},
h2: {
bold: { fontSize: '32px', lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' },
medium: { fontSize: '32px', lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.01em' },
regular: { fontSize: '32px', lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' },
},
h3: {
bold: { fontSize: '24px', lineHeight: '130%', fontWeight: '700', letterSpacing: '-0.005em' },
medium: { fontSize: '24px', lineHeight: '130%', fontWeight: '500', letterSpacing: '-0.005em' },
regular: { fontSize: '24px', lineHeight: '130%', fontWeight: '400', letterSpacing: '-0.005em' },
},
h4: {
bold: { fontSize: '20px', lineHeight: '140%', fontWeight: '700' },
medium: { fontSize: '20px', lineHeight: '140%', fontWeight: '500' },
regular: { fontSize: '20px', lineHeight: '140%', fontWeight: '400' },
},
} as const;
/**
* Sans Body 系列
* 用于正文和段落文本
*/
export const sansBody = {
large: {
bold: { fontSize: '18px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '18px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '18px', lineHeight: '150%', fontWeight: '400' },
},
default: {
bold: { fontSize: '16px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '16px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '16px', lineHeight: '150%', fontWeight: '400' },
},
small: {
bold: { fontSize: '14px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '14px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '14px', lineHeight: '150%', fontWeight: '400' },
},
} as const;
/**
* Sans Caption 系列
* 用于说明文字、标签等小字
*/
export const sansCaption = {
tiny: {
bold: { fontSize: '12px', lineHeight: '150%', fontWeight: '700', letterSpacing: '0.01em' },
medium: { fontSize: '12px', lineHeight: '150%', fontWeight: '500', letterSpacing: '0.01em' },
regular: { fontSize: '12px', lineHeight: '150%', fontWeight: '400', letterSpacing: '0.01em' },
},
minuscule: {
bold: { fontSize: '10px', lineHeight: '150%', fontWeight: '700', letterSpacing: '0.02em' },
medium: { fontSize: '10px', lineHeight: '150%', fontWeight: '500', letterSpacing: '0.02em' },
regular: { fontSize: '10px', lineHeight: '150%', fontWeight: '400', letterSpacing: '0.02em' },
},
} as const;
/**
* Mono Numeric 系列 (JetBrains Mono)
* 用于数字、统计数据展示
*/
export const monoNumeric = {
giant: {
extraBold: { fontSize: '48px', lineHeight: '110%', fontWeight: '800', letterSpacing: '-0.01em' },
bold: { fontSize: '48px', lineHeight: '110%', fontWeight: '700', letterSpacing: '-0.01em' },
medium: { fontSize: '48px', lineHeight: '110%', fontWeight: '500', letterSpacing: '-0.01em' },
regular: { fontSize: '48px', lineHeight: '110%', fontWeight: '400', letterSpacing: '-0.01em' },
},
huge: {
extraBold: { fontSize: '32px', lineHeight: '120%', fontWeight: '800', letterSpacing: '-0.005em' },
bold: { fontSize: '32px', lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.005em' },
medium: { fontSize: '32px', lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.005em' },
regular: { fontSize: '32px', lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.005em' },
},
large: {
extraBold: { fontSize: '24px', lineHeight: '130%', fontWeight: '800' },
bold: { fontSize: '24px', lineHeight: '130%', fontWeight: '700' },
medium: { fontSize: '24px', lineHeight: '130%', fontWeight: '500' },
regular: { fontSize: '24px', lineHeight: '130%', fontWeight: '400' },
},
default: {
extraBold: { fontSize: '16px', lineHeight: '140%', fontWeight: '800' },
bold: { fontSize: '16px', lineHeight: '140%', fontWeight: '700' },
medium: { fontSize: '16px', lineHeight: '140%', fontWeight: '500' },
regular: { fontSize: '16px', lineHeight: '140%', fontWeight: '400' },
},
} as const;
/**
* Mono Code 系列
* 用于代码、技术文本
*/
export const monoCode = {
small: {
extraBold: { fontSize: '14px', lineHeight: '150%', fontWeight: '800' },
bold: { fontSize: '14px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '14px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '14px', lineHeight: '150%', fontWeight: '400' },
},
tiny: {
extraBold: { fontSize: '12px', lineHeight: '150%', fontWeight: '800' },
bold: { fontSize: '12px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '12px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '12px', lineHeight: '150%', fontWeight: '400' },
},
minuscule: {
extraBold: { fontSize: '10px', lineHeight: '150%', fontWeight: '800' },
bold: { fontSize: '10px', lineHeight: '150%', fontWeight: '700' },
medium: { fontSize: '10px', lineHeight: '150%', fontWeight: '500' },
regular: { fontSize: '10px', lineHeight: '150%', fontWeight: '400' },
},
} as const;
/**
* 统一导出
*/
export const typography = {
serif: {
display: serifDisplay,
},
sans: {
heading: sansHeading,
body: sansBody,
caption: sansCaption,
},
mono: {
numeric: monoNumeric,
code: monoCode,
},
} as const;
export type Typography = typeof typography;

View File

@@ -0,0 +1,162 @@
import type { Config } from "tailwindcss";
import { heroui } from "@heroui/theme";
export default {
darkMode: "class",
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
screens: {
'3xl': '2000px', // 2K+
'4xl': '2800px', // 4K
},
colors: {
// 向后兼容
background: "var(--background)",
foreground: "var(--foreground)",
// 设计系统 - 文本颜色
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'text-tertiary': 'var(--text-tertiary)',
'text-inverse': 'var(--text-inverse)',
'text-disabled': 'var(--text-disabled)',
// 设计系统 - 背景颜色
'bg-base': 'var(--bg-base)',
'bg-subtle': 'var(--bg-subtle)',
'bg-surface': 'var(--bg-surface)',
'bg-elevated': 'var(--bg-elevated)',
'bg-overlay': 'var(--bg-overlay)',
// 设计系统 - 边框颜色
'border-normal': 'var(--border-normal)',
'border-subtle': 'var(--border-subtle)',
'border-strong': 'var(--border-strong)',
// 向后兼容的别名
'border-gray': 'var(--border-subtle)',
'fill-secondary-click': 'var(--bg-elevated)',
// 设计系统 - 强调色
'accent-green': 'var(--accent-green)',
'accent-green-bg': 'var(--accent-green-bg)',
'accent-blue': 'var(--accent-blue)',
'accent-blue-bg': 'var(--accent-blue-bg)',
'accent-red': 'var(--accent-red)',
'accent-yellow': 'var(--accent-yellow)',
// 设计系统 - 特殊颜色
'special-black': 'var(--special-black)',
'special-white': 'var(--special-white)',
},
fontFamily: {
inter: ['var(--font-noto-sans-sc)', 'var(--font-inter)', 'Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif'],
jetbrains: ['var(--font-jetbrains)', 'JetBrains Mono', 'monospace'],
domine: ['var(--font-noto-serif-sc)', 'var(--font-domine)', 'Domine', 'Georgia', 'Noto Serif', 'serif'],
'noto-sans': ['var(--font-noto-sans-sc)', 'sans-serif'],
'noto-serif': ['var(--font-noto-serif-sc)', 'serif'],
},
fontSize: {
// 向后兼容的别名
'caption-tiny': ['12px', { lineHeight: '150%', letterSpacing: '0.01em' }],
'body-small': ['14px', { lineHeight: '150%' }],
'body-default': ['16px', { lineHeight: '150%' }],
'body-large': ['18px', { lineHeight: '150%' }],
'heading-h3': ['24px', { lineHeight: '130%', letterSpacing: '-0.005em' }],
'heading-h2': ['32px', { lineHeight: '120%', letterSpacing: '-0.01em' }],
// Serif Display 系列
'serif-display-giant-bold': ['100px', { lineHeight: '100%', fontWeight: '700', letterSpacing: '-0.03em' }],
'serif-display-giant': ['100px', { lineHeight: '100%', fontWeight: '400', letterSpacing: '-0.03em' }],
'serif-display-hero-bold': ['64px', { lineHeight: '110%', fontWeight: '700', letterSpacing: '-0.02em' }],
'serif-display-hero': ['64px', { lineHeight: '110%', fontWeight: '400', letterSpacing: '-0.02em' }],
'serif-display-title1-bold': ['48px', { lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' }],
'serif-display-title1': ['48px', { lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' }],
'serif-display-title2-bold': ['32px', { lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' }],
'serif-display-title2': ['32px', { lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' }],
'serif-display-quote-bold': ['24px', { lineHeight: '130%', fontWeight: '700', letterSpacing: '-0.005em' }],
'serif-display-quote': ['24px', { lineHeight: '130%', fontWeight: '400', letterSpacing: '-0.005em' }],
'serif-display-text-bold': ['16px', { lineHeight: '150%', fontWeight: '700' }],
'serif-display-text': ['16px', { lineHeight: '150%', fontWeight: '400' }],
// Sans Heading 系列
'sans-h1-bold': ['48px', { lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' }],
'sans-h1-medium': ['48px', { lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.01em' }],
'sans-h1': ['48px', { lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' }],
'sans-h2-bold': ['32px', { lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.01em' }],
'sans-h2-medium': ['32px', { lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.01em' }],
'sans-h2': ['32px', { lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.01em' }],
'sans-h3-bold': ['24px', { lineHeight: '130%', fontWeight: '700', letterSpacing: '-0.005em' }],
'sans-h3-medium': ['24px', { lineHeight: '130%', fontWeight: '500', letterSpacing: '-0.005em' }],
'sans-h3': ['24px', { lineHeight: '130%', fontWeight: '400', letterSpacing: '-0.005em' }],
'sans-h4-bold': ['20px', { lineHeight: '140%', fontWeight: '700' }],
'sans-h4-medium': ['20px', { lineHeight: '140%', fontWeight: '500' }],
'sans-h4': ['20px', { lineHeight: '140%', fontWeight: '400' }],
// Sans Body 系列
'sans-body-large-bold': ['18px', { lineHeight: '150%', fontWeight: '700' }],
'sans-body-large-medium': ['18px', { lineHeight: '150%', fontWeight: '500' }],
'sans-body-large': ['18px', { lineHeight: '150%', fontWeight: '400' }],
'sans-body-default-bold': ['16px', { lineHeight: '150%', fontWeight: '700' }],
'sans-body-default-medium': ['16px', { lineHeight: '150%', fontWeight: '500' }],
'sans-body-default': ['16px', { lineHeight: '150%', fontWeight: '400' }],
'sans-body-small-bold': ['14px', { lineHeight: '150%', fontWeight: '700' }],
'sans-body-small-medium': ['14px', { lineHeight: '150%', fontWeight: '500' }],
'sans-body-small': ['14px', { lineHeight: '150%', fontWeight: '400' }],
// Sans Caption 系列
'sans-caption-tiny-bold': ['12px', { lineHeight: '150%', fontWeight: '700', letterSpacing: '0.01em' }],
'sans-caption-tiny-medium': ['12px', { lineHeight: '150%', fontWeight: '500', letterSpacing: '0.01em' }],
'sans-caption-tiny': ['12px', { lineHeight: '150%', fontWeight: '400', letterSpacing: '0.01em' }],
'sans-caption-minuscule-bold': ['10px', { lineHeight: '150%', fontWeight: '700', letterSpacing: '0.02em' }],
'sans-caption-minuscule-medium': ['10px', { lineHeight: '150%', fontWeight: '500', letterSpacing: '0.02em' }],
'sans-caption-minuscule': ['10px', { lineHeight: '150%', fontWeight: '400', letterSpacing: '0.02em' }],
// Mono Numeric 系列
'mono-numeric-giant-extrabold': ['48px', { lineHeight: '110%', fontWeight: '800', letterSpacing: '-0.01em' }],
'mono-numeric-giant-bold': ['48px', { lineHeight: '110%', fontWeight: '700', letterSpacing: '-0.01em' }],
'mono-numeric-giant-medium': ['48px', { lineHeight: '110%', fontWeight: '500', letterSpacing: '-0.01em' }],
'mono-numeric-giant': ['48px', { lineHeight: '110%', fontWeight: '400', letterSpacing: '-0.01em' }],
'mono-numeric-huge-extrabold': ['32px', { lineHeight: '120%', fontWeight: '800', letterSpacing: '-0.005em' }],
'mono-numeric-huge-bold': ['32px', { lineHeight: '120%', fontWeight: '700', letterSpacing: '-0.005em' }],
'mono-numeric-huge-medium': ['32px', { lineHeight: '120%', fontWeight: '500', letterSpacing: '-0.005em' }],
'mono-numeric-huge': ['32px', { lineHeight: '120%', fontWeight: '400', letterSpacing: '-0.005em' }],
'mono-numeric-large-extrabold': ['24px', { lineHeight: '130%', fontWeight: '800' }],
'mono-numeric-large-bold': ['24px', { lineHeight: '130%', fontWeight: '700' }],
'mono-numeric-large-medium': ['24px', { lineHeight: '130%', fontWeight: '500' }],
'mono-numeric-large': ['24px', { lineHeight: '130%', fontWeight: '400' }],
'mono-numeric-default-extrabold': ['16px', { lineHeight: '140%', fontWeight: '800' }],
'mono-numeric-default-bold': ['16px', { lineHeight: '140%', fontWeight: '700' }],
'mono-numeric-default-medium': ['16px', { lineHeight: '140%', fontWeight: '500' }],
'mono-numeric-default': ['16px', { lineHeight: '140%', fontWeight: '400' }],
// Mono Code 系列
'mono-code-small-extrabold': ['14px', { lineHeight: '150%', fontWeight: '800' }],
'mono-code-small-bold': ['14px', { lineHeight: '150%', fontWeight: '700' }],
'mono-code-small-medium': ['14px', { lineHeight: '150%', fontWeight: '500' }],
'mono-code-small': ['14px', { lineHeight: '150%', fontWeight: '400' }],
'mono-code-tiny-extrabold': ['12px', { lineHeight: '150%', fontWeight: '800' }],
'mono-code-tiny-bold': ['12px', { lineHeight: '150%', fontWeight: '700' }],
'mono-code-tiny-medium': ['12px', { lineHeight: '150%', fontWeight: '500' }],
'mono-code-tiny': ['12px', { lineHeight: '150%', fontWeight: '400' }],
'mono-code-minuscule-extrabold': ['10px', { lineHeight: '150%', fontWeight: '800' }],
'mono-code-minuscule-bold': ['10px', { lineHeight: '150%', fontWeight: '700' }],
'mono-code-minuscule-medium': ['10px', { lineHeight: '150%', fontWeight: '500' }],
'mono-code-minuscule': ['10px', { lineHeight: '150%', fontWeight: '400' }],
},
fontWeight: {
regular: '400',
medium: '500',
bold: '700',
extrabold: '800',
},
},
},
plugins: [heroui()],
} satisfies Config;

41
landingpage/tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}