merge: bring stripe pricing console into release v0.2
This commit is contained in:
commit
67a119631d
@ -8,6 +8,8 @@
|
||||
:root {
|
||||
--font-geist-sans: "Geist", sans-serif;
|
||||
--font-geist-mono: "Geist Mono", monospace;
|
||||
--font-editorial-display:
|
||||
"Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
||||
--app-shell-nav-offset: 5.5rem;
|
||||
|
||||
/* Light theme defaults */
|
||||
@ -89,6 +91,11 @@ body {
|
||||
font-size: var(--type-body-size);
|
||||
line-height: var(--type-body-line-height);
|
||||
background-color: var(--color-background);
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.58),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
color: var(--color-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
@ -149,6 +156,13 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.editorial-display {
|
||||
font-family: var(--font-editorial-display);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.mobile-home-shell {
|
||||
--color-background: #fbfaf7;
|
||||
|
||||
330
src/app/page.tsx
330
src/app/page.tsx
@ -18,19 +18,25 @@ import {
|
||||
Terminal,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import Footer from "../components/Footer";
|
||||
import { HeroCard } from "../components/HeroCard";
|
||||
import UnifiedNavigation from "../components/UnifiedNavigation";
|
||||
import { useUserStore } from "../lib/userStore";
|
||||
import { useLanguage } from "../i18n/LanguageProvider";
|
||||
import { translations } from "../i18n/translations";
|
||||
import { useMoltbotStore } from "../lib/moltbotStore";
|
||||
import { useUserStore } from "../lib/userStore";
|
||||
import { cn } from "../lib/utils";
|
||||
import { AskAIDialog } from "../components/AskAIDialog";
|
||||
import { HeroCard } from "../components/HeroCard";
|
||||
import useSWR from "swr";
|
||||
|
||||
const HOME_SECTION_CLASS =
|
||||
"rounded-[2rem] border border-slate-900/10 bg-white/90 shadow-[0_18px_40px_rgba(15,23,42,0.05)]";
|
||||
const HOME_SECTION_LABEL_CLASS =
|
||||
"text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-text-subtle";
|
||||
const HOME_LIST_CARD_CLASS =
|
||||
"rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] transition duration-200";
|
||||
|
||||
const iconMap: Record<string, any> = {
|
||||
// English keys
|
||||
"Global Acceleration Network": Link,
|
||||
"Full-link SaaS Hosting": Layers,
|
||||
"AI-Driven Observability": Sparkles,
|
||||
@ -45,7 +51,6 @@ const iconMap: Record<string, any> = {
|
||||
"Machine-to-Machine": Layers,
|
||||
"Connect via CLI": Terminal,
|
||||
"REST & Admin APIs": Link,
|
||||
// Chinese keys
|
||||
全球加速网络: Link,
|
||||
"全链路 SaaS 托管": Layers,
|
||||
"AI 驱动的可观测性": Sparkles,
|
||||
@ -67,22 +72,22 @@ export default function HomePage() {
|
||||
const { mode, isOpen } = useMoltbotStore();
|
||||
|
||||
return (
|
||||
<div className="mobile-home-shell min-h-screen bg-background text-text transition-colors duration-150 flex flex-col overflow-x-hidden">
|
||||
<div className="mobile-home-shell relative flex min-h-screen flex-col overflow-x-hidden bg-background text-text transition-colors duration-150">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.56),rgba(255,255,255,0))]"
|
||||
/>
|
||||
<UnifiedNavigation />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 relative overflow-hidden",
|
||||
"relative flex flex-1 overflow-hidden",
|
||||
mode === "left-sidebar" && isOpen && "flex-row-reverse",
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto relative">
|
||||
<div className="relative flex-1 overflow-y-auto">
|
||||
<div className="relative mx-auto max-w-6xl px-4 pb-16 sm:px-6 sm:pb-20">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(37,78,219,0.08),transparent_28%),radial-gradient(circle_at_bottom_right,rgba(15,23,42,0.05),transparent_32%),linear-gradient(180deg,rgba(255,255,255,0.82),transparent_58%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<main className="relative space-y-8 pt-6 sm:space-y-12 sm:pt-10">
|
||||
<main className="relative space-y-6 pt-6 sm:space-y-8 sm:pt-10">
|
||||
<HeroSection />
|
||||
<NextStepsSection />
|
||||
<StatsSection />
|
||||
@ -101,70 +106,113 @@ export default function HomePage() {
|
||||
export function HeroSection() {
|
||||
const { user } = useUserStore();
|
||||
const { language } = useLanguage();
|
||||
const isChinese = language === "zh";
|
||||
const t = translations[language].marketing.home;
|
||||
|
||||
return (
|
||||
<section className="grid gap-8 lg:grid-cols-[0.9fr_1.1fr] lg:gap-12">
|
||||
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{t.hero.eyebrow && (
|
||||
<p className="font-semibold uppercase tracking-[0.28em] text-text-subtle">
|
||||
{t.hero.eyebrow}
|
||||
<section className="relative overflow-hidden rounded-[2.75rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_24px_56px_rgba(15,23,42,0.05)] sm:p-8 lg:p-10">
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0">
|
||||
<div className="absolute left-[8%] top-[8%] h-[16rem] w-[16rem] rounded-full bg-[radial-gradient(circle,rgba(37,78,219,0.1),transparent_64%)] blur-3xl" />
|
||||
<div className="absolute left-[30%] top-[12%] h-[14rem] w-[14rem] rounded-full bg-[radial-gradient(circle,rgba(245,211,170,0.42),transparent_66%)] blur-3xl" />
|
||||
<div className="absolute right-[10%] top-[10%] h-[18rem] w-[18rem] rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.92),transparent_72%)]" />
|
||||
<div className="absolute inset-x-0 top-0 h-[18rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0)_72%)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative grid gap-8 lg:grid-cols-[0.96fr_1.04fr] lg:gap-12">
|
||||
<div className="flex flex-col justify-between gap-8">
|
||||
<div className="space-y-5">
|
||||
{t.hero.eyebrow ? (
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>{t.hero.eyebrow}</p>
|
||||
) : null}
|
||||
<h1
|
||||
className={cn(
|
||||
"max-w-[11ch] leading-[0.88] text-heading",
|
||||
isChinese
|
||||
? "text-[2.85rem] font-semibold tracking-[-0.08em] sm:text-[3.4rem] lg:text-[4.5rem]"
|
||||
: "editorial-display text-[3.05rem] tracking-[-0.06em] sm:text-[3.6rem] lg:text-[4.8rem]",
|
||||
)}
|
||||
>
|
||||
{t.hero.title}
|
||||
</h1>
|
||||
<p className="max-w-xl text-[1rem] leading-8 text-text-muted sm:text-[1.05rem]">
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
)}
|
||||
<h1 className="max-w-[12ch] text-[2.22rem] font-semibold leading-[0.92] tracking-[-0.075em] text-heading sm:max-w-none sm:text-3xl lg:text-[3.35rem]">
|
||||
{t.hero.title}
|
||||
</h1>
|
||||
<p className="max-w-xl text-[1.02rem] leading-7 text-text-muted">
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center sm:gap-3">
|
||||
{user ? (
|
||||
<div className="flex items-center justify-center gap-2 rounded-full border border-success/30 bg-success/10 px-4 py-2 text-sm font-medium text-success sm:justify-start sm:py-1.5">
|
||||
<div className="h-2 w-2 rounded-full bg-success animate-pulse" />
|
||||
{t.signedIn.replace("{{username}}", user.username)}
|
||||
</div>
|
||||
) : (
|
||||
<button className="flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3 text-sm font-semibold text-white transition hover:bg-primary-hover sm:py-2.5">
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
{t.heroButtons.create}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{user ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-success/25 bg-success/10 px-4 py-2 text-sm font-semibold text-success">
|
||||
<div className="h-2 w-2 rounded-full bg-success animate-pulse" />
|
||||
{t.signedIn.replace("{{username}}", user.username)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-slate-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-primary"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
{t.heroButtons.create}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-6 py-3 text-sm font-semibold text-slate-800 transition hover:bg-slate-50"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
{t.heroButtons.playground}
|
||||
</button>
|
||||
)}
|
||||
<button className="flex items-center justify-center gap-2 rounded-full border border-surface-border bg-surface/90 px-6 py-3 text-sm font-semibold text-text transition hover:bg-surface-hover sm:py-2.5">
|
||||
<Play className="h-4 w-4" />
|
||||
{t.heroButtons.playground}
|
||||
</button>
|
||||
<button className="flex items-center justify-center gap-2 rounded-full border border-surface-border bg-surface/90 px-6 py-3 text-sm font-semibold text-text transition hover:bg-surface-hover sm:py-2.5">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
{t.heroButtons.tutorials}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<p className="text-text-muted">{t.trustedBy}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LogoPill label="Next.js" />
|
||||
<LogoPill label="Go" />
|
||||
<LogoPill label="Vercel" />
|
||||
<LogoPill label="Cloud Run" />
|
||||
<LogoPill label="PostgreSQL" />
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-[#f8f4ec] px-6 py-3 text-sm font-semibold text-slate-800 transition hover:bg-[#f2ebdd]"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
{t.heroButtons.tutorials}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-slate-900/10 pt-5">
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>{t.trustedBy}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LogoPill label="Next.js" />
|
||||
<LogoPill label="Go" />
|
||||
<LogoPill label="Vercel" />
|
||||
<LogoPill label="Cloud Run" />
|
||||
<LogoPill label="PostgreSQL" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:gap-4">
|
||||
<div className="relative flex flex-col gap-3 sm:gap-4">
|
||||
{t.heroCards.map((card) => {
|
||||
const Icon = getIcon(card.title, PlusCircle);
|
||||
return (
|
||||
<HeroCard
|
||||
key={card.title}
|
||||
icon={Icon}
|
||||
title={card.title}
|
||||
description={card.description}
|
||||
guide={card.guide}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex flex-col gap-3 lg:pl-4">
|
||||
<div className="flex flex-col gap-2 border-b border-slate-900/10 pb-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>
|
||||
{isChinese ? "主要入口" : "Launch paths"}
|
||||
</p>
|
||||
<p className="mt-2 max-w-md text-sm leading-6 text-text-muted">
|
||||
{isChinese
|
||||
? "从接入、托管到观测,保留原有入口,但改成更轻的阅读节奏。"
|
||||
: "Keep the same entry points, but present them with a calmer editorial rhythm."}
|
||||
</p>
|
||||
</div>
|
||||
<span className="hidden rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
|
||||
{t.heroCards.length} {isChinese ? "个入口" : "entry paths"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{t.heroCards.map((card) => {
|
||||
const Icon = getIcon(card.title, PlusCircle);
|
||||
return (
|
||||
<HeroCard
|
||||
key={card.title}
|
||||
icon={Icon}
|
||||
title={card.title}
|
||||
description={card.description}
|
||||
guide={card.guide}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -176,40 +224,50 @@ export function NextStepsSection() {
|
||||
const t = translations[language].marketing.home;
|
||||
|
||||
return (
|
||||
<section className="space-y-4 rounded-[1.75rem] border border-surface-border/70 bg-white/70 p-5 shadow-[0_16px_45px_rgba(15,23,42,0.05)] lg:rounded-none lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none">
|
||||
<header className="flex flex-col gap-2 text-sm text-text-muted sm:flex-row sm:items-center sm:gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{t.nextSteps.title}
|
||||
</p>
|
||||
<span className="w-fit rounded-full bg-surface-muted px-3 py-1 text-xs font-semibold text-primary">
|
||||
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-5 lg:p-7")}>
|
||||
<header className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>{t.nextSteps.title}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-text-muted">
|
||||
{language === "zh"
|
||||
? "保留原有 onboarding 内容,但改成更轻、更整齐的起步列表。"
|
||||
: "Keep the same onboarding content, in a lighter and calmer starting list."}
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex w-fit items-center rounded-full border border-slate-900/10 bg-[#f8f4ec] px-3 py-1 text-xs font-semibold text-slate-700">
|
||||
{t.nextSteps.badge}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||
{t.nextSteps.items.map((item, index: number) => {
|
||||
const Icon = getIcon(item.title, Users);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-3 rounded-[1.4rem] border border-surface-border bg-surface/92 p-4 shadow-lg shadow-shadow-sm"
|
||||
className={cn(
|
||||
HOME_LIST_CARD_CLASS,
|
||||
"flex h-full flex-col justify-between gap-6 p-4 hover:-translate-y-[1px] hover:bg-white",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/12 text-primary">
|
||||
<Icon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wide text-primary-muted">
|
||||
<span className="rounded-full bg-primary/20 px-2 py-0.5">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
|
||||
<Icon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<span className="rounded-full border border-slate-900/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-heading">
|
||||
<p className="text-base font-semibold leading-7 text-slate-900">
|
||||
{item.title}
|
||||
</p>
|
||||
<button className="inline-flex items-center gap-1 text-xs font-semibold text-primary transition hover:text-primary-hover">
|
||||
{t.nextSteps.learnMore}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="inline-flex items-center gap-1 text-sm font-semibold text-primary transition hover:text-primary-hover">
|
||||
{t.nextSteps.learnMore}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -286,17 +344,33 @@ export function StatsSection() {
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-[1.9rem] border border-surface-border/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.92),rgba(243,244,246,0.88))] p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] sm:p-6">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-3 lg:grid-cols-5">
|
||||
<section className={cn(HOME_SECTION_CLASS, "space-y-5 p-5 lg:p-7")}>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>
|
||||
{language === "zh" ? "平台统计" : "Platform pulse"}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-6 text-text-muted">
|
||||
{language === "zh"
|
||||
? "把关键数字收在同一条平静的视线上,不再单独做成重型数据舱。"
|
||||
: "Keep key numbers in the same calm visual rhythm instead of a separate heavy dashboard block."}
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex w-fit items-center rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600">
|
||||
{language === "zh" ? "每小时更新" : "Updated hourly"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3 lg:grid-cols-5">
|
||||
{displayStats.map((stat, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-1 text-left even:text-right md:text-left"
|
||||
className="rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] px-4 py-5"
|
||||
>
|
||||
<div className="text-[2rem] font-semibold tracking-[-0.06em] text-heading sm:text-3xl">
|
||||
<div className="editorial-display text-[2.2rem] leading-none text-slate-950 sm:text-[2.7rem]">
|
||||
{stat.value}
|
||||
</div>
|
||||
<p className="max-w-[9rem] text-sm text-text-muted even:ml-auto md:max-w-none">
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
@ -306,16 +380,6 @@ export function StatsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
type HomeStatsResponse = {
|
||||
registeredUsers: number | null;
|
||||
visits: {
|
||||
daily: number | null;
|
||||
weekly: number | null;
|
||||
monthly: number | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function ShortcutsSection() {
|
||||
const { language } = useLanguage();
|
||||
const t = translations[language].marketing.home;
|
||||
@ -352,26 +416,37 @@ export function ShortcutsSection() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<section className="space-y-4 rounded-[1.75rem] border border-surface-border/70 bg-white/70 p-5 shadow-[0_16px_45px_rgba(15,23,42,0.05)] lg:rounded-none lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-5 lg:p-7")}>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{t.shortcuts.title}
|
||||
<p className={HOME_SECTION_LABEL_CLASS}>{t.shortcuts.title}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-text-muted">
|
||||
{t.shortcuts.subtitle}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-text-muted">{t.shortcuts.subtitle}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs font-semibold text-primary">
|
||||
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-900/10 bg-slate-950 px-3 py-2 text-white transition hover:bg-primary"
|
||||
>
|
||||
{t.shortcuts.buttons.start}
|
||||
</button>
|
||||
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
{t.shortcuts.buttons.docs}
|
||||
</button>
|
||||
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
{t.shortcuts.buttons.guides}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{shortcutItems.map((item, index: number) => {
|
||||
const Icon = getIcon(item.title, Sparkles);
|
||||
@ -379,19 +454,24 @@ export function ShortcutsSection() {
|
||||
<a
|
||||
key={index}
|
||||
href={item.href}
|
||||
className="group flex items-start gap-3 rounded-[1.4rem] border border-surface-border bg-surface/92 p-4 transition hover:-translate-y-[1px] hover:border-primary/50 hover:bg-surface-hover"
|
||||
className={cn(
|
||||
HOME_LIST_CARD_CLASS,
|
||||
"group flex items-start gap-3 p-4 hover:-translate-y-[1px] hover:bg-white",
|
||||
)}
|
||||
>
|
||||
<div className="mt-1 flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/12 text-primary">
|
||||
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
|
||||
<Icon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="text-sm font-semibold text-heading">
|
||||
<div className="text-base font-semibold leading-7 text-slate-900">
|
||||
{item.title}
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">{item.description}</p>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight
|
||||
className="ml-auto h-4 w-4 text-text-subtle transition group-hover:text-primary"
|
||||
className="ml-auto h-4 w-4 text-slate-500 transition group-hover:text-primary"
|
||||
aria-hidden
|
||||
/>
|
||||
</a>
|
||||
@ -402,6 +482,16 @@ export function ShortcutsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
type HomeStatsResponse = {
|
||||
registeredUsers: number | null;
|
||||
visits: {
|
||||
daily: number | null;
|
||||
weekly: number | null;
|
||||
monthly: number | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type LatestBlogPost = {
|
||||
slug: string;
|
||||
title: string;
|
||||
@ -410,8 +500,8 @@ type LatestBlogPost = {
|
||||
|
||||
function LogoPill({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-surface-border bg-surface/88 px-3.5 py-1.5 text-xs font-semibold text-text shadow-[0_8px_22px_rgba(15,23,42,0.04)]">
|
||||
<div className="h-2 w-2 rounded-full bg-success" />
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-3.5 py-1.5 text-xs font-semibold text-slate-700">
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -36,25 +36,16 @@ export default function Footer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="mt-12 flex flex-col items-center justify-center gap-4 rounded-[1.75rem] border border-surface-border bg-surface/88 px-6 py-4 text-sm text-text-muted shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:rounded-2xl lg:border-white/10 lg:bg-white/5 lg:text-slate-300 lg:shadow-none">
|
||||
<footer className="mt-12 flex flex-col items-center justify-center gap-4 rounded-[2rem] border border-white/75 bg-[linear-gradient(135deg,rgba(15,23,42,0.95),rgba(30,41,59,0.92))] px-6 py-5 text-sm text-slate-300 shadow-[0_24px_60px_rgba(15,23,42,0.16)]">
|
||||
<div className="flex w-full flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||||
<div className="flex gap-4 order-2 sm:order-1">
|
||||
<Link
|
||||
href="/terms"
|
||||
className="transition-colors hover:text-text lg:hover:text-white"
|
||||
>
|
||||
<Link href="/terms" className="transition-colors hover:text-white">
|
||||
{isChinese ? "服务条款" : "Terms of Service"}
|
||||
</Link>
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="transition-colors hover:text-text lg:hover:text-white"
|
||||
>
|
||||
<Link href="/privacy" className="transition-colors hover:text-white">
|
||||
{isChinese ? "隐私政策" : "Privacy Policy"}
|
||||
</Link>
|
||||
<Link
|
||||
href="/support"
|
||||
className="transition-colors hover:text-text lg:hover:text-white"
|
||||
>
|
||||
<Link href="/support" className="transition-colors hover:text-white">
|
||||
{isChinese ? "联系我们" : "Contact Us"}
|
||||
</Link>
|
||||
</div>
|
||||
@ -64,7 +55,7 @@ export default function Footer() {
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-surface-border bg-surface-muted text-text transition hover:border-surface-border-strong hover:text-text lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50 lg:hover:text-indigo-100"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition hover:border-indigo-400/50 hover:text-indigo-100"
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden />
|
||||
<span className="sr-only">{label}</span>
|
||||
@ -78,7 +69,7 @@ export default function Footer() {
|
||||
onClick={handleViewToggle}
|
||||
aria-label={viewToggleLabel}
|
||||
title={viewToggleLabel}
|
||||
className="group flex h-10 w-10 items-center justify-center rounded-full border border-surface-border bg-surface-muted text-text transition hover:border-surface-border-strong focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50"
|
||||
className="group flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition hover:border-indigo-400/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">
|
||||
{view === "classic" ? "view_quilt" : "view_cozy"}
|
||||
@ -90,21 +81,21 @@ export default function Footer() {
|
||||
aria-pressed={isDark}
|
||||
aria-label={toggleLabel}
|
||||
title={toggleLabel}
|
||||
className="group relative flex h-10 w-20 items-center rounded-full border border-surface-border bg-surface-muted px-2 text-text transition hover:border-surface-border-strong focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50"
|
||||
className="group relative flex h-10 w-20 items-center rounded-full border border-white/10 bg-white/5 px-2 text-white transition hover:border-indigo-400/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||
>
|
||||
<span className="relative z-10 flex w-full items-center justify-between text-text-subtle lg:text-slate-300">
|
||||
<span className="relative z-10 flex w-full items-center justify-between text-slate-300">
|
||||
<Moon
|
||||
className={`h-4 w-4 transition-colors ${isDark ? "text-text" : "text-text-subtle lg:text-slate-500"}`}
|
||||
className={`h-4 w-4 transition-colors ${isDark ? "text-white" : "text-slate-500"}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<Sun
|
||||
className={`h-4 w-4 transition-colors ${isDark ? "text-text-subtle lg:text-slate-500" : "text-amber-500 lg:text-amber-300"}`}
|
||||
className={`h-4 w-4 transition-colors ${isDark ? "text-slate-500" : "text-amber-300"}`}
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`absolute inset-y-1 left-1 h-8 w-8 rounded-full bg-background shadow-sm transition-transform duration-300 ease-out lg:bg-white/90 ${isDark ? "translate-x-0" : "translate-x-10"}`}
|
||||
className={`absolute inset-y-1 left-1 h-8 w-8 rounded-full bg-white shadow-sm transition-transform duration-300 ease-out ${isDark ? "translate-x-0" : "translate-x-10"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useState, type KeyboardEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, QrCode, X } from "lucide-react";
|
||||
|
||||
import { useLanguage } from "../i18n/LanguageProvider";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface GuideStep {
|
||||
@ -32,6 +33,13 @@ export function HeroCard({
|
||||
}: HeroCardProps) {
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
const hasGuide = Boolean(guide);
|
||||
const { language } = useLanguage();
|
||||
const isChinese = language === "zh";
|
||||
const guideLabel = isChinese ? "查看向导" : "View guide";
|
||||
const qrTitle = isChinese ? "VLESS 协议就绪" : "VLESS Protocol Ready";
|
||||
const qrDescription = isChinese
|
||||
? "在控制台中扫码,即可快速完成连接。"
|
||||
: "Scan the QR code in the control panel to connect automatically.";
|
||||
|
||||
function openGuide(): void {
|
||||
if (!hasGuide) {
|
||||
@ -59,26 +67,26 @@ export function HeroCard({
|
||||
onClick={hasGuide ? openGuide : undefined}
|
||||
onKeyDown={handleCardKeyDown}
|
||||
className={cn(
|
||||
"group relative flex items-start gap-4 overflow-hidden rounded-[1.6rem] border border-surface-border bg-white/88 p-5 shadow-[0_18px_42px_rgba(15,23,42,0.05)] transition-all duration-300 sm:rounded-2xl sm:p-6",
|
||||
"group relative flex items-start gap-4 overflow-hidden rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] p-5 transition-all duration-200 sm:p-6",
|
||||
hasGuide
|
||||
? "cursor-pointer hover:border-primary/50 hover:bg-surface-hover"
|
||||
: "hover:border-primary/50 hover:bg-surface-hover",
|
||||
showGuide ? "border-primary/50 shadow-lg" : "",
|
||||
? "cursor-pointer hover:-translate-y-[1px] hover:border-primary/35 hover:bg-white"
|
||||
: "hover:-translate-y-[1px] hover:border-primary/25 hover:bg-white",
|
||||
showGuide ? "border-primary/40 bg-white" : "",
|
||||
)}
|
||||
>
|
||||
<div className="mt-1 rounded-full border border-surface-border bg-surface-muted p-2.5 group-hover:border-primary/50 group-hover:text-primary">
|
||||
<div className="mt-1 rounded-full border border-slate-900/10 bg-white p-2.5 text-slate-700 group-hover:border-primary/30 group-hover:text-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold tracking-[-0.03em] text-heading">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[1.05rem] font-semibold leading-7 tracking-[-0.03em] text-slate-900">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm leading-6 text-text-muted">{description}</p>
|
||||
<p className="text-sm leading-6 text-slate-600">{description}</p>
|
||||
</div>
|
||||
{hasGuide ? (
|
||||
<span className="inline-flex w-fit shrink-0 items-center gap-1 rounded-full border border-primary/20 bg-primary/10 px-3 py-1.5 text-xs font-semibold text-primary">
|
||||
点击查看向导
|
||||
<span className="inline-flex w-fit shrink-0 items-center gap-1 rounded-full border border-slate-900/10 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600">
|
||||
{guideLabel}
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
) : null}
|
||||
@ -153,11 +161,10 @@ export function HeroCard({
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold text-heading">
|
||||
VLESS Protocol Ready
|
||||
{qrTitle}
|
||||
</p>
|
||||
<p className="text-xs leading-relaxed text-text-muted">
|
||||
Scan the QR code in the control panel to connect
|
||||
automatically.
|
||||
{qrDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user