打磨细节
62
DEV-GUIDE.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 开发指南
|
||||
|
||||
## 启动开发服务器
|
||||
|
||||
### 自动端口选择
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
脚本会自动尝试以下端口:
|
||||
- 3002 (默认)
|
||||
- 3003
|
||||
- 3004
|
||||
- 3005
|
||||
- 3006
|
||||
|
||||
如果以上端口都被占用,会随机选择一个3000-4000之间的端口。
|
||||
|
||||
### 结束所有开发进程
|
||||
|
||||
如果遇到端口占用问题,可以运行:
|
||||
```bash
|
||||
npm run dev:kill
|
||||
```
|
||||
|
||||
这会清理所有正在运行的Next.js开发服务器。
|
||||
|
||||
然后再运行:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 其他命令
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 启动生产服务器
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 端口被占用怎么办?**
|
||||
A: 直接运行 `npm run dev`,脚本会自动找到可用端口。如果还是不行,先运行 `npm run dev:kill` 清理进程。
|
||||
|
||||
**Q: 如何查看当前使用的端口?**
|
||||
A: 启动时会显示 "🚀 Starting dev server on port XXXX"
|
||||
|
||||
**Q: 如何强制使用特定端口?**
|
||||
A: 直接运行:
|
||||
```bash
|
||||
npx next dev -H 0.0.0.0 -p 3002
|
||||
```
|
||||
@@ -17,7 +17,7 @@
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: var(--font-inter), Arial, Helvetica, sans-serif;
|
||||
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;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -26,6 +26,19 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 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); }
|
||||
@@ -37,6 +50,10 @@ body {
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
.calculator-card-container {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calculator-card-container:hover {
|
||||
animation: wiggle 2.5s ease-in-out infinite;
|
||||
}
|
||||
@@ -69,3 +86,75 @@ body {
|
||||
.animate-slide-down {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutFast {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-out-fast {
|
||||
animation: fadeOutFast 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Shatter transition animation - 3D effect */
|
||||
@keyframes shatter {
|
||||
0%, 20% {
|
||||
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(var(--tx), var(--ty), -200px)
|
||||
rotateX(var(--rx))
|
||||
rotateY(var(--ry))
|
||||
scale(var(--s));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shatter-fragment {
|
||||
animation: shatter 0.9s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
|
||||
animation-delay: var(--delay);
|
||||
will-change: transform, opacity;
|
||||
transform-style: preserve-3d;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.glass-shard {
|
||||
transform-style: preserve-3d;
|
||||
transition: all 0.3s ease;
|
||||
/* 确保在动画开始时完全不透明 */
|
||||
background-color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
|
||||
/* Fade out animation for background mask */
|
||||
@keyframes fadeOut {
|
||||
0%, 30% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
animation: fadeOut 1.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono, Domine } from "next/font/google";
|
||||
import { Inter, JetBrains_Mono, Domine, Noto_Sans_SC, Noto_Serif_SC } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Providers from "@/components/Providers";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -21,6 +22,20 @@ const domine = Domine({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const notoSansSC = Noto_Sans_SC({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-sans-sc",
|
||||
weight: ["400", "500", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const notoSerifSC = Noto_Serif_SC({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-serif-sc",
|
||||
weight: ["400", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Asset Homepage",
|
||||
description: "Asset management platform homepage",
|
||||
@@ -32,9 +47,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" className={`${inter.variable} ${jetbrainsMono.variable} ${domine.variable}`}>
|
||||
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable} ${domine.variable} ${notoSansSC.variable} ${notoSerifSC.variable}`}>
|
||||
<body className="antialiased">
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
141
components/ConsenSysLogo.tsx
Normal file
@@ -1,30 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const productLinks = [
|
||||
'AX-Fund',
|
||||
'AX-Matrix',
|
||||
'Launchpad',
|
||||
'Liquid Market',
|
||||
'Token Factory'
|
||||
];
|
||||
|
||||
const resourceLinks = [
|
||||
'Docs',
|
||||
'FAQ & Support',
|
||||
'Trust & Security',
|
||||
'Learning Center',
|
||||
'Community Forum'
|
||||
];
|
||||
|
||||
const companyLinks = [
|
||||
'About Team',
|
||||
'Careers',
|
||||
'Contact Us',
|
||||
'Press & Media'
|
||||
];
|
||||
const { t } = useLanguage();
|
||||
|
||||
const socialIcons = [
|
||||
{ src: '/component-12.svg', alt: 'Twitter', width: 24, height: 24 },
|
||||
@@ -36,7 +16,7 @@ export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-white border-t border-[#e5e7eb] w-full flex flex-col items-center">
|
||||
{/* Main Footer Content */}
|
||||
<div className="w-full max-w-[1280px] px-6 py-20 flex flex-row gap-8">
|
||||
<div className="w-full max-w-[1440px] px-6 py-20 flex flex-row gap-8">
|
||||
|
||||
{/* Left Section - Logo, Address, Social Icons */}
|
||||
<div className="flex flex-col gap-6 w-[389.33px]">
|
||||
@@ -95,12 +75,12 @@ export default function Footer() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Products
|
||||
{t('footer.products')}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{productLinks.map((link, index) => (
|
||||
{[1, 2, 3, 4, 5].map((num) => (
|
||||
<a
|
||||
key={index}
|
||||
key={num}
|
||||
href="#"
|
||||
className="text-[#9ca1af] hover:text-[#111827] transition-colors font-inter cursor-pointer"
|
||||
style={{
|
||||
@@ -109,7 +89,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
{t(`footer.product${num}`)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -125,12 +105,12 @@ export default function Footer() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Resources
|
||||
{t('footer.resources')}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{resourceLinks.map((link, index) => (
|
||||
{[1, 2, 3, 4, 5].map((num) => (
|
||||
<a
|
||||
key={index}
|
||||
key={num}
|
||||
href="#"
|
||||
className="text-[#9ca1af] hover:text-[#111827] transition-colors font-inter cursor-pointer"
|
||||
style={{
|
||||
@@ -139,7 +119,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
{t(`footer.resource${num}`)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -155,12 +135,12 @@ export default function Footer() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Company
|
||||
{t('footer.company')}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{companyLinks.map((link, index) => (
|
||||
{[1, 2, 3, 4].map((num) => (
|
||||
<a
|
||||
key={index}
|
||||
key={num}
|
||||
href="#"
|
||||
className="text-[#9ca1af] hover:text-[#111827] transition-colors font-inter cursor-pointer"
|
||||
style={{
|
||||
@@ -169,7 +149,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
{t(`footer.company${num}`)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -178,7 +158,7 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - Copyright and Legal Links */}
|
||||
<div className="w-full max-w-[1280px] border-t border-[#f3f4f6] px-6 py-8 flex flex-row items-center justify-between">
|
||||
<div className="w-full max-w-[1440px] border-t border-[#f3f4f6] px-6 py-8 flex flex-row items-center justify-between">
|
||||
{/* Copyright */}
|
||||
<div
|
||||
className="text-[#9ca1af] font-inter"
|
||||
@@ -188,7 +168,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
© 2025 ASSETX Protocol. All rights reserved.
|
||||
{t('footer.copyright')}
|
||||
</div>
|
||||
|
||||
{/* Legal Links */}
|
||||
@@ -202,7 +182,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Privacy Policy
|
||||
{t('footer.privacy')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
@@ -213,7 +193,7 @@ export default function Footer() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Terms of Service
|
||||
{t('footer.terms')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
86
components/HeroButtons.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function HeroButtons() {
|
||||
const { t } = useLanguage();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('HeroButtons mounted');
|
||||
// 延迟一些时间后再触发按钮动画,形成渐进效果
|
||||
const timer = setTimeout(() => {
|
||||
console.log('HeroButtons animation starting');
|
||||
setMounted(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row gap-5 items-start justify-start flex-shrink-0 relative"
|
||||
style={{
|
||||
transform: mounted ? 'translateY(0)' : 'translateY(3rem)',
|
||||
opacity: mounted ? 1 : 0,
|
||||
transition: 'all 1s ease-out'
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{/* .component-82 - Start Investing Button */}
|
||||
<button
|
||||
className="bg-white flex flex-row gap-2 items-center justify-center flex-shrink-0 h-[60px] relative overflow-hidden hover:bg-[#f3f4f6] transition-colors"
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
padding: '0px 32px'
|
||||
}}
|
||||
>
|
||||
{/* .text4 */}
|
||||
<div
|
||||
className="text-[#111827] text-center relative flex items-center justify-center font-inter"
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{t('hero.startInvesting')}
|
||||
</div>
|
||||
|
||||
{/* .component-1 - Arrow icon */}
|
||||
<Image
|
||||
src="/component-10.svg"
|
||||
alt="Arrow"
|
||||
width={20}
|
||||
height={20}
|
||||
className="flex-shrink-0 w-5 h-5 relative overflow-visible"
|
||||
style={{ aspectRatio: 1 }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* .component-9 - Read the Whitepaper Button */}
|
||||
<button
|
||||
className="border border-white/20 flex flex-row gap-2 items-center justify-center self-stretch flex-shrink-0 relative overflow-hidden hover:bg-white/70 hover:border-white/60 transition-all"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '4px',
|
||||
padding: '12px 32px',
|
||||
backdropFilter: 'blur(30px)'
|
||||
}}
|
||||
>
|
||||
{/* .text5 */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-center relative flex items-center justify-center font-inter"
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{t('hero.readWhitepaper')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import HeroTitle from './HeroTitle';
|
||||
import HeroButtons from './HeroButtons';
|
||||
|
||||
export default function HeroSection() {
|
||||
|
||||
return (
|
||||
<section className="relative w-full">
|
||||
<section className="relative w-full" style={{ overflow: 'hidden' }} suppressHydrationWarning>
|
||||
{/* Background Video */}
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
style={{ zIndex: 0 }}
|
||||
>
|
||||
<source src="/hero-background.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
{/* .header - 背景容器 */}
|
||||
<div
|
||||
className="flex flex-col items-start justify-center flex-shrink-0 h-[670px] relative"
|
||||
className="flex flex-col items-start justify-center flex-shrink-0 relative"
|
||||
style={{
|
||||
background: 'linear-gradient(to left, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55))',
|
||||
padding: '114px 85px'
|
||||
padding: '114px 85px 164px 85px',
|
||||
minHeight: '670px',
|
||||
zIndex: 1
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{/* .frame-21 - 主内容容器 */}
|
||||
<div className="flex flex-col gap-[60px] flex-shrink-0 w-[926px] relative">
|
||||
|
||||
{/* .frame-25 - 标题和描述区域 */}
|
||||
<div className="flex flex-col gap-8 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
|
||||
{/* .frame-22 - 标题容器 */}
|
||||
<div className="flex flex-col gap-4 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
{/* .yield-bearing-asset */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative self-stretch font-domine"
|
||||
style={{
|
||||
fontSize: '100px',
|
||||
lineHeight: '100%',
|
||||
letterSpacing: '-0.03em',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Yield-Bearing Asset
|
||||
</div>
|
||||
|
||||
{/* .on-chain */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative w-[926px] h-[100px] font-domine"
|
||||
style={{
|
||||
fontSize: '100px',
|
||||
lineHeight: '100%',
|
||||
letterSpacing: '-0.03em',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
On Chain.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description text */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative w-[488px] flex items-center justify-start font-domine"
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Access Yield- Bearing Asset like Equities, Real Estate, and
|
||||
Commercial Loans with low barriers. Enjoy 10%-30% APY backed by
|
||||
real assets.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* .frame-23 - 按钮容器 */}
|
||||
<div className="flex flex-row gap-5 items-start justify-start flex-shrink-0 relative">
|
||||
|
||||
{/* .component-82 - Start Investing Button */}
|
||||
<button
|
||||
className="bg-white flex flex-row gap-2 items-center justify-center flex-shrink-0 h-[60px] relative overflow-hidden hover:opacity-90 transition-opacity"
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
padding: '0px 32px'
|
||||
}}
|
||||
>
|
||||
{/* .text4 */}
|
||||
<div
|
||||
className="text-[#111827] text-center relative flex items-center justify-center font-inter"
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Start Investing
|
||||
</div>
|
||||
|
||||
{/* .component-1 - Arrow icon */}
|
||||
<Image
|
||||
src="/component-10.svg"
|
||||
alt="Arrow"
|
||||
width={20}
|
||||
height={20}
|
||||
className="flex-shrink-0 w-5 h-5 relative overflow-visible"
|
||||
style={{ aspectRatio: 1 }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* .component-9 - Read the Whitepaper Button */}
|
||||
<button
|
||||
className="border border-white/20 flex flex-row gap-2 items-center justify-center self-stretch flex-shrink-0 relative overflow-hidden hover:bg-white/20 transition-colors"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '4px',
|
||||
padding: '12px 32px',
|
||||
backdropFilter: 'blur(30px)'
|
||||
}}
|
||||
>
|
||||
{/* .text5 */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-center relative flex items-center justify-center font-inter"
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Read the Whitepaper
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<HeroTitle />
|
||||
<HeroButtons />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
71
components/HeroTitle.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function HeroTitle() {
|
||||
const { t } = useLanguage();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('HeroTitle mounted');
|
||||
const timer = setTimeout(() => {
|
||||
console.log('HeroTitle animation starting');
|
||||
setMounted(true);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-8 items-start justify-start self-stretch flex-shrink-0 relative"
|
||||
style={{
|
||||
transform: mounted ? 'translateY(0)' : 'translateY(3rem)',
|
||||
opacity: mounted ? 1 : 0,
|
||||
transition: 'all 1s ease-out'
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{/* .frame-22 - 标题容器 */}
|
||||
<div className="flex flex-col gap-4 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
{/* .yield-bearing-asset */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative self-stretch font-domine"
|
||||
style={{
|
||||
fontSize: '100px',
|
||||
lineHeight: '100%',
|
||||
letterSpacing: '-0.03em',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{t('hero.title1')}
|
||||
</div>
|
||||
|
||||
{/* .on-chain */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative w-[926px] h-[100px] font-domine"
|
||||
style={{
|
||||
fontSize: '100px',
|
||||
lineHeight: '100%',
|
||||
letterSpacing: '-0.03em',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{t('hero.title2')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description text */}
|
||||
<div
|
||||
className="text-[#fcfcfd] text-left relative w-[488px] flex items-center justify-start font-domine"
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
lineHeight: '150%',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{t('hero.description')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
// 数字增长动画Hook
|
||||
function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [count, setCount] = useState(end);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const startValueRef = useRef<number>(end);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -22,8 +23,7 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !hasAnimated) {
|
||||
setHasAnimated(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
const start = startValueRef.current;
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -42,7 +42,13 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
}
|
||||
}, 16);
|
||||
|
||||
animationRef.current = timer as unknown as number;
|
||||
return () => clearInterval(timer);
|
||||
} else {
|
||||
if (animationRef.current) {
|
||||
clearInterval(animationRef.current);
|
||||
}
|
||||
setCount(startValueRef.current);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
@@ -53,14 +59,14 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [end, duration, hasAnimated, mounted]);
|
||||
}, [end, duration, mounted]);
|
||||
|
||||
return { count, elementRef };
|
||||
}
|
||||
|
||||
export default function HowItWorksSection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const [activeStep, setActiveStep] = useState(1); // 默认第1步激活
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
@@ -69,49 +75,40 @@ export default function HowItWorksSection() {
|
||||
const earnAmount = useCountUp(5150, 1500, 0.85);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const currentRef = sectionRef.current;
|
||||
if (!currentRef) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !visible) {
|
||||
setVisible(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
setAnimate(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, [visible]);
|
||||
observer.observe(currentRef);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: 1,
|
||||
title: 'Deposit & Mint WUSD',
|
||||
description: 'Connect your wallet and deposit USDC to swap WUSD. This serves as the native stablecoin and gateway to the entire AssetX ecosystem.',
|
||||
title: t('how.step1.title'),
|
||||
description: t('how.step1.desc'),
|
||||
hasLine: true
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Dual Investment Options',
|
||||
description: 'Choose your strategy: use WUSD to purchase specific Yield-Bearing Assets for real-world returns, or provide liquidity in DeFi Pools to earn trading fees.',
|
||||
title: t('how.step2.title'),
|
||||
description: t('how.step2.desc'),
|
||||
hasLine: true
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Earn & Boost',
|
||||
description: 'Receive daily yield distributions. Use Asset Tokens as collateral to borrow WUSD and leverage up to 2.5x for maximized returns.',
|
||||
title: t('how.step3.title'),
|
||||
description: t('how.step3.desc'),
|
||||
hasLine: false
|
||||
}
|
||||
];
|
||||
@@ -128,7 +125,7 @@ export default function HowItWorksSection() {
|
||||
}}
|
||||
>
|
||||
{/* .container20 - Main container */}
|
||||
<div className="flex flex-row items-start justify-between flex-shrink-0 relative w-[1200px]">
|
||||
<div className="flex flex-row items-start justify-between flex-shrink-0 relative w-[1440px]">
|
||||
|
||||
{/* Left Side - Steps (.container21) */}
|
||||
<div className="flex flex-col gap-10 items-start justify-start flex-shrink-0 relative w-[520px]">
|
||||
@@ -144,7 +141,7 @@ export default function HowItWorksSection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
How it works
|
||||
{t('how.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +156,7 @@ export default function HowItWorksSection() {
|
||||
key={step.number}
|
||||
onClick={() => setActiveStep(step.number)}
|
||||
className={`flex flex-row gap-6 items-start justify-start flex-shrink-0 relative cursor-pointer transition-all duration-300 ease-out hover:opacity-80 ${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-x-0 opacity-100'
|
||||
: '-translate-x-12 opacity-0'
|
||||
}`}
|
||||
@@ -224,12 +221,13 @@ export default function HowItWorksSection() {
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="text-[#111827] text-left relative flex items-center justify-start font-inter transition-all duration-300"
|
||||
className="text-left relative flex items-center justify-start font-inter transition-all duration-300"
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontSize: '28px',
|
||||
lineHeight: '130%',
|
||||
letterSpacing: '-0.005em',
|
||||
fontWeight: isActive ? 700 : 500
|
||||
fontWeight: 600,
|
||||
color: isActive ? '#111827' : '#6b7280'
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
@@ -251,28 +249,31 @@ export default function HowItWorksSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Calculator Card */}
|
||||
{/* Right Side - Dynamic Cards Container */}
|
||||
<div
|
||||
className={`calculator-card-container flex flex-col items-start justify-start flex-shrink-0 w-[558px] relative transition-all duration-700 ease-out ${
|
||||
mounted && visible
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}`}
|
||||
className="calculator-card-container flex flex-col items-start justify-start flex-shrink-0 w-[558px] relative"
|
||||
style={{
|
||||
transitionDelay: '450ms',
|
||||
height: '439px'
|
||||
}}
|
||||
>
|
||||
{/* Step 1: Deposit & Mint WUSD Card */}
|
||||
<>
|
||||
{/* Coin Images Container - Rotated Card */}
|
||||
<div
|
||||
className="bg-[#111827] rounded-[16px] flex flex-row gap-4 items-start justify-start h-[162px] absolute z-0"
|
||||
className={`bg-[#111827] rounded-[16px] flex flex-row gap-4 items-start justify-start h-[162px] absolute z-0 transition-all duration-700 ease-out ${
|
||||
animate && activeStep === 1 ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
padding: '25px 25px 1px 25px',
|
||||
left: '205.43px',
|
||||
top: '15.96px',
|
||||
boxShadow: '0px 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
transformOrigin: '0 0',
|
||||
transform: 'rotate(6.535deg) scale(1, 1)'
|
||||
transform: animate && activeStep === 1
|
||||
? 'rotate(6.535deg) scale(1, 1) translateY(0)'
|
||||
: 'rotate(6.535deg) scale(1, 1) translateY(3rem)',
|
||||
transitionDelay: activeStep === 1 ? '200ms' : '0ms',
|
||||
pointerEvents: activeStep === 1 ? 'auto' : 'none'
|
||||
}}
|
||||
>
|
||||
{/* USDC Logo */}
|
||||
@@ -299,10 +300,16 @@ export default function HowItWorksSection() {
|
||||
|
||||
{/* Calculator Card */}
|
||||
<div
|
||||
className="bg-white/85 backdrop-blur-md rounded-[24px] border border-[#e5e7eb] p-8 absolute left-0 top-[99px] w-full shadow-lg z-10"
|
||||
className={`bg-white/85 backdrop-blur-md rounded-[24px] border border-[#e5e7eb] p-8 absolute left-0 top-[99px] w-full shadow-lg z-10 transition-all duration-700 ease-out ${
|
||||
animate && activeStep === 1 ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
transform: 'rotate(-3deg)',
|
||||
transformOrigin: 'center center'
|
||||
transform: animate && activeStep === 1
|
||||
? 'rotate(-3deg) translateY(0)'
|
||||
: 'rotate(-3deg) translateY(3rem)',
|
||||
transformOrigin: 'center center',
|
||||
transitionDelay: activeStep === 1 ? '200ms' : '0ms',
|
||||
pointerEvents: activeStep === 1 ? 'auto' : 'none'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -325,7 +332,7 @@ export default function HowItWorksSection() {
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
Portfolio Overview Simulator
|
||||
{t('how.simulator.title')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -345,7 +352,7 @@ export default function HowItWorksSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
If you invest
|
||||
{t('how.simulator.invest')}
|
||||
</span>
|
||||
<span
|
||||
ref={investAmount.elementRef}
|
||||
@@ -377,7 +384,7 @@ export default function HowItWorksSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
You earn per year
|
||||
{t('how.simulator.earn')}
|
||||
</span>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Image
|
||||
@@ -401,6 +408,286 @@ export default function HowItWorksSection() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Step 2: Dual Investment Options Card */}
|
||||
<div
|
||||
className={`bg-white rounded-[16px] border border-[#f3f4f6] p-6 flex flex-col gap-6 absolute left-0 top-[99px] w-full transition-all duration-700 ease-out ${
|
||||
animate && activeStep === 2
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
boxShadow: '0px 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
transform: animate && activeStep === 2
|
||||
? 'rotate(3deg) translateY(0)'
|
||||
: 'rotate(3deg) translateY(3rem)',
|
||||
transformOrigin: '0 0',
|
||||
transitionDelay: activeStep === 2 ? '200ms' : '0ms',
|
||||
pointerEvents: activeStep === 2 ? 'auto' : 'none'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="border-b border-[#f3f4f6] pb-4 flex flex-row items-center justify-between w-full"
|
||||
>
|
||||
<span className="text-[#000000] font-inter text-[18px] font-bold leading-7">
|
||||
Fund Market
|
||||
</span>
|
||||
<span className="text-[#059669] font-inter text-[16px] font-bold leading-6">
|
||||
Connect Wallet
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Investment Options Container */}
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{/* Option 1 - Active */}
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-row gap-4 items-center h-24">
|
||||
{/* Icon */}
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold">O</span>
|
||||
</div>
|
||||
|
||||
{/* Text Placeholders */}
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<div className="bg-[#e5e7eb] rounded h-4 w-24" />
|
||||
<div className="bg-[#f3f4f6] rounded h-3 w-16" />
|
||||
</div>
|
||||
|
||||
{/* APY Info */}
|
||||
<div className="flex flex-col gap-2 items-end flex-shrink-0">
|
||||
<div className="bg-[#e5e7eb] rounded h-4 w-20" />
|
||||
<span className="text-[#059669] font-inter text-[16px] font-bold leading-6">
|
||||
APY 30.2%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 2 - Inactive */}
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-row gap-4 items-center h-24 opacity-60">
|
||||
{/* Icon */}
|
||||
<div className="bg-[#d1d5db] rounded-full w-12 h-12 flex-shrink-0" />
|
||||
|
||||
{/* Text Placeholders */}
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<div className="bg-[#e5e7eb] rounded h-4 w-32" />
|
||||
<div className="bg-[#f3f4f6] rounded h-3 w-20" />
|
||||
</div>
|
||||
|
||||
{/* APY Info */}
|
||||
<div className="flex flex-col gap-2 items-end flex-shrink-0">
|
||||
<div className="bg-[#e5e7eb] rounded h-4 w-20" />
|
||||
<span className="text-[#059669] font-inter text-[16px] font-bold leading-6">
|
||||
APY 15.2%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 3: Earn & Boost Card */}
|
||||
<div
|
||||
className={`bg-white rounded-[16px] border border-[#f3f4f6] p-6 flex flex-col gap-6 absolute left-0 top-[99px] w-[520px] transition-all duration-700 ease-out ${
|
||||
animate && activeStep === 3 ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
boxShadow: '0px 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
transform: animate && activeStep === 3
|
||||
? 'rotate(3deg) translateY(0)'
|
||||
: 'rotate(3deg) translateY(3rem)',
|
||||
transformOrigin: '0 0',
|
||||
transitionDelay: activeStep === 3 ? '200ms' : '0ms',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: activeStep === 3 ? 'auto' : 'none'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="border-b border-[#f3f4f6] pb-[17px] flex flex-row items-center justify-between" style={{ width: '470px', height: '45px' }}>
|
||||
<span className="text-[#000000] font-inter text-[18px] font-bold leading-7">
|
||||
Defi
|
||||
</span>
|
||||
<span className="text-[#059669] font-inter text-[16px] font-bold leading-6">
|
||||
+5.2% APY
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cards Grid Container */}
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
width: '466px',
|
||||
height: '195px',
|
||||
transform: 'rotate(-3deg)',
|
||||
transformOrigin: '0 0',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* Container 1 */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
width: '135.53px',
|
||||
height: '165.15px',
|
||||
left: '8.58px',
|
||||
top: '0.46px',
|
||||
transform: 'rotate(3deg)',
|
||||
transformOrigin: '0 0'
|
||||
}}
|
||||
>
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold leading-6">O</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[#000000] font-inter text-[16px] font-bold text-center"
|
||||
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
+10% APY
|
||||
</span>
|
||||
<div
|
||||
className="bg-[#000000] rounded-lg px-4 py-2"
|
||||
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
|
||||
>
|
||||
<span className="text-white font-inter text-[12px] font-bold leading-4">
|
||||
Boost
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container 2 */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
width: '135.53px',
|
||||
height: '165.15px',
|
||||
left: '160.18px',
|
||||
top: '11.66px',
|
||||
transform: 'rotate(3deg)',
|
||||
transformOrigin: '0 0'
|
||||
}}
|
||||
>
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold leading-6">O</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[#000000] font-inter text-[16px] font-bold text-center"
|
||||
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
+10% APY
|
||||
</span>
|
||||
<div
|
||||
className="bg-[#000000] rounded-lg px-4 py-2"
|
||||
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
|
||||
>
|
||||
<span className="text-white font-inter text-[12px] font-bold leading-4">
|
||||
Boost
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container 3 */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
width: '135.53px',
|
||||
height: '165.15px',
|
||||
left: '312.18px',
|
||||
top: '19.66px',
|
||||
transform: 'rotate(3deg)',
|
||||
transformOrigin: '0 0'
|
||||
}}
|
||||
>
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold leading-6">O</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[#000000] font-inter text-[16px] font-bold text-center"
|
||||
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
+10% APY
|
||||
</span>
|
||||
<div
|
||||
className="bg-[#000000] rounded-lg px-4 py-2"
|
||||
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
|
||||
>
|
||||
<span className="text-white font-inter text-[12px] font-bold leading-4">
|
||||
Boost
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container 4 - with SWAP button */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
width: '135.53px',
|
||||
height: '165.15px',
|
||||
left: '160.18px',
|
||||
top: '11.66px',
|
||||
transform: 'rotate(3deg)',
|
||||
transformOrigin: '0 0'
|
||||
}}
|
||||
>
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold leading-6">O</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[#000000] font-inter text-[16px] font-bold text-center"
|
||||
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
Get USDC
|
||||
</span>
|
||||
<div
|
||||
className="bg-[#000000] rounded-lg px-4 py-2"
|
||||
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
|
||||
>
|
||||
<span className="text-white font-inter text-[12px] font-bold leading-4">
|
||||
SWAP
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container 5 - with LP button */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
width: '135.53px',
|
||||
height: '165.15px',
|
||||
left: '312.18px',
|
||||
top: '19.66px',
|
||||
transform: 'rotate(3deg)',
|
||||
transformOrigin: '0 0'
|
||||
}}
|
||||
>
|
||||
<div className="bg-[#f9fafb] rounded-[12px] border border-[#f3f4f6] p-4 flex flex-col gap-4 items-center absolute" style={{ width: '134.91px', height: '165.18px', left: '0px', top: '0px' }}>
|
||||
<div className="bg-[#000000] rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-inter text-[16px] font-bold leading-6">O</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[#000000] font-inter text-[16px] font-bold text-center"
|
||||
style={{ width: '124px', height: '17px', transform: 'rotate(-3deg)', transformOrigin: '0 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
10% APY
|
||||
</span>
|
||||
<div
|
||||
className="bg-[#000000] rounded-lg px-4 py-2"
|
||||
style={{ transform: 'rotate(-3deg)', transformOrigin: '0 0' }}
|
||||
>
|
||||
<span className="text-white font-inter text-[12px] font-bold leading-4">
|
||||
LP
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,15 @@ import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import ProductMenu from './ProductMenu';
|
||||
import ResourceMenu from './ResourceMenu';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [language, setLanguage] = useState<'zh' | 'en'>('zh');
|
||||
const { language, setLanguage, t } = useLanguage();
|
||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||
const [showProductMenu, setShowProductMenu] = useState(false);
|
||||
const [showResourceMenu, setShowResourceMenu] = useState(false);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@@ -21,6 +23,15 @@ export default function Navbar() {
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 页面加载后触发动画
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setAnimate(true);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Element;
|
||||
@@ -52,7 +63,14 @@ export default function Navbar() {
|
||||
: 'bg-white/90 backdrop-blur-[50px]'
|
||||
}`}>
|
||||
{/* Logo Section */}
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className="flex-1 transition-all duration-1000 ease-out"
|
||||
style={{
|
||||
transform: animate ? 'translateY(0)' : 'translateY(-3rem)',
|
||||
opacity: animate ? 1 : 0
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<Image
|
||||
src="/logo0.svg"
|
||||
alt="Logo"
|
||||
@@ -68,7 +86,7 @@ export default function Navbar() {
|
||||
{/* Product - Active */}
|
||||
<div className="product-menu-container">
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 h-9 flex items-center justify-center cursor-pointer transition-colors ${
|
||||
className={`rounded-lg px-4 py-2 h-9 flex items-center justify-center cursor-pointer transition-colors select-none ${
|
||||
showProductMenu ? 'bg-[#f3f4f6]' : 'bg-transparent hover:bg-[#f3f4f6]'
|
||||
}`}
|
||||
onClick={() => {
|
||||
@@ -79,22 +97,22 @@ export default function Navbar() {
|
||||
<span className={`font-bold text-sm leading-[150%] font-inter ${
|
||||
showProductMenu ? 'text-[#111827]' : 'text-[#4b5563]'
|
||||
}`}>
|
||||
{language === 'zh' ? '产品' : 'Product'}
|
||||
{t('nav.product')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ecosystem */}
|
||||
<div className="bg-transparent rounded-lg px-4 py-2 h-9 flex items-center justify-center hover:bg-[#f3f4f6] transition-colors cursor-pointer">
|
||||
{/* Community */}
|
||||
<div className="bg-transparent rounded-lg px-4 py-2 h-9 flex items-center justify-center hover:bg-[#f3f4f6] transition-colors cursor-pointer select-none">
|
||||
<span className="text-[#4b5563] font-bold text-sm leading-[150%] font-inter">
|
||||
{language === 'zh' ? '生态' : 'Ecosystem'}
|
||||
{t('nav.community')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Resource */}
|
||||
<div className="resource-menu-container">
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 h-9 flex items-center justify-center cursor-pointer transition-colors ${
|
||||
className={`rounded-lg px-4 py-2 h-9 flex items-center justify-center cursor-pointer transition-colors select-none ${
|
||||
showResourceMenu ? 'bg-[#f3f4f6]' : 'bg-transparent hover:bg-[#f3f4f6]'
|
||||
}`}
|
||||
onClick={() => {
|
||||
@@ -105,7 +123,7 @@ export default function Navbar() {
|
||||
<span className={`font-bold text-sm leading-[150%] font-inter ${
|
||||
showResourceMenu ? 'text-[#111827]' : 'text-[#4b5563]'
|
||||
}`}>
|
||||
{language === 'zh' ? '资源' : 'Resource'}
|
||||
{t('nav.resource')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,16 +132,25 @@ export default function Navbar() {
|
||||
|
||||
{/* Launch App Button & Language Selector */}
|
||||
<div className="flex-1 flex justify-end items-center gap-4">
|
||||
<div className="bg-[#111827] rounded-lg px-5 py-2.5 h-10 flex items-center justify-center overflow-hidden cursor-pointer hover:opacity-90 transition-opacity">
|
||||
<div
|
||||
className="bg-[#111827] rounded-lg px-5 py-2.5 h-10 flex items-center justify-center overflow-hidden cursor-pointer hover:opacity-90 select-none"
|
||||
style={{
|
||||
transform: animate ? 'translateY(0)' : 'translateY(-3rem)',
|
||||
opacity: animate ? 1 : 0,
|
||||
transition: 'all 1s ease-out',
|
||||
transitionDelay: '200ms'
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<span className="text-[#fcfcfd] font-bold text-sm leading-[150%] font-inter">
|
||||
{language === 'zh' ? '启动应用' : 'Launch App'}
|
||||
{t('nav.launchApp')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="relative language-selector">
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-[#f3f4f6] transition-colors cursor-pointer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-[#f3f4f6] transition-colors cursor-pointer select-none"
|
||||
onClick={() => setShowLangMenu(!showLangMenu)}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -143,7 +170,7 @@ export default function Navbar() {
|
||||
{showLangMenu && (
|
||||
<div className="absolute right-0 mt-2 w-32 bg-white rounded-lg shadow-lg border border-[#e5e7eb] overflow-hidden z-50">
|
||||
<div
|
||||
className={`px-4 py-2.5 hover:bg-[#f3f4f6] cursor-pointer transition-colors ${
|
||||
className={`px-4 py-2.5 hover:bg-[#f3f4f6] cursor-pointer transition-colors select-none ${
|
||||
language === 'zh' ? 'bg-[#f3f4f6]' : ''
|
||||
}`}
|
||||
onClick={() => toggleLanguage('zh')}
|
||||
@@ -151,7 +178,7 @@ export default function Navbar() {
|
||||
<span className="text-[#111827] font-inter text-sm font-medium">中文</span>
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2.5 hover:bg-[#f3f4f6] cursor-pointer transition-colors ${
|
||||
className={`px-4 py-2.5 hover:bg-[#f3f4f6] cursor-pointer transition-colors select-none ${
|
||||
language === 'en' ? 'bg-[#f3f4f6]' : ''
|
||||
}`}
|
||||
onClick={() => toggleLanguage('en')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface ProductMenuProps {
|
||||
isOpen: boolean;
|
||||
@@ -9,7 +10,26 @@ interface ProductMenuProps {
|
||||
}
|
||||
|
||||
export default function ProductMenu({ isOpen, onClose, language }: ProductMenuProps) {
|
||||
if (!isOpen) return null;
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
setIsClosing(false);
|
||||
} else if (isVisible) {
|
||||
// 触发退出动画
|
||||
setIsClosing(true);
|
||||
// 动画结束后隐藏组件
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setIsClosing(false);
|
||||
}, 300); // 匹配动画时长
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, isVisible]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const content = {
|
||||
zh: {
|
||||
@@ -68,12 +88,12 @@ export default function ProductMenu({ isOpen, onClose, language }: ProductMenuPr
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 z-40 animate-fade-in"
|
||||
className={`fixed inset-0 bg-black/20 z-40 ${isClosing ? 'animate-fade-out-fast' : 'animate-fade-in'}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Menu */}
|
||||
<div className="fixed left-1/2 -translate-x-1/2 bg-white rounded-2xl shadow-xl border border-[#e5e7eb] p-6 z-50 animate-slide-down"
|
||||
<div className={`fixed left-1/2 -translate-x-1/2 bg-white rounded-2xl shadow-xl border border-[#e5e7eb] p-6 z-50 ${isClosing ? 'animate-slide-up' : 'animate-slide-down'}`}
|
||||
style={{ width: '1080px', top: '90px' }}
|
||||
>
|
||||
<div className="flex flex-row gap-8">
|
||||
|
||||
15
components/Providers.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||
import TransitionWrapper from './TransitionWrapper';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<TransitionWrapper>
|
||||
{children}
|
||||
</TransitionWrapper>
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface ResourceMenuProps {
|
||||
isOpen: boolean;
|
||||
@@ -9,7 +10,26 @@ interface ResourceMenuProps {
|
||||
}
|
||||
|
||||
export default function ResourceMenu({ isOpen, onClose, language }: ResourceMenuProps) {
|
||||
if (!isOpen) return null;
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
setIsClosing(false);
|
||||
} else if (isVisible) {
|
||||
// 触发退出动画
|
||||
setIsClosing(true);
|
||||
// 动画结束后隐藏组件
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setIsClosing(false);
|
||||
}, 300); // 匹配动画时长
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, isVisible]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const content = {
|
||||
zh: {
|
||||
@@ -76,12 +96,12 @@ export default function ResourceMenu({ isOpen, onClose, language }: ResourceMenu
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 z-40 animate-fade-in"
|
||||
className={`fixed inset-0 bg-black/20 z-40 ${isClosing ? 'animate-fade-out-fast' : 'animate-fade-in'}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Menu */}
|
||||
<div className="fixed left-1/2 -translate-x-1/2 bg-white rounded-2xl shadow-xl border border-[#e5e7eb] p-6 z-50 animate-slide-down"
|
||||
<div className={`fixed left-1/2 -translate-x-1/2 bg-white rounded-2xl shadow-xl border border-[#e5e7eb] p-6 z-50 ${isClosing ? 'animate-slide-up' : 'animate-slide-down'}`}
|
||||
style={{ width: '1080px', top: '90px' }}
|
||||
>
|
||||
<div className="flex flex-row gap-8">
|
||||
@@ -98,7 +118,7 @@ export default function ResourceMenu({ isOpen, onClose, language }: ResourceMenu
|
||||
<div className="bg-transparent hover:bg-[#f9fafb] border border-transparent hover:border-[#e5e7eb] rounded-xl p-3 flex gap-2 cursor-pointer transition-all">
|
||||
<div className="bg-[#111827] rounded-xl w-12 h-12 flex items-center justify-center flex-shrink-0">
|
||||
<Image
|
||||
src="/component-10.svg"
|
||||
src="/icon-book.svg"
|
||||
alt="Docs"
|
||||
width={24}
|
||||
height={24}
|
||||
|
||||
@@ -2,62 +2,54 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function SecuritySection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const currentRef = sectionRef.current;
|
||||
if (!currentRef) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !visible) {
|
||||
setVisible(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
setAnimate(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, [visible]);
|
||||
observer.observe(currentRef);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: '/interface-search-magnifying-glass0.svg',
|
||||
title: 'Audited',
|
||||
description: 'Smart contracts audited by top-tier firms. Underlying assets undergo strict due diligence and financial auditing.',
|
||||
titleKey: 'security.audited.title',
|
||||
descKey: 'security.audited.desc',
|
||||
position: 'top-left'
|
||||
},
|
||||
{
|
||||
icon: '/navigation-building-010.svg',
|
||||
title: 'Segregated',
|
||||
description: 'SPV Setup. User assets are held in segregated Special Purpose Vehicles, isolated from platform risks.',
|
||||
titleKey: 'security.segregated.title',
|
||||
descKey: 'security.segregated.desc',
|
||||
position: 'top-right'
|
||||
},
|
||||
{
|
||||
icon: '/interface-chart-bar-vertical-010.svg',
|
||||
title: 'Transparency',
|
||||
description: 'Real-Time NAV. Fund performance data and asset valuations are publicly available and updated on-chain.',
|
||||
titleKey: 'security.transparency.title',
|
||||
descKey: 'security.transparency.desc',
|
||||
position: 'bottom-left'
|
||||
},
|
||||
{
|
||||
icon: '/component-11.svg',
|
||||
title: 'Powered By NASDAQ & HKEX Listed Partners',
|
||||
description: null,
|
||||
titleKey: 'security.partners.title',
|
||||
descKey: null,
|
||||
position: 'bottom-right',
|
||||
special: true
|
||||
}
|
||||
@@ -78,7 +70,7 @@ export default function SecuritySection() {
|
||||
{/* Left Side - Title */}
|
||||
<div
|
||||
className={`flex flex-row items-center justify-center flex-shrink-0 w-[459px] h-[604px] relative transition-all duration-700 ease-out ${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-x-0 opacity-100'
|
||||
: '-translate-x-12 opacity-0'
|
||||
}`}
|
||||
@@ -99,7 +91,7 @@ export default function SecuritySection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Security First Architecture
|
||||
{t('security.title')}
|
||||
</h2>
|
||||
<p
|
||||
className="text-[#9ca1af] text-left relative w-[290px] flex items-center justify-start font-domine"
|
||||
@@ -109,7 +101,7 @@ export default function SecuritySection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Real-time data transparency with segregated asset management.
|
||||
{t('security.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,9 +117,9 @@ export default function SecuritySection() {
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
key={feature.titleKey}
|
||||
className={`flex flex-col items-start justify-center relative transition-all duration-700 ease-out ${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}`}
|
||||
@@ -158,7 +150,7 @@ export default function SecuritySection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{feature.title}
|
||||
{t(feature.titleKey)}
|
||||
</h3>
|
||||
<Image
|
||||
src={feature.icon}
|
||||
@@ -174,7 +166,7 @@ export default function SecuritySection() {
|
||||
<div className="flex flex-col gap-6 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
<Image
|
||||
src={feature.icon}
|
||||
alt={feature.title}
|
||||
alt={t(feature.titleKey)}
|
||||
width={32}
|
||||
height={32}
|
||||
className="flex-shrink-0"
|
||||
@@ -189,7 +181,7 @@ export default function SecuritySection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{feature.title}
|
||||
{t(feature.titleKey)}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[#9ca1af] text-left relative self-stretch font-inter"
|
||||
@@ -199,7 +191,7 @@ export default function SecuritySection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{feature.description}
|
||||
{feature.descKey && t(feature.descKey)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
103
components/ShatterTransition.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
interface ShatterTransitionProps {
|
||||
isActive: boolean;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
// 增加网格密度以获得更细腻的破碎感
|
||||
const GRID_COLS = 10;
|
||||
const GRID_ROWS = 10;
|
||||
|
||||
export default function ShatterTransition({ isActive, onComplete }: ShatterTransitionProps) {
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
setIsAnimating(true);
|
||||
// 总动画时间 = 最大延迟 + 单个动画时长
|
||||
// 这里预留充足时间确保最后一片落完
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
onComplete();
|
||||
}, 1500);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
}, [isActive, onComplete]);
|
||||
|
||||
// 预先计算碎片数据,避免重渲染时的性能抖动
|
||||
const fragments = useMemo(() => {
|
||||
return Array.from({ length: GRID_COLS * GRID_ROWS }, (_, i) => {
|
||||
const col = i % GRID_COLS;
|
||||
const row = Math.floor(i / GRID_COLS);
|
||||
|
||||
// 核心算法:计算距离左上角的距离 (欧几里得距离)
|
||||
// 距离越远,delay 越大
|
||||
const distance = Math.sqrt(col * col + row * row);
|
||||
const maxDistance = Math.sqrt(GRID_COLS * GRID_COLS + GRID_ROWS * GRID_ROWS);
|
||||
const normalizedDistance = distance / maxDistance;
|
||||
|
||||
// 爆炸参数
|
||||
const randomX = (Math.random() - 0.5) * 200; // X轴偏移
|
||||
const randomY = Math.random() * 300 + 100; // Y轴主要向下掉落
|
||||
const randomRotateX = (Math.random() - 0.5) * 720; // 剧烈翻滚
|
||||
const randomRotateY = (Math.random() - 0.5) * 720;
|
||||
const randomScale = 0.5 + Math.random() * 0.5; // 碎片大小不一
|
||||
|
||||
return {
|
||||
id: i,
|
||||
col,
|
||||
row,
|
||||
// 延迟时间:基准延迟 + 随机微扰 (让波浪不那么死板)
|
||||
delay: normalizedDistance * 600 + (Math.random() * 100),
|
||||
randomX,
|
||||
randomY,
|
||||
randomRotateX,
|
||||
randomRotateY,
|
||||
randomScale
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!isActive && !isAnimating) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] pointer-events-none overflow-hidden"
|
||||
style={{ perspective: '1200px' }} // 开启3D透视,这很重要!
|
||||
>
|
||||
<div className="relative w-full h-full bg-transparent">
|
||||
{fragments.map((frag) => (
|
||||
<div
|
||||
key={frag.id}
|
||||
className="absolute shatter-fragment will-change-transform"
|
||||
style={{
|
||||
left: `${(frag.col / GRID_COLS) * 100}%`,
|
||||
top: `${(frag.row / GRID_ROWS) * 100}%`,
|
||||
width: `${100 / GRID_COLS + 0.1}%`, // +0.1% 防止渲染缝隙
|
||||
height: `${100 / GRID_ROWS + 0.1}%`,
|
||||
|
||||
// 动画变量传入 CSS
|
||||
'--delay': `${frag.delay}ms`,
|
||||
'--tx': `${frag.randomX}px`,
|
||||
'--ty': `${frag.randomY}px`,
|
||||
'--rx': `${frag.randomRotateX}deg`,
|
||||
'--ry': `${frag.randomRotateY}deg`,
|
||||
'--s': `${frag.randomScale}`,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{/* 内部容器负责材质,外部容器负责位置,分离关注点 */}
|
||||
<div className="w-full h-full glass-shard bg-white backdrop-blur-lg border-[0.5px] border-white/30 shadow-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 可选:背景遮罩,防止碎片飞走后直接漏出底部内容,根据需求调整 */}
|
||||
<div className="absolute inset-0 bg-white -z-10 animate-fade-out" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
// 数字增长动画Hook
|
||||
// 数字增长动画Hook - 可重复触发
|
||||
function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [count, setCount] = useState(end); // 初始值设为目标值,避免hydration错误
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const startValueRef = useRef<number>(end);
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 客户端挂载后设置随机起始值
|
||||
useEffect(() => {
|
||||
@@ -23,12 +24,13 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !hasAnimated) {
|
||||
setHasAnimated(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
// 进入视口 - 检查是否需要开始动画
|
||||
if (!timerRef.current) {
|
||||
const start = startValueRef.current;
|
||||
const startTime = Date.now();
|
||||
|
||||
const timer = setInterval(() => {
|
||||
timerRef.current = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
@@ -40,11 +42,20 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
|
||||
if (progress === 1) {
|
||||
setCount(end); // 确保最终值准确
|
||||
clearInterval(timer);
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}
|
||||
}, 16);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
} else {
|
||||
// 离开视口 - 停止并重置
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setCount(startValueRef.current);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
@@ -54,8 +65,14 @@ function useCountUp(end: number, duration: number = 1500, startRangePercent: num
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [end, duration, hasAnimated, mounted]);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [end, duration, mounted]);
|
||||
|
||||
return { count, elementRef };
|
||||
}
|
||||
@@ -75,6 +92,7 @@ function formatNumber(num: number, prefix: string = '', suffix: string = '', for
|
||||
}
|
||||
|
||||
export default function StatsSection() {
|
||||
const { t } = useLanguage();
|
||||
const tvl = useCountUp(485, 1500, 0.80); // 从80-95%开始
|
||||
const apy = useCountUp(515, 1500, 0.85); // 5.15 * 100,从85-100%开始
|
||||
const yield_ = useCountUp(45, 1500, 0.75); // 从75-90%开始
|
||||
@@ -135,7 +153,7 @@ export default function StatsSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Total Value Locked
|
||||
{t('stats.tvl')}
|
||||
</div>
|
||||
<div
|
||||
ref={tvl.elementRef}
|
||||
@@ -173,7 +191,7 @@ export default function StatsSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Avg. APY
|
||||
{t('stats.apy')}
|
||||
</div>
|
||||
<div
|
||||
ref={apy.elementRef}
|
||||
@@ -211,7 +229,7 @@ export default function StatsSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Yield Captured
|
||||
{t('stats.yield')}
|
||||
</div>
|
||||
<div
|
||||
ref={yield_.elementRef}
|
||||
@@ -249,7 +267,7 @@ export default function StatsSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Connected Users
|
||||
{t('stats.users')}
|
||||
</div>
|
||||
|
||||
{/* .frame-38 - Number and avatars */}
|
||||
|
||||
36
components/TransitionWrapper.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import ShatterTransition from './ShatterTransition';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function TransitionWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { transitionKey } = useLanguage();
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// transitionKey > 0 表示已经发生过语言切换
|
||||
if (transitionKey > 0) {
|
||||
setIsActive(true);
|
||||
|
||||
// 动画结束后重置状态
|
||||
const timer = setTimeout(() => {
|
||||
setIsActive(false);
|
||||
}, 1500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [transitionKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ShatterTransition
|
||||
isActive={isActive}
|
||||
onComplete={() => {
|
||||
// 动画完成后的回调
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,55 +2,47 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import ConsenSysLogo from './ConsenSysLogo';
|
||||
|
||||
export default function TrustedBySection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const currentRef = sectionRef.current;
|
||||
if (!currentRef) return;
|
||||
|
||||
// 使用 IntersectionObserver 检测元素进入视口
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !visible) {
|
||||
setVisible(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
setAnimate(true);
|
||||
observer.disconnect(); // 触发后立即断开
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.2, // 当20%的元素可见时触发
|
||||
}
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
observer.observe(currentRef);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
// 合作伙伴 logo 数据
|
||||
// 合作伙伴 logo 数据 - 统一高度为40px
|
||||
const partners = [
|
||||
{
|
||||
name: 'BlackRock',
|
||||
src: '/nav-ireland0.svg',
|
||||
width: 193,
|
||||
height: 28,
|
||||
className: 'w-[193px] h-[28px]'
|
||||
width: 200,
|
||||
height: 40,
|
||||
className: 'w-[200px] h-[40px]'
|
||||
},
|
||||
{
|
||||
name: 'Coinbase',
|
||||
src: '/coinbase-10.svg',
|
||||
width: 179,
|
||||
height: 32,
|
||||
className: 'w-[179px] h-[32px]'
|
||||
width: 180,
|
||||
height: 40,
|
||||
className: 'w-[180px] h-[40px]'
|
||||
},
|
||||
{
|
||||
name: 'Wintermute',
|
||||
@@ -68,10 +60,10 @@ export default function TrustedBySection() {
|
||||
},
|
||||
{
|
||||
name: 'ConsenSys',
|
||||
src: '/logo1.svg', // 临时使用一个 logo
|
||||
width: 176,
|
||||
height: 40,
|
||||
className: 'w-[176px] h-[40px]'
|
||||
src: '', // 使用组合的vector文件
|
||||
width: 220,
|
||||
height: 50,
|
||||
className: 'w-[220px] h-[50px]'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -96,26 +88,31 @@ export default function TrustedBySection() {
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
Trusted by Industry Leaders
|
||||
{t('trusted.title')}
|
||||
</div>
|
||||
|
||||
{/* Logo容器 - .frame-26 */}
|
||||
<div
|
||||
className="flex flex-row items-center justify-between flex-shrink-0 w-[1200px] relative"
|
||||
className="flex flex-row items-center justify-between flex-shrink-0 w-[1440px] relative"
|
||||
>
|
||||
{partners.map((partner, index) => (
|
||||
<div
|
||||
key={partner.name}
|
||||
className={`flex-shrink-0 relative overflow-hidden transition-all duration-700 ease-out ${partner.className} ${
|
||||
mounted && visible
|
||||
className={`flex-shrink-0 relative overflow-hidden transition-all duration-700 ease-out ${
|
||||
animate
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${index * 150}ms`, // 每个logo延迟150ms
|
||||
transitionDelay: `${index * 100}ms`,
|
||||
width: `${partner.width}px`,
|
||||
height: `${partner.height}px`,
|
||||
aspectRatio: `${partner.width}/${partner.height}`
|
||||
}}
|
||||
>
|
||||
{partner.name === 'ConsenSys' ? (
|
||||
<ConsenSysLogo width={partner.width} height={partner.height} />
|
||||
) : (
|
||||
<Image
|
||||
src={partner.src}
|
||||
alt={partner.name}
|
||||
@@ -123,6 +120,7 @@ export default function TrustedBySection() {
|
||||
height={partner.height}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,67 +2,31 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function WhyAssetXSection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const currentRef = sectionRef.current;
|
||||
if (!currentRef) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !visible) {
|
||||
setVisible(true);
|
||||
if (entries[0].isIntersecting) {
|
||||
setAnimate(true);
|
||||
observer.disconnect(); // 触发后立即断开,不再监听
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
{ threshold: 0.5 } // 当50%可见时才触发
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
observer.observe(currentRef);
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
const features = [
|
||||
{
|
||||
id: 'sustainable',
|
||||
icon: '/system-data0.svg',
|
||||
title: 'Sustainable Real Yield',
|
||||
description: 'Access 15%-30% returns from Delta-neutral arbitrage strategies and commercial credit. No inflationary token emissions, just real profits.',
|
||||
image: '/frame-110.svg',
|
||||
imageWidth: 305,
|
||||
imageHeight: 162,
|
||||
large: true
|
||||
},
|
||||
{
|
||||
id: 'reliability',
|
||||
icon: '/warning-shield-check0.svg',
|
||||
title: 'Proven Reliability',
|
||||
description: 'Backed by partners holding HK SFC Licenses (1/2/4/5/9) and NASDAQ-listed entities. Audited, compliant, and transparent.',
|
||||
rightIcon: '/icon0.svg',
|
||||
large: false
|
||||
},
|
||||
{
|
||||
id: 'liquidity',
|
||||
icon: '/arrow-arrow-left-right0.svg',
|
||||
title: 'Flexible Liquidity',
|
||||
description: 'Solve the illiquidity of traditional finance. Enjoy instant exit via secondary markets or leverage your positions for up to 40% APY.',
|
||||
badges: ['40%+ APR', 'Instant Exit'],
|
||||
large: false
|
||||
}
|
||||
];
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -74,9 +38,9 @@ export default function WhyAssetXSection() {
|
||||
alignContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center w-[1200px]">
|
||||
<div className="flex flex-col items-center justify-center w-[1440px]">
|
||||
{/* Header - .frame-27 */}
|
||||
<div className="flex flex-col gap-2 items-start justify-start flex-shrink-0 w-[1200px] relative mb-10">
|
||||
<div className="flex flex-col gap-2 items-start justify-start flex-shrink-0 w-[1440px] relative mb-10">
|
||||
{/* Title - .why-assetx */}
|
||||
<h2
|
||||
className="text-[#111827] text-left relative font-inter"
|
||||
@@ -87,7 +51,7 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Why ASSETX?
|
||||
{t('why.title')}
|
||||
</h2>
|
||||
|
||||
{/* Subtitle */}
|
||||
@@ -99,18 +63,18 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Institutional-grade access to real-world yield, with DeFi-native composability.
|
||||
{t('why.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cards Container - .frame-30 */}
|
||||
<div className="flex flex-row gap-6 items-center justify-start flex-shrink-0 w-[1199px] relative">
|
||||
<div className="flex flex-row gap-6 items-center justify-start flex-shrink-0 w-[1440px] relative">
|
||||
|
||||
{/* Left Large Card - Sustainable Real Yield */}
|
||||
<div
|
||||
className={`bg-white rounded-[24px] border border-[#e5e7eb] p-10 flex flex-col items-start justify-between self-stretch flex-1 relative overflow-hidden
|
||||
${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}
|
||||
@@ -142,7 +106,7 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Sustainable Real Yield
|
||||
{t('why.sustainable.title')}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[#9ca1af] text-left relative self-stretch font-inter"
|
||||
@@ -152,7 +116,7 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Access 15%-30% returns from Delta-neutral arbitrage strategies and commercial credit. No inflationary token emissions, just real profits.
|
||||
{t('why.sustainable.desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,18 +127,18 @@ export default function WhyAssetXSection() {
|
||||
alt="Chart"
|
||||
width={305}
|
||||
height={162}
|
||||
className="flex-shrink-0 relative"
|
||||
className="w-full h-auto flex-shrink-0 relative"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - .frame-29 */}
|
||||
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 w-[790px] relative">
|
||||
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 w-[850px] relative">
|
||||
|
||||
{/* Proven Reliability Card */}
|
||||
<div
|
||||
className={`bg-white rounded-[24px] border border-[#e5e7eb] p-10 flex flex-col items-start justify-start self-stretch flex-shrink-0 relative overflow-hidden
|
||||
${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}
|
||||
@@ -208,7 +172,7 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Proven Reliability
|
||||
{t('why.reliability.title')}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[#9ca1af] text-left relative font-inter"
|
||||
@@ -218,28 +182,65 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Backed by partners holding HK SFC Licenses (1/2/4/5/9) and NASDAQ-listed entities. Audited, compliant, and transparent.
|
||||
{t('why.reliability.desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Icon */}
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
<div
|
||||
className="bg-[#f9fafb] rounded-[16px] border border-[#e5e7eb] flex items-center justify-center flex-shrink-0 relative"
|
||||
style={{
|
||||
padding: '32px',
|
||||
height: '180px',
|
||||
width: '196px',
|
||||
isolation: 'isolate',
|
||||
willChange: 'auto',
|
||||
contain: 'layout style paint'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rounded-full border-2 border-[#e5e7eb] flex items-center justify-center flex-shrink-0 relative"
|
||||
style={{
|
||||
width: '132px',
|
||||
height: '132px',
|
||||
minWidth: '132px',
|
||||
minHeight: '132px',
|
||||
maxWidth: '132px',
|
||||
maxHeight: '132px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-[#e5e7eb] rounded-full flex items-center justify-center flex-shrink-0 relative"
|
||||
style={{
|
||||
width: '88.59px',
|
||||
height: '88.59px',
|
||||
minWidth: '88.59px',
|
||||
minHeight: '88.59px',
|
||||
maxWidth: '88.59px',
|
||||
maxHeight: '88.59px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/icon0.svg"
|
||||
alt="Icon"
|
||||
width={200}
|
||||
height={200}
|
||||
alt="Lock Icon"
|
||||
style={{
|
||||
width: '44.29px',
|
||||
height: '44.29px',
|
||||
display: 'block'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flexible Liquidity Card */}
|
||||
<div
|
||||
className={`bg-white rounded-[24px] border border-[#e5e7eb] p-10 flex flex-col items-start justify-start self-stretch flex-shrink-0 relative overflow-hidden
|
||||
${
|
||||
mounted && visible
|
||||
animate
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-12 opacity-0'
|
||||
}
|
||||
@@ -248,11 +249,9 @@ export default function WhyAssetXSection() {
|
||||
transition: 'transform 0.3s ease-out, box-shadow 0.3s ease-out, opacity 0.7s ease-out 0.3s, translate 0.7s ease-out 0.3s'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-6 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
{/* Top Section with Icon and Badges */}
|
||||
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 relative">
|
||||
{/* Icon and Badges Row */}
|
||||
<div className="flex flex-row gap-6 items-center justify-start relative">
|
||||
<div className="flex flex-row items-start justify-between self-stretch flex-shrink-0 relative">
|
||||
{/* Left Content */}
|
||||
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 w-[550px] relative">
|
||||
{/* Icon */}
|
||||
<div className="bg-[#f9fafb] rounded-[16px] flex flex-row items-center justify-center flex-shrink-0 w-12 h-12 relative">
|
||||
<Image
|
||||
@@ -263,20 +262,8 @@ export default function WhyAssetXSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="bg-[#f9fafb] rounded-lg px-3 py-1 flex items-center justify-center">
|
||||
<span className="text-[#111827] font-inter text-sm font-bold">40%+ APR</span>
|
||||
</div>
|
||||
<div className="bg-[#f9fafb] rounded-lg px-3 py-1 flex items-center justify-center">
|
||||
<span className="text-[#111827] font-inter text-sm font-bold">Instant Exit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<div className="flex flex-col gap-4 items-start justify-start self-stretch flex-shrink-0 relative">
|
||||
<div className="flex flex-col gap-4 items-start justify-start flex-shrink-0 relative">
|
||||
<h3
|
||||
className="text-[#111827] text-left relative font-inter"
|
||||
style={{
|
||||
@@ -286,7 +273,7 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
Flexible Liquidity
|
||||
{t('why.liquidity.title')}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[#9ca1af] text-left relative font-inter"
|
||||
@@ -296,10 +283,25 @@ export default function WhyAssetXSection() {
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
Solve the illiquidity of traditional finance. Enjoy instant exit via secondary markets or leverage your positions for up to 40% APY.
|
||||
{t('why.liquidity.desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Badges */}
|
||||
<div className="flex flex-row gap-3 items-center justify-end flex-shrink-0">
|
||||
<div className="bg-[#e1f8ec] rounded-lg px-4 py-2 flex items-center justify-center">
|
||||
<span className="text-[#10b981] font-inter font-bold" style={{ fontSize: '12px' }}>
|
||||
{t('why.liquidity.badge1')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-[#ebf2ff] rounded-lg px-4 py-2 flex items-center justify-center">
|
||||
<span className="text-[#1447e6] font-inter font-bold" style={{ fontSize: '12px' }}>
|
||||
{t('why.liquidity.badge2')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
232
contexts/LanguageContext.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
type Language = 'zh' | 'en';
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
t: (key: string) => string;
|
||||
transitionKey: number;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
// 默认语言为英文
|
||||
const [language, setLanguage] = useState<Language>('en');
|
||||
const [transitionKey, setTransitionKey] = useState(0);
|
||||
const [pendingLanguage, setPendingLanguage] = useState<Language | null>(null);
|
||||
|
||||
// 监听 pendingLanguage,延迟切换语言
|
||||
useEffect(() => {
|
||||
if (pendingLanguage !== null) {
|
||||
// 延迟 300ms 切换语言,此时蒙版已经完全覆盖
|
||||
const timer = setTimeout(() => {
|
||||
setLanguage(pendingLanguage);
|
||||
setPendingLanguage(null);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [pendingLanguage]);
|
||||
|
||||
// 切换语言
|
||||
const handleSetLanguage = (lang: Language) => {
|
||||
if (lang === language) return; // 如果语言相同,不执行切换
|
||||
|
||||
// 先递增transitionKey触发动画
|
||||
setTransitionKey(prev => prev + 1);
|
||||
|
||||
// 设置待切换的语言,稍后才真正切换
|
||||
setPendingLanguage(lang);
|
||||
};
|
||||
|
||||
// 简单的翻译函数
|
||||
const t = (key: string): string => {
|
||||
const translations = getTranslations(language);
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage, t, transitionKey }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// 翻译内容
|
||||
function getTranslations(lang: Language): Record<string, string> {
|
||||
const translations = {
|
||||
zh: {
|
||||
// Navbar
|
||||
'nav.product': '产品',
|
||||
'nav.resource': '资源',
|
||||
'nav.community': '社区',
|
||||
'nav.launchApp': '启动应用',
|
||||
|
||||
// Hero Section
|
||||
'hero.title1': '链上收益资产',
|
||||
'hero.title2': '随时可得',
|
||||
'hero.description': '低门槛投资股票、房地产和商业贷款等收益资产。享受真实资产支持的10%-30%年化收益。',
|
||||
'hero.startInvesting': '开始投资',
|
||||
'hero.readWhitepaper': '阅读白皮书',
|
||||
|
||||
// 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': '存入并铸造WUSD',
|
||||
'how.step1.desc': '连接您的钱包并存入USDC兑换WUSD。这是整个AssetX生态系统的原生稳定币和入口。',
|
||||
'how.step2.title': '双重投资选择',
|
||||
'how.step2.desc': '选择您的策略:使用WUSD购买特定的收益资产以获得现实世界回报,或在DeFi池中提供流动性赚取交易费用。',
|
||||
'how.step3.title': '赚取与提升',
|
||||
'how.step3.desc': '获得每日收益分配。使用资产代币作为抵押品借入WUSD,利用高达2.5倍的杠杆实现收益最大化。',
|
||||
'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': '© 2025 ASSETX协议。保留所有权利。',
|
||||
},
|
||||
en: {
|
||||
// Navbar
|
||||
'nav.product': 'Product',
|
||||
'nav.resource': 'Resource',
|
||||
'nav.community': 'Community',
|
||||
'nav.launchApp': 'Launch App',
|
||||
|
||||
// Hero Section
|
||||
'hero.title1': 'Yield-Bearing Asset',
|
||||
'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 the Whitepaper',
|
||||
|
||||
// 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': 'Deposit & Mint WUSD',
|
||||
'how.step1.desc': 'Connect your wallet and deposit USDC to swap WUSD. This serves as the native stablecoin and gateway to the entire AssetX ecosystem.',
|
||||
'how.step2.title': 'Dual Investment Options',
|
||||
'how.step2.desc': 'Choose your strategy: use WUSD to purchase specific Yield-Bearing Assets for real-world returns, or provide liquidity in DeFi Pools to earn trading fees.',
|
||||
'how.step3.title': 'Earn & Boost',
|
||||
'how.step3.desc': 'Receive daily yield distributions. Use Asset Tokens as collateral to borrow WUSD and leverage up to 2.5x for maximized returns.',
|
||||
'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': '© 2025 ASSETX Protocol. All rights reserved.',
|
||||
}
|
||||
};
|
||||
|
||||
return translations[lang];
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -H 0.0.0.0 -p 3002",
|
||||
"dev": "./scripts/dev.sh",
|
||||
"dev:kill": "pkill -f 'next dev' || pkill -f 'next-server' || true",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3002",
|
||||
"lint": "next lint"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4.6665V13.9998" stroke="#FCFCFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.00016 12C1.82335 12 1.65378 11.9298 1.52876 11.8047C1.40373 11.6797 1.3335 11.5101 1.3335 11.3333V2.66667C1.3335 2.48986 1.40373 2.32029 1.52876 2.19526C1.65378 2.07024 1.82335 2 2.00016 2H5.3335C6.04074 2 6.71902 2.28095 7.21911 2.78105C7.71921 3.28115 8.00016 3.95942 8.00016 4.66667C8.00016 3.95942 8.28111 3.28115 8.78121 2.78105C9.28131 2.28095 9.95958 2 10.6668 2H14.0002C14.177 2 14.3465 2.07024 14.4716 2.19526C14.5966 2.32029 14.6668 2.48986 14.6668 2.66667V11.3333C14.6668 11.5101 14.5966 11.6797 14.4716 11.8047C14.3465 11.9298 14.177 12 14.0002 12H10.0002C9.46973 12 8.96102 12.2107 8.58595 12.5858C8.21088 12.9609 8.00016 13.4696 8.00016 14C8.00016 13.4696 7.78945 12.9609 7.41438 12.5858C7.0393 12.2107 6.5306 12 6.00016 12H2.00016Z" stroke="#FCFCFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.16699 10H15.8337" stroke="#111827" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 4.1665L15.8333 9.99984L10 15.8332" stroke="#111827" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 347 B |
BIN
public/hero-background.mp4
Normal file
4
public/icon-book.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4.6665V13.9998" stroke="#FCFCFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.00016 12C1.82335 12 1.65378 11.9298 1.52876 11.8047C1.40373 11.6797 1.3335 11.5101 1.3335 11.3333V2.66667C1.3335 2.48986 1.40373 2.32029 1.52876 2.19526C1.65378 2.07024 1.82335 2 2.00016 2H5.3335C6.04074 2 6.71902 2.28095 7.21911 2.78105C7.71921 3.28115 8.00016 3.95942 8.00016 4.66667C8.00016 3.95942 8.28111 3.28115 8.78121 2.78105C9.28131 2.28095 9.95958 2 10.6668 2H14.0002C14.177 2 14.3465 2.07024 14.4716 2.19526C14.5966 2.32029 14.6668 2.48986 14.6668 2.66667V11.3333C14.6668 11.5101 14.5966 11.6797 14.4716 11.8047C14.3465 11.9298 14.177 12 14.0002 12H10.0002C9.46973 12 8.96102 12.2107 8.58595 12.5858C8.21088 12.9609 8.00016 13.4696 8.00016 14C8.00016 13.4696 7.78945 12.9609 7.41438 12.5858C7.0393 12.2107 6.5306 12 6.00016 12H2.00016Z" stroke="#FCFCFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
public/vector0.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
3
public/vector1.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="11" height="16" viewBox="0 0 11 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5208 11.0771C10.492 11.1059 10.4631 11.1059 10.4631 11.1348C10.0308 13.1232 8.7337 14.2471 6.91778 14.7659C4.7848 15.3711 2.82477 14.9388 1.21062 13.3538C0.432365 12.5757 0.0288241 11.6247 0 10.5007C0 10.0396 0.317065 9.69383 0.720603 9.66501C1.18179 9.63619 1.52768 9.92437 1.58533 10.3567C1.84474 12.1434 2.96888 13.2097 4.75598 13.3538C5.82247 13.4402 6.86014 13.2385 7.75369 12.6045C9.13725 11.6247 9.28135 10.0685 8.09957 8.85809C7.29249 8.05117 6.28365 7.67654 5.18834 7.53245C3.89125 7.38835 2.70946 7.01371 1.67179 6.17798C-3.57354e-06 4.82351 7.00965e-06 2.63331 1.70063 1.25002C3.74714 -0.421447 7.03309 -0.421446 9.05078 1.27884C9.74255 1.85521 10.1749 2.54685 10.2902 3.46905C10.319 3.72841 10.319 3.98778 10.1749 4.21833C9.97314 4.53533 9.71372 4.6506 9.36783 4.62178C9.02194 4.59296 8.7337 4.36241 8.67605 4.01659C8.47428 2.4604 7.35014 2.02813 6.08188 1.76876C5.10186 1.59585 4.15067 1.71112 3.25712 2.20104C3.14182 2.25867 2.99771 2.34512 2.88241 2.43158C2.01769 3.0944 1.61416 4.16069 2.99771 5.02524C3.83361 5.54397 4.7848 5.77452 5.76482 5.91861C7.03308 6.09152 8.18604 6.58143 9.16606 7.47481C9.80019 8.05118 10.2326 8.77164 10.4343 9.63619C10.4632 9.69383 10.4055 9.8091 10.5208 9.83792V11.0771Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
public/vector2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.35033 13.1412C10.4633 13.1412 12.971 10.634 13.0287 7.57925C13.0863 4.43804 10.6075 1.7291 7.43681 1.70029C4.23733 1.70029 1.72964 4.20749 1.70082 7.37752C1.70082 10.634 4.17968 13.1412 7.35033 13.1412ZM7.35033 14.8703C3.28613 14.8703 -0.0286375 11.4986 0.000186571 7.43516C0.0290107 3.4294 3.14201 0.0288184 7.35033 0C11.4722 0 14.7293 3.34294 14.7293 7.46397C14.7293 11.5562 11.3857 14.8703 7.35033 14.8703Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 B |
3
public/vector3.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="15" viewBox="0 0 10 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 7.49279C0 5.33141 0 3.17003 0 1.00865C0 0.345821 0.317063 0 0.980018 0C3.57419 0 6.16835 0 8.7337 0C9.25253 0 9.5696 0.230542 9.6849 0.634C9.77137 1.03746 9.59843 1.44092 9.25254 1.61383C9.07959 1.70029 8.87782 1.70029 8.70487 1.70029C6.48542 1.70029 4.26596 1.70029 2.07533 1.70029C1.78709 1.70029 1.70062 1.75792 1.70062 2.07492C1.72944 3.45821 1.70062 4.87031 1.70062 6.2536C1.70062 6.51296 1.75827 6.59942 2.01769 6.59942C3.60301 6.59942 5.21716 6.59942 6.80249 6.59942C7.23485 6.59942 7.55192 6.8876 7.63839 7.26224C7.72486 7.69452 7.49426 8.06916 7.11955 8.24207C6.97543 8.29971 6.86013 8.29971 6.71601 8.29971C5.15951 8.29971 3.60301 8.29971 2.04651 8.29971C1.7871 8.29971 1.70062 8.35735 1.70062 8.61671C1.72944 10.0576 1.70062 11.4986 1.70062 12.9395C1.70062 13.1988 1.75827 13.2853 2.01769 13.2853C4.26597 13.2853 6.48542 13.2853 8.7337 13.2853C9.31018 13.2853 9.65607 13.5735 9.71372 14.0346C9.77137 14.5821 9.39666 14.9856 8.82018 14.9856C7.06191 14.9856 5.33246 14.9856 3.57419 14.9856H1.12414C0.288239 14.9856 0.0288259 14.7262 0.0288259 13.8905V7.49279H0Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
public/vector4.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="11" height="15" viewBox="0 0 11 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.39011 0C6.88896 0.0288184 8.30133 0.461097 9.39665 1.61383C9.97313 2.1902 10.2902 2.91066 10.2902 3.7464C10.2902 4.20749 10.002 4.55331 9.56961 4.61095C9.13724 4.66859 8.70487 4.43804 8.67605 3.97694C8.53193 2.59366 7.55191 2.13256 6.42777 1.84438C5.44775 1.58501 4.46773 1.67146 3.54536 2.07492C3.05535 2.27665 2.65182 2.56484 2.36358 2.99712C1.96004 3.6023 2.04651 4.23631 2.59417 4.6974C3.40124 5.38904 4.35243 5.67723 5.36128 5.82132C6.51424 5.99423 7.63838 6.31123 8.6184 7.00288C11.0684 8.70317 11.1837 11.9308 8.82017 13.7752C6.68718 15.4755 3.48771 15.389 1.47002 13.6023C0.518827 12.7666 0 11.7291 0 10.4323C0 9.99999 0.317058 9.68299 0.74942 9.65418C1.21061 9.62536 1.5565 9.94236 1.58533 10.3746C1.61415 11.7291 2.3924 12.5648 3.54536 13.0259C5.07304 13.6599 6.57189 13.487 7.89779 12.4784C9.07958 11.585 9.22372 10.2017 8.27252 9.04899C7.46545 8.09798 6.39895 7.66571 5.18834 7.52161C3.89125 7.37752 2.70946 7.00288 1.67179 6.16714C-3.57354e-06 4.81268 3.57354e-06 2.68011 1.64298 1.26801C2.70947 0.374638 3.97773 0.0288184 5.39011 0Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
public/vector5.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="11" height="15" viewBox="0 0 11 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.47772 0C6.94775 0.0576369 8.3313 0.461097 9.42662 1.61383C10.0031 2.21902 10.3202 2.93948 10.3202 3.77522C10.3202 4.20749 10.0031 4.55331 9.59956 4.58213C9.19603 4.63977 8.76366 4.3804 8.70601 3.94812C8.56189 2.59365 7.61071 2.10374 6.51539 1.81556C5.4489 1.52737 4.41123 1.64265 3.40238 2.10374C2.97002 2.30547 2.62413 2.59366 2.36471 2.9683C1.99 3.54466 2.04765 4.12104 2.53766 4.61095C3.22944 5.24496 4.06533 5.56196 4.95888 5.73487C5.88125 5.90778 6.80363 6.02305 7.63953 6.42651C8.73484 6.94524 9.65721 7.66571 10.176 8.81844C10.8966 10.4611 10.4355 12.3631 9.02308 13.5735C6.86127 15.4467 3.48885 15.4467 1.38469 13.5158C0.491147 12.7089 -0.0276831 11.7003 0.00114097 10.4323C0.00114097 9.94236 0.289382 9.62535 0.721744 9.59654C1.18293 9.56772 1.55764 9.85591 1.58647 10.3746C1.61529 10.7205 1.64411 11.0375 1.78823 11.3545C2.16294 12.219 2.85472 12.7666 3.69062 13.0547C5.30477 13.6023 6.80363 13.4006 8.10072 12.2478C9.13838 11.2968 9.16721 9.97118 8.21601 8.93372C7.40894 8.04034 6.37127 7.63689 5.21831 7.49279C3.95004 7.3487 2.76826 7.00288 1.75941 6.19596C0.0299668 4.8415 -0.0276885 2.68011 1.64411 1.26801C2.73943 0.374638 4.03652 0.0288184 5.47772 0Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
public/vector6.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="11" height="16" viewBox="0 0 11 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.809 9.64464C10.809 11.1432 10.809 12.6418 10.809 14.1403C10.809 14.5438 10.6073 14.8608 10.2614 14.9761C9.91549 15.0913 9.51195 14.9761 9.31018 14.6879C9.16606 14.515 9.13724 14.3132 9.13724 14.0827C9.13724 11.2873 9.13724 8.4919 9.13724 5.69652C9.13724 3.93859 8.18605 2.52649 6.62954 1.95012C4.32362 1.11438 1.75827 2.81467 1.70062 5.29306C1.64297 8.1749 1.67179 11.0279 1.67179 13.9098C1.67179 13.9962 1.67179 14.1115 1.67179 14.198C1.64297 14.6302 1.29709 14.9472 0.893549 14.9761C0.461187 15.0049 0.0864741 14.7167 0.0288259 14.2844C1.75885e-06 14.1691 0 14.0539 0 13.9098C0 11.0856 0 8.29018 0 5.46597C0 2.72822 2.01769 0.39393 4.66951 0.0481085C7.89781 -0.384168 10.7802 2.15185 10.809 5.43715C10.8379 6.84925 10.809 8.26136 10.809 9.64464Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 922 B |
3
public/vector7.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="15" viewBox="0 0 13 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.63362 14.9397C3.00178 14.9397 -0.370635 11.1933 0.0329022 6.84169C0.349967 3.29702 2.91531 0.530451 6.40303 0.0693565C8.79543 -0.247646 10.8419 0.530451 12.5714 2.23074C12.629 2.28838 12.6579 2.31719 12.6867 2.37483C13.0037 2.80711 13.0037 3.2682 12.6867 3.55639C12.3696 3.84457 11.8508 3.84457 11.4761 3.52757C10.5825 2.77829 9.63133 2.08665 8.44954 1.82728C5.53831 1.16446 2.59825 3.03766 1.82 6.03478C0.868805 9.78117 3.80887 13.4123 7.61365 13.2106C9.02603 13.1241 10.2655 12.5766 11.2743 11.5679C11.332 11.5103 11.3896 11.4526 11.4761 11.3662C11.8508 11.0204 12.3408 10.9627 12.629 11.2509C12.9461 11.5391 13.0037 12.0866 12.7155 12.4901C12.4561 12.8647 12.1102 13.1529 11.7643 13.4123C10.4961 14.4498 8.9972 14.9397 6.63362 14.9397Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 912 B |
3
public/vector8.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="11" height="15" viewBox="0 0 11 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7802 9.58126C10.7802 11.0798 10.7802 12.5784 10.7802 14.0769C10.7802 14.6245 10.4632 14.9703 9.97315 14.9703C9.48314 14.9703 9.10843 14.6245 9.10843 14.0769C9.10843 11.2239 9.10843 8.37088 9.10843 5.51786C9.10843 3.41411 7.43663 1.71383 5.36129 1.71383C3.31478 1.71383 1.67181 3.41411 1.67181 5.51786C1.67181 8.34206 1.67181 11.1663 1.67181 13.9905C1.67181 14.2498 1.61415 14.5092 1.44121 14.7109C1.18179 14.9703 0.864737 15.0279 0.518848 14.9127C0.201783 14.7974 0 14.5092 0 14.1634C0 13.3277 0 12.5207 0 11.685C0 9.58126 0 7.4487 0 5.34495C0 2.57838 2.24829 0.215271 5.0154 0.0135419C7.84016 -0.188187 10.3479 1.88674 10.7514 4.71094C10.7802 4.91267 10.7802 5.14322 10.7802 5.34495C10.7802 6.75706 10.7802 8.16915 10.7802 9.58126Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 907 B |
3
public/vector9.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.11959 11.689C6.11959 10.9397 6.11959 10.1905 6.11959 9.44117C6.11959 9.23945 6.06193 9.06654 5.94663 8.89363C4.04424 6.41524 2.14185 3.93685 0.239463 1.45846C-0.106426 0.997369 -0.0776053 0.478639 0.325932 0.190455C0.72947 -0.126548 1.21949 -0.0400877 1.56538 0.421007C3.29483 2.66885 5.05309 4.9455 6.78254 7.19334C6.89783 7.36625 6.98431 7.48152 7.18608 7.22215C8.97318 4.94549 10.7603 2.66884 12.5474 0.392187C12.8932 -0.0400894 13.3833 -0.126548 13.7868 0.190455C14.1615 0.478639 14.1903 0.997376 13.8444 1.42965C12.1726 3.56222 10.5297 5.69478 8.85788 7.82734C8.80023 7.88498 8.7714 7.97143 8.71376 8.00025C7.96433 8.6919 7.76255 9.55645 7.8202 10.5939C7.90667 11.7466 7.84903 12.9282 7.84903 14.0809C7.84903 14.5132 7.61844 14.8302 7.2149 14.9167C6.81136 15.032 6.43665 14.859 6.26371 14.5132C6.17724 14.3403 6.1484 14.1674 6.1484 13.9945C6.11958 13.2164 6.11959 12.4383 6.11959 11.689Z" fill="#9CA1AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
36
scripts/dev.sh
Normal 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
|
||||
@@ -21,9 +21,11 @@ export default {
|
||||
'fill-secondary-click': '#f3f4f6',
|
||||
},
|
||||
fontFamily: {
|
||||
inter: ['var(--font-inter)', 'Inter', 'sans-serif'],
|
||||
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-domine)', 'Domine', 'serif'],
|
||||
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' }],
|
||||
|
||||