feat(web): rebuild web with Astro 6 + React 19 + Tailwind 4
Replace static HTML website with Astro SSG framework: - Astro 6 + React 19 (client islands) + Tailwind CSS 4 + shadcn/ui - i18n: zh/zh_Hant/en with URL prefix routing - Pages: Landing, Features, Pricing, About, Privacy, Terms (3 locales) - Responsive full-width layout with scroll reveal animations - Cyber gradient theme with particle effects inspired by Kimi - Features page: alternating layout with hexagram illustrations - Legal pages: markdown rendering with side info card - Language switcher preserves current page path - Assets shared via symlinks to web/design/assets/ (no duplication) Tech decisions documented in .trellis/spec/web/index.md Task: .trellis/tasks/05-08-web-astro-react-tailwind-shadcn-ui
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import { t, localePath, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const a = t(locale, 'about');
|
||||
const footer = t(locale, 'footer');
|
||||
---
|
||||
|
||||
<section class="bg-gradient-to-b from-white to-violet-50 py-16 md:py-20 px-5 md:px-20 text-center">
|
||||
<h1 class="reveal text-slate-900 text-4xl md:text-[48px] font-extrabold">{a.title}</h1>
|
||||
<p class="reveal stagger-1 text-slate-500 text-lg mt-4">{a.subtitle}</p>
|
||||
</section>
|
||||
|
||||
<section class="bg-white py-16 md:py-20 px-5 md:px-20">
|
||||
<div class="max-w-[1200px] mx-auto flex flex-col md:flex-row gap-16">
|
||||
<div class="reveal-left flex-1 flex flex-col gap-6">
|
||||
<h2 class="text-slate-900 text-[28px] font-bold">{a.storyTitle}</h2>
|
||||
<div class="w-12 h-1 bg-violet-600 rounded"></div>
|
||||
<p class="text-slate-600 text-base leading-loose">{a.p1}</p>
|
||||
<p class="text-slate-600 text-base leading-loose">{a.p2}</p>
|
||||
<p class="text-slate-600 text-base leading-loose">{a.p3}</p>
|
||||
</div>
|
||||
|
||||
<div class="reveal-right w-full md:w-[400px] bg-slate-50 border border-slate-200 rounded-2xl p-8 flex flex-col gap-6 shrink-0">
|
||||
<h3 class="text-slate-900 text-xl font-bold">{a.companyInfo}</h3>
|
||||
<div class="h-px bg-slate-200"></div>
|
||||
<p class="text-slate-600 text-base">{a.company}</p>
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{a.emailLabel}</p>
|
||||
<p class="text-slate-600 text-[15px]">{a.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{a.devLabel}</p>
|
||||
<p class="text-slate-600 text-[15px]">{a.developer}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{a.icpLabel}</p>
|
||||
<p class="text-slate-600 text-[15px]">{a.icp}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-amber-50 py-20 px-5 md:px-20">
|
||||
<div class="reveal max-w-[800px] mx-auto text-center flex flex-col gap-5">
|
||||
<h3 class="text-amber-800 text-2xl font-bold">{a.warningTitle}</h3>
|
||||
<p class="text-amber-700 text-[15px] leading-loose">{a.warningBody}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-slate-50 py-12 px-5 md:px-20">
|
||||
<div class="reveal max-w-[600px] mx-auto text-center flex flex-col gap-5">
|
||||
<h3 class="text-slate-900 text-xl font-bold">{a.legalTitle}</h3>
|
||||
<div class="flex justify-center gap-8">
|
||||
<a href={localePath(locale, '/privacy')} class="text-violet-600 text-[15px] hover:underline">{footer.col3Link1}</a>
|
||||
<a href={localePath(locale, '/terms')} class="text-violet-600 text-[15px] hover:underline">{footer.col3Link2}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import { t, localePath, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const cta = t(locale, 'cta');
|
||||
---
|
||||
|
||||
<section class="reveal-scale w-full py-24 md:py-32 relative overflow-hidden">
|
||||
<div class="glow-bg glow-bg-wide absolute inset-0 pointer-events-none"></div>
|
||||
|
||||
<div class="relative z-10 max-w-3xl mx-auto px-6 flex flex-col items-center gap-6 text-center">
|
||||
<h2 class="text-slate-900 text-3xl md:text-[48px] font-bold">
|
||||
{cta.title}
|
||||
</h2>
|
||||
<p class="text-slate-500 text-lg max-w-xl">
|
||||
{cta.subtitle}
|
||||
</p>
|
||||
<a href={localePath(locale, '/login')} class="cyber-gradient cyber-glow text-white text-lg font-semibold px-12 py-5 rounded-xl hover:-translate-y-0.5 transition-all duration-300 mt-4">
|
||||
{cta.button}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const f = t(locale, 'features');
|
||||
|
||||
const icons = [
|
||||
// 01 - 两种起卦方式: sparkle/sparkles
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>`,
|
||||
// 02 - AI 解卦分析: brain
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4.5 4.5 0 0 1 .555-.396"/><path d="M20.523 10.896a4.5 4.5 0 0 0-.555-.396"/></svg>`,
|
||||
// 03 - 九类问题覆盖: grid/layout
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg>`,
|
||||
// 04 - 追问互动: message/chat
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>`,
|
||||
// 05 - 历史记录: clock/history
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
|
||||
// 06 - 点数系统: coins
|
||||
`<svg class="w-5 h-5 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71V9"/></svg>`,
|
||||
];
|
||||
|
||||
const cards = [
|
||||
{ num: '01', title: f.c1Title, desc: f.c1Desc },
|
||||
{ num: '02', title: f.c2Title, desc: f.c2Desc },
|
||||
{ num: '03', title: f.c3Title, desc: f.c3Desc },
|
||||
{ num: '04', title: f.c4Title, desc: f.c4Desc },
|
||||
{ num: '05', title: f.c5Title, desc: f.c5Desc },
|
||||
{ num: '06', title: f.c6Title, desc: f.c6Desc },
|
||||
];
|
||||
---
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="w-full pt-32 md:pt-40 pb-16 md:pb-20 relative">
|
||||
<div class="glow-bg absolute inset-0 pointer-events-none"></div>
|
||||
<div class="relative text-center px-6 max-w-3xl mx-auto">
|
||||
<h1 class="reveal text-slate-900 text-3xl md:text-4xl lg:text-5xl font-bold mb-4">{f.title}</h1>
|
||||
<p class="reveal stagger-1 text-slate-500 text-lg">{f.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature Details - alternating layout -->
|
||||
<section class="w-full py-16 md:py-24" style="background-color: #F8F7FC;">
|
||||
<div class="max-w-6xl mx-auto px-6 space-y-24 md:space-y-32">
|
||||
{cards.map((card, index) => {
|
||||
const isEven = index % 2 === 1;
|
||||
const lines = [0,1,2,3,4,5].map(i => (index + i) % 2 === 0);
|
||||
return (
|
||||
<div class={`reveal flex flex-col ${isEven ? 'md:flex-row-reverse' : 'md:flex-row'} gap-10 md:gap-16 items-center`}>
|
||||
<!-- Text -->
|
||||
<div class="flex-1">
|
||||
<span class="font-mono text-6xl md:text-7xl font-bold select-none block mb-2 text-violet-100">{card.num}</span>
|
||||
<div class="flex items-center gap-3 mb-4 -mt-4">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center bg-violet-50 shrink-0">
|
||||
<Fragment set:html={icons[index]} />
|
||||
</div>
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-slate-900">{card.title}</h2>
|
||||
</div>
|
||||
<p class="text-slate-500 leading-relaxed max-w-lg">{card.desc}</p>
|
||||
</div>
|
||||
|
||||
<!-- Hexagram Illustration -->
|
||||
<div class="flex-1 w-full">
|
||||
<div class="relative w-full h-48 md:h-64 rounded-xl border border-violet-100 flex items-center justify-center bg-violet-50/30">
|
||||
<div class="flex flex-col gap-2.5 items-center">
|
||||
{lines.map((isYang) => isYang ? (
|
||||
<div class="hex-yang"></div>
|
||||
) : (
|
||||
<div class="hex-yin"><div></div><div></div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="w-full py-24 md:py-32 relative">
|
||||
<div class="glow-bg absolute inset-0 pointer-events-none"></div>
|
||||
<div class="reveal relative text-center px-6 max-w-3xl mx-auto">
|
||||
<h2 class="text-slate-900 text-3xl md:text-4xl font-bold mb-4">{f.tagline}</h2>
|
||||
<p class="text-slate-500 text-lg mb-8">{f.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { t, localePath, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const footer = t(locale, 'footer');
|
||||
---
|
||||
|
||||
<footer class="w-full bg-slate-950 px-6 md:px-20 py-16 md:py-12">
|
||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row gap-10 md:gap-16">
|
||||
<div class="w-full md:w-64 flex flex-col gap-4 shrink-0">
|
||||
<a href={localePath(locale, '/')} class="flex items-center gap-3">
|
||||
<img src="/images/logo.png" alt="MeiYao" class="w-8 h-8" />
|
||||
<span class="text-white text-lg font-bold">{footer.brandName}</span>
|
||||
</a>
|
||||
<p class="text-slate-400 text-sm leading-relaxed">{footer.desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-wrap gap-10 md:gap-16">
|
||||
<div class="flex flex-col gap-3 min-w-[120px]">
|
||||
<span class="text-white text-sm font-semibold">{footer.col1Title}</span>
|
||||
<a href={localePath(locale, '/features')} class="text-slate-400 text-sm hover:text-white">{footer.col1Link1}</a>
|
||||
<a href={localePath(locale, '/pricing')} class="text-slate-400 text-sm hover:text-white">{footer.col1Link2}</a>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 min-w-[120px]">
|
||||
<span class="text-white text-sm font-semibold">{footer.col2Title}</span>
|
||||
<a href="#" class="text-slate-400 text-sm hover:text-white">{footer.col2Link1}</a>
|
||||
<a href="#" class="text-slate-400 text-sm hover:text-white">{footer.col2Link2}</a>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 min-w-[120px]">
|
||||
<span class="text-white text-sm font-semibold">{footer.col3Title}</span>
|
||||
<a href={localePath(locale, '/privacy')} class="text-slate-400 text-sm hover:text-white">{footer.col3Link1}</a>
|
||||
<a href={localePath(locale, '/terms')} class="text-slate-400 text-sm hover:text-white">{footer.col3Link2}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import { t, localePath, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const hero = t(locale, 'hero');
|
||||
---
|
||||
|
||||
<section class="relative w-full min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<!-- Particles -->
|
||||
<div class="particle-lines"></div>
|
||||
<div class="particle"></div><div class="particle"></div><div class="particle"></div>
|
||||
<div class="particle"></div><div class="particle"></div><div class="particle"></div>
|
||||
<div class="particle"></div><div class="particle"></div>
|
||||
|
||||
<!-- Purple glow -->
|
||||
<div class="glow-bg absolute inset-0 pointer-events-none"></div>
|
||||
|
||||
<div class="relative z-10 text-center px-6 max-w-3xl mx-auto">
|
||||
<span class="reveal stagger-1 inline-block px-4 py-1.5 rounded-full text-xs font-medium tracking-widest text-violet-600 border border-violet-200 bg-violet-50 mb-8">
|
||||
{hero.badge}
|
||||
</span>
|
||||
|
||||
<h1 class="reveal stagger-2 text-slate-900 text-4xl sm:text-5xl md:text-6xl font-bold leading-tight mb-6">
|
||||
{hero.headline}
|
||||
</h1>
|
||||
|
||||
<p class="reveal stagger-3 text-slate-500 text-lg md:text-xl leading-relaxed max-w-xl mx-auto mb-10">
|
||||
{hero.subtext}
|
||||
</p>
|
||||
|
||||
<div class="reveal stagger-4 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href={localePath(locale, '/login')} class="w-full sm:w-auto text-center cyber-gradient cyber-glow text-white text-sm font-medium px-8 py-3.5 rounded-lg hover:-translate-y-0.5 transition-all duration-300">
|
||||
{hero.primaryCta}
|
||||
</a>
|
||||
<a href={localePath(locale, '/features')} class="w-full sm:w-auto text-center text-violet-600 border border-violet-200 hover:bg-violet-50 text-sm font-medium px-8 py-3.5 rounded-lg transition-all duration-300">
|
||||
{hero.secondaryCta}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="reveal stagger-5 text-slate-400 text-sm mt-12">{hero.trust}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
import { t, localePath, type Locale } from '../i18n/utils';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { marked } from 'marked';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
docType: 'privacy_policy' | 'terms_of_service' | 'about_us';
|
||||
}
|
||||
|
||||
const { locale, docType } = Astro.props;
|
||||
|
||||
const footer = t(locale, 'footer');
|
||||
const about = t(locale, 'about');
|
||||
|
||||
const titleMap: Record<string, Record<Locale, string>> = {
|
||||
privacy_policy: { zh: '隐私政策', zh_Hant: '隱私政策', en: 'Privacy Policy' },
|
||||
terms_of_service: { zh: '服务条款', zh_Hant: '服務條款', en: 'Terms of Service' },
|
||||
};
|
||||
const subtitleMap: Record<string, Record<Locale, string>> = {
|
||||
privacy_policy: { zh: '最后更新日期:2026年4月27日', zh_Hant: '最後更新日期:2026年4月27日', en: 'Last updated: April 27, 2026' },
|
||||
terms_of_service: { zh: '最后更新日期:2026年4月27日', zh_Hant: '最後更新日期:2026年4月27日', en: 'Last updated: April 27, 2026' },
|
||||
};
|
||||
const title = titleMap[docType]?.[locale] ?? '';
|
||||
const subtitle = subtitleMap[docType]?.[locale] ?? '';
|
||||
|
||||
const filePath = path.resolve('public/legal', locale, `${docType}.md`);
|
||||
let raw = '';
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, 'utf-8');
|
||||
raw = raw.replace(/^#\s+.+\n*/m, '');
|
||||
} catch {
|
||||
raw = `Content not available.`;
|
||||
}
|
||||
const content = await marked(raw);
|
||||
|
||||
const sideInfo: Record<string, Record<Locale, { type: string; date: string; law?: string; email: string }>> = {
|
||||
privacy_policy: {
|
||||
zh: { type: '隐私政策', date: '2026年4月27日', email: 'ann@xunmee.com' },
|
||||
zh_Hant: { type: '隱私政策', date: '2026年4月27日', email: 'ann@xunmee.com' },
|
||||
en: { type: 'Privacy Policy', date: 'April 27, 2026', email: 'ann@xunmee.com' },
|
||||
},
|
||||
terms_of_service: {
|
||||
zh: { type: '服务条款', date: '2026年4月27日', law: '美国加利福尼亚州法律', email: 'ann@xunmee.com' },
|
||||
zh_Hant: { type: '服務條款', date: '2026年4月27日', law: '美國加利福尼亞州法律', email: 'ann@xunmee.com' },
|
||||
en: { type: 'Terms of Service', date: 'April 27, 2026', law: 'California, USA', email: 'ann@xunmee.com' },
|
||||
},
|
||||
};
|
||||
const info = sideInfo[docType]?.[locale];
|
||||
|
||||
const isActive = (linkType: string) => linkType === docType;
|
||||
---
|
||||
|
||||
<!-- Header -->
|
||||
<section class="w-full bg-gradient-to-b from-white to-violet-50 py-16 md:py-20 px-6 md:px-20 text-center">
|
||||
<h1 class="reveal text-slate-900 text-4xl md:text-[48px] font-extrabold">{title}</h1>
|
||||
<p class="reveal stagger-1 text-slate-500 text-lg mt-4">{subtitle}</p>
|
||||
</section>
|
||||
|
||||
<!-- Content: Left article + Right info card -->
|
||||
<section class="w-full py-16 md:py-20 px-6 md:px-20 bg-white">
|
||||
<div class="max-w-6xl mx-auto flex flex-col md:flex-row gap-16">
|
||||
<!-- Article -->
|
||||
<div class="reveal flex-1 prose prose-slate max-w-none
|
||||
[&_h2]:text-[28px] [&_h2]:font-bold [&_h2]:text-slate-900 [&_h2]:mt-8 [&_h2]:mb-2
|
||||
[&_h3]:text-2xl [&_h3]:font-bold [&_h3]:text-slate-900 [&_h3]:mt-6 [&_h3]:mb-2
|
||||
[&_p]:text-slate-500 [&_p]:text-base [&_p]:leading-loose
|
||||
[&_strong]:text-slate-700 [&_ul]:list-disc [&_ul]:pl-6 [&_li]:text-slate-500 [&_li]:leading-loose
|
||||
[&_a]:text-violet-600 [&_hr]:border-slate-200 [&_hr]:my-6">
|
||||
<Fragment set:html={content} />
|
||||
</div>
|
||||
|
||||
<!-- Side card -->
|
||||
{info && (
|
||||
<div class="reveal-right w-full md:w-[320px] bg-slate-50 border border-slate-200 rounded-2xl p-8 flex flex-col gap-6 shrink-0 self-start sticky top-28">
|
||||
<h3 class="text-slate-900 text-xl font-bold">
|
||||
{locale === 'en' ? 'Document Info' : '文档信息'}
|
||||
</h3>
|
||||
<div class="h-px bg-slate-200"></div>
|
||||
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{locale === 'en' ? 'Type' : '文档类型'}</p>
|
||||
<p class="text-slate-600 text-[15px]">{info.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{locale === 'en' ? 'Last Updated' : '最后更新'}</p>
|
||||
<p class="text-slate-600 text-[15px]">{info.date}</p>
|
||||
</div>
|
||||
{info.law && (
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{locale === 'en' ? 'Governing Law' : '适用法律'}</p>
|
||||
<p class="text-slate-600 text-[15px]">{info.law}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p class="text-slate-400 text-sm font-semibold">{locale === 'en' ? 'Contact' : '联系邮箱'}</p>
|
||||
<p class="text-slate-600 text-[15px]">{info.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Warning -->
|
||||
<section class="w-full bg-amber-50 py-20 px-6 md:px-20">
|
||||
<div class="reveal max-w-[800px] mx-auto text-center flex flex-col gap-5">
|
||||
<h3 class="text-amber-600 text-2xl font-bold">{about.warningTitle}</h3>
|
||||
<p class="text-amber-700 text-[15px] leading-loose">{about.warningBody}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Legal Links -->
|
||||
<section class="w-full bg-slate-50 py-12 px-6 md:px-20">
|
||||
<div class="reveal max-w-[600px] mx-auto text-center flex flex-col gap-5">
|
||||
<h3 class="text-slate-900 text-xl font-bold">{about.legalTitle}</h3>
|
||||
<div class="flex justify-center gap-8">
|
||||
<a href={localePath(locale, '/privacy')} class={`text-[15px] hover:underline ${isActive('privacy_policy') ? 'text-violet-600 font-semibold' : 'text-slate-500'}`}>{footer.col3Link1}</a>
|
||||
<a href={localePath(locale, '/terms')} class={`text-[15px] hover:underline ${isActive('terms_of_service') ? 'text-violet-600 font-semibold' : 'text-slate-500'}`}>{footer.col3Link2}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import { t, localePath, getLocaleLabel, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const currentPath = Astro.url.pathname.replace(new RegExp(`^/(zh|zh_Hant|en)`), '');
|
||||
const nav = t(locale, 'nav');
|
||||
const footer = t(locale, 'footer');
|
||||
const otherLocales: Locale[] = (['zh', 'zh_Hant', 'en'] as Locale[]).filter((l) => l !== locale);
|
||||
---
|
||||
|
||||
<header class="w-full flex items-center justify-between h-16 md:h-20 px-5 md:px-20 border-b border-slate-200 bg-white sticky top-0 z-50">
|
||||
<a href={localePath(locale, '/')} class="flex items-center gap-2 md:gap-3 shrink-0">
|
||||
<img src="/images/logo.png" alt="MeiYao" class="w-8 h-8 md:w-9 md:h-9" />
|
||||
<span class="text-slate-900 text-lg md:text-xl font-bold whitespace-nowrap">{footer.brandName}</span>
|
||||
</a>
|
||||
|
||||
<nav class="flex items-center gap-4 md:gap-8">
|
||||
<a href={localePath(locale, '/features')} class="hidden sm:block text-slate-600 text-sm hover:text-slate-900 whitespace-nowrap">{nav.features}</a>
|
||||
<a href={localePath(locale, '/pricing')} class="hidden sm:block text-slate-600 text-sm hover:text-slate-900 whitespace-nowrap">{nav.pricing}</a>
|
||||
<a href={localePath(locale, '/about')} class="hidden sm:block text-slate-600 text-sm hover:text-slate-900 whitespace-nowrap">{nav.about}</a>
|
||||
|
||||
<div class="relative group">
|
||||
<button class="flex items-center gap-1 px-2 py-1.5 text-xs text-slate-600 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 whitespace-nowrap">
|
||||
{getLocaleLabel(locale)}
|
||||
<svg class="w-3 h-3 text-slate-400" viewBox="0 0 12 12" fill="none"><path d="M3 5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<div class="absolute right-0 top-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg py-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50">
|
||||
{otherLocales.map((l) => (
|
||||
<a href={localePath(l, currentPath)} class="block px-4 py-2 text-sm text-slate-600 hover:bg-slate-50 hover:text-slate-900 whitespace-nowrap">
|
||||
{getLocaleLabel(l)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href={localePath(locale, '/login')} class="bg-violet-600 text-white text-xs md:text-sm font-semibold px-4 md:px-5 py-2 md:py-2.5 rounded-lg hover:bg-violet-700 transition-colors whitespace-nowrap">
|
||||
{nav.getStarted}
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const p = t(locale, 'pricing');
|
||||
|
||||
const plans = [
|
||||
{ name: p.p1Name, badge: p.p1Badge, price: p.p1Price, credits: p.p1Credits, desc: p.p1Desc, detail: '', featured: false },
|
||||
{ name: p.p2Name, badge: '', price: p.p2Price, credits: p.p2Credits, desc: p.p2Desc, detail: p.p2Detail, featured: false },
|
||||
{ name: p.p3Name, badge: p.p3Badge, price: p.p3Price, credits: p.p3Credits, desc: p.p3Desc, detail: '', featured: true },
|
||||
{ name: p.p4Name, badge: '', price: p.p4Price, credits: p.p4Credits, desc: p.p4Desc, detail: p.p4Detail, featured: false },
|
||||
];
|
||||
---
|
||||
|
||||
<!-- Header -->
|
||||
<section class="w-full py-24 md:py-32 relative overflow-hidden">
|
||||
<div class="glow-bg absolute inset-0 pointer-events-none"></div>
|
||||
<div class="relative text-center px-6 max-w-3xl mx-auto">
|
||||
<h1 class="reveal stagger-1 text-slate-900 text-3xl md:text-4xl lg:text-5xl font-bold mb-4">{p.title}</h1>
|
||||
<p class="reveal stagger-2 text-slate-500 text-lg max-w-xl mx-auto">{p.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pricing Cards -->
|
||||
<section class="w-full py-16 md:py-24 px-6 md:px-20" style="background-color: #F8F7FC;">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{plans.map((plan, i) => (
|
||||
<div class={`reveal stagger-${(i % 4) + 1} rounded-2xl p-8 flex flex-col items-center text-center ${plan.featured ? 'bg-violet-50 border-2 border-violet-600 shadow-lg shadow-violet-100' : 'bg-white border border-slate-200'}`}>
|
||||
<h3 class="text-slate-900 text-[22px] font-bold whitespace-nowrap">{plan.name}</h3>
|
||||
{plan.badge && <span class={`text-xs font-medium mt-2 px-3 py-1 rounded-full ${plan.featured ? 'bg-violet-600 text-white' : 'bg-amber-100 text-amber-700'}`}>{plan.badge}</span>}
|
||||
<p class="text-slate-900 text-4xl font-extrabold mt-4">{plan.price}</p>
|
||||
<p class="text-violet-600 text-sm font-medium">{plan.credits}</p>
|
||||
<div class={`w-full h-px my-5 ${plan.featured ? 'bg-violet-200' : 'bg-slate-100'}`}></div>
|
||||
<p class="text-slate-500 text-sm">{plan.desc}</p>
|
||||
{plan.detail && <p class="text-slate-600 text-sm leading-relaxed mt-2">{plan.detail}</p>}
|
||||
<div class="flex-1 min-h-4"></div>
|
||||
<a href="#" class={`w-full text-center py-3 rounded-lg font-semibold transition-all duration-300 mt-6 ${plan.featured ? 'cyber-gradient cyber-glow text-white hover:-translate-y-0.5' : 'bg-white text-violet-600 border border-violet-200 hover:bg-violet-50'}`}>{p.buyNow}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const showcase = t(locale, 'showcase');
|
||||
const features = t(locale, 'features');
|
||||
---
|
||||
|
||||
<!-- Features Grid Section -->
|
||||
<section class="w-full py-24 md:py-32" style="background-color: #F8F7FC;">
|
||||
<div class="max-w-7xl mx-auto px-6">
|
||||
<div class="reveal text-center mb-16">
|
||||
<p class="text-xs font-medium tracking-[0.15em] text-violet-600 uppercase mb-4">
|
||||
{showcase.title}
|
||||
</p>
|
||||
<h2 class="text-slate-900 text-3xl md:text-4xl font-bold mb-4">
|
||||
{features.c1Title}
|
||||
</h2>
|
||||
<p class="text-slate-500 text-lg max-w-xl mx-auto">{showcase.desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="reveal stagger-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 10 10 0 000-20z"/><path d="M2 12h20"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c1Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c1Desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c2Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c2Desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c3Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c3Desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c4Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c4Desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c5Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c5Desc}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-white rounded-xl p-8 border border-violet-100">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mb-5 bg-violet-50">
|
||||
<svg class="w-6 h-6 text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-3 text-slate-900">{features.c6Title}</h3>
|
||||
<p class="text-sm leading-relaxed text-slate-500">{features.c6Desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const test = t(locale, 'testimonials');
|
||||
---
|
||||
|
||||
<section class="w-full py-24 md:py-32 px-6 md:px-20 bg-slate-900">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h2 class="reveal text-white text-3xl md:text-[40px] font-bold text-center mb-12">
|
||||
{test.title}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="reveal stagger-1 bg-slate-800 rounded-2xl p-8 flex flex-col justify-between min-h-[200px]">
|
||||
<p class="text-slate-300 text-base leading-relaxed">"{test.t1Quote}"</p>
|
||||
<div class="flex items-center gap-3 mt-6">
|
||||
<div class="w-10 h-10 bg-indigo-500 rounded-full shrink-0"></div>
|
||||
<span class="text-white text-sm font-medium">{test.t1Name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reveal stagger-2 bg-slate-800 rounded-2xl p-8 flex flex-col justify-between min-h-[200px]">
|
||||
<p class="text-slate-300 text-base leading-relaxed">"{test.t2Quote}"</p>
|
||||
<div class="flex items-center gap-3 mt-6">
|
||||
<div class="w-10 h-10 bg-violet-500 rounded-full shrink-0"></div>
|
||||
<span class="text-white text-sm font-medium">{test.t2Name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reveal stagger-3 bg-slate-800 rounded-2xl p-8 flex flex-col justify-between min-h-[200px]">
|
||||
<p class="text-slate-300 text-base leading-relaxed">"{test.t3Quote}"</p>
|
||||
<div class="flex items-center gap-3 mt-6">
|
||||
<div class="w-10 h-10 bg-cyan-500 rounded-full shrink-0"></div>
|
||||
<span class="text-white text-sm font-medium">{test.t3Name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user