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,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>
|
||||
Reference in New Issue
Block a user