Restore news and community sections below product matrix (#537)

This commit is contained in:
shenlan 2025-10-17 10:08:44 +08:00 committed by GitHub
parent ef077917ae
commit 98609cb9b0
10 changed files with 257 additions and 121 deletions

View File

@ -5,8 +5,6 @@ import Navbar from '@components/Navbar'
import { AskAIButton } from '@components/AskAIButton'
import ArticleFeed from '@components/home/ArticleFeed'
import ContactPanel from '@components/home/ContactPanel'
import HeroBanner from '@components/home/HeroBanner'
import ProductMatrix from '@components/home/ProductMatrix'
import Sidebar from '@components/home/Sidebar'
@ -14,17 +12,19 @@ export default function HomePage() {
return (
<>
<Navbar />
<main className="bg-slate-50 pb-16 pt-24">
<HeroBanner />
<section className="relative z-10 -mt-12 px-4 sm:-mt-20">
<div className="mx-auto max-w-6xl">
<div className="grid gap-8 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
<div className="space-y-8">
<ProductMatrix />
<main className="bg-slate-950">
<section className="pb-24 pt-24">
<div className="px-4">
<div className="mx-auto max-w-6xl">
<ProductMatrix />
</div>
</div>
</section>
<section className="bg-slate-50 pb-16 pt-20">
<div className="px-4">
<div className="mx-auto max-w-6xl">
<div className="grid gap-8 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
<ArticleFeed />
</div>
<div className="space-y-6">
<ContactPanel />
<Sidebar />
</div>
</div>

View File

@ -10,13 +10,11 @@ import {
X,
type LucideIcon,
} from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import clsx from 'clsx'
import type { ContactPanelContent, ContactItemContent } from '@lib/homepageContent'
const STORAGE_KEY = 'xcontrol-homepage-contact-collapsed'
const iconMap: Record<string, LucideIcon> = {
mail: Mail,
'life-buoy': LifeBuoy,
@ -157,17 +155,6 @@ function InfoCard({ item }: { item: ContactItemContent }) {
export default function ContactPanelClient({ panel, className }: ContactPanelClientProps) {
const [collapsed, setCollapsed] = useState(false)
useEffect(() => {
const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored === 'true') {
setCollapsed(true)
}
}, [])
useEffect(() => {
window.localStorage.setItem(STORAGE_KEY, collapsed ? 'true' : 'false')
}, [collapsed])
if (!panel.items.length) {
return null
}

View File

@ -143,7 +143,8 @@ export default function HeroProductTabs({ items }: HeroProductTabsProps) {
/>
) : null}
{(activeItem.primaryCtaLabel && activeItem.primaryCtaHref) ||
(activeItem.secondaryCtaLabel && activeItem.secondaryCtaHref) ? (
(activeItem.secondaryCtaLabel && activeItem.secondaryCtaHref) ||
(activeItem.tertiaryCtaLabel && activeItem.tertiaryCtaHref) ? (
<div className="mt-6 flex flex-wrap gap-3">
{activeItem.primaryCtaLabel && activeItem.primaryCtaHref ? (
<Link
@ -163,6 +164,15 @@ export default function HeroProductTabs({ items }: HeroProductTabsProps) {
{activeItem.secondaryCtaLabel}
</Link>
) : null}
{activeItem.tertiaryCtaLabel && activeItem.tertiaryCtaHref ? (
<Link
prefetch={false}
href={activeItem.tertiaryCtaHref}
className="inline-flex items-center justify-center rounded-full border border-white/10 px-5 py-2 text-sm font-semibold text-slate-100 transition hover:border-white/40"
>
{activeItem.tertiaryCtaLabel}
</Link>
) : null}
</div>
) : null}
</div>

View File

@ -1,14 +1,8 @@
import Link from 'next/link'
import ContactPanel from './ContactPanel'
import ProductMatrixClient from './ProductMatrixClient'
import { getHeroSolutions } from '@lib/homepageContent'
function truncate(text: string, maxLength: number) {
if (text.length <= maxLength) {
return text
}
return `${text.slice(0, maxLength - 1)}`
}
export default async function ProductMatrix() {
const solutions = await getHeroSolutions()
@ -17,75 +11,9 @@ export default async function ProductMatrix() {
}
return (
<section className="space-y-6">
<header className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-sky-600"></p>
<h2 className="text-2xl font-semibold text-slate-900 sm:text-3xl"></h2>
</div>
<Link href="/docs" className="text-sm font-medium text-sky-600 hover:text-sky-700">
</Link>
</header>
<div className="grid gap-4 sm:grid-cols-2">
{solutions.map((solution) => (
<article
key={solution.slug}
className="group relative overflow-hidden rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg sm:p-7"
>
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-sky-400 via-cyan-400 to-indigo-400 opacity-0 transition group-hover:opacity-100" aria-hidden />
<div className="space-y-3">
<div>
<h3 className="text-xl font-semibold text-slate-900 transition group-hover:text-sky-600">
{solution.title}
</h3>
{solution.tagline ? (
<p className="mt-1 text-sm text-slate-500">{truncate(solution.tagline, 60)}</p>
) : null}
</div>
{solution.features.length ? (
<ul className="space-y-2 text-sm text-slate-600">
{solution.features.slice(0, 3).map((feature) => (
<li key={feature} className="flex items-start gap-2">
<span className="mt-1 inline-block h-1.5 w-1.5 flex-shrink-0 rounded-full bg-sky-400" aria-hidden />
<span>{feature}</span>
</li>
))}
</ul>
) : null}
{solution.bodyHtml ? (
<div
className="prose prose-sm max-w-none text-slate-600"
dangerouslySetInnerHTML={{ __html: solution.bodyHtml }}
/>
) : null}
</div>
{(solution.primaryCtaLabel && solution.primaryCtaHref) ||
(solution.secondaryCtaLabel && solution.secondaryCtaHref) ? (
<div className="mt-5 flex flex-wrap gap-3">
{solution.primaryCtaLabel && solution.primaryCtaHref ? (
<Link
prefetch={false}
href={solution.primaryCtaHref}
className="inline-flex items-center justify-center rounded-full bg-sky-500 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-sky-400"
>
{solution.primaryCtaLabel}
</Link>
) : null}
{solution.secondaryCtaLabel && solution.secondaryCtaHref ? (
<Link
prefetch={false}
href={solution.secondaryCtaHref}
className="inline-flex items-center justify-center rounded-full border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300"
>
{solution.secondaryCtaLabel}
</Link>
) : null}
</div>
) : null}
</article>
))}
</div>
</section>
<div className="grid gap-8 lg:grid-cols-[minmax(0,2.5fr)_minmax(0,1fr)] lg:items-start">
<ProductMatrixClient solutions={solutions} />
<ContactPanel className="lg:sticky lg:top-6" />
</div>
)
}

View File

@ -0,0 +1,199 @@
'use client'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import clsx from 'clsx'
import type { HeroSolution } from '@lib/homepageContent'
const OVERVIEW_HIGHLIGHTS = [
'跨集群与多云环境的一体化策略治理',
'以策略为核心的安全与合规自动化',
'将标准化模板加速落地业务流程',
]
type ProductMatrixClientProps = {
solutions: HeroSolution[]
}
export default function ProductMatrixClient({ solutions }: ProductMatrixClientProps) {
const [activeIndex, setActiveIndex] = useState(0)
const activeSolution = useMemo(() => solutions[activeIndex] ?? solutions[0], [solutions, activeIndex])
if (!solutions.length || !activeSolution) {
return null
}
const ctas = [
activeSolution.primaryCtaLabel && activeSolution.primaryCtaHref
? {
label: activeSolution.primaryCtaLabel,
href: activeSolution.primaryCtaHref,
variant: 'primary' as const,
}
: null,
activeSolution.secondaryCtaLabel && activeSolution.secondaryCtaHref
? {
label: activeSolution.secondaryCtaLabel,
href: activeSolution.secondaryCtaHref,
variant: 'secondary' as const,
}
: null,
activeSolution.tertiaryCtaLabel && activeSolution.tertiaryCtaHref
? {
label: activeSolution.tertiaryCtaLabel,
href: activeSolution.tertiaryCtaHref,
variant: 'ghost' as const,
}
: null,
].filter(Boolean) as Array<{ label: string; href: string; variant: 'primary' | 'secondary' | 'ghost' }>
return (
<section className="space-y-8">
<div className="relative overflow-hidden rounded-3xl border border-slate-800/60 bg-slate-950/90 p-8 text-slate-100 shadow-2xl shadow-sky-900/30 lg:p-10">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.25),_transparent_65%)]" aria-hidden />
<header className="space-y-3">
<span className="text-xs font-semibold uppercase tracking-[0.4em] text-sky-300/80"></span>
<h1 className="text-3xl font-semibold leading-tight text-white sm:text-4xl">
XControl
</h1>
<p className="text-sm text-slate-300 sm:text-base">
访
</p>
</header>
<ul className="mt-6 grid gap-3 text-sm text-slate-200 sm:grid-cols-3">
{OVERVIEW_HIGHLIGHTS.map((highlight) => (
<li
key={highlight}
className="flex items-start gap-3 rounded-2xl border border-white/10 bg-white/5 p-3"
>
<span className="mt-1 h-2 w-2 flex-shrink-0 rounded-full bg-sky-400" aria-hidden />
<span>{highlight}</span>
</li>
))}
</ul>
<div className="mt-8 rounded-3xl border border-white/10 bg-white/5 p-4">
<div className="flex flex-wrap gap-2">
{solutions.map((solution, index) => {
const isActive = index === activeIndex
return (
<button
key={solution.slug}
type="button"
onClick={() => setActiveIndex(index)}
className={clsx(
'flex min-w-[9rem] flex-1 items-center justify-between rounded-2xl border px-4 py-3 text-left text-sm font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-200/80',
isActive
? 'border-sky-300/80 bg-sky-300/90 text-slate-900 shadow-lg shadow-sky-500/30'
: 'border-white/10 bg-white/5 text-slate-100 hover:border-white/30 hover:bg-white/10',
)}
>
<span className="flex-1">{solution.title}</span>
<span
className={clsx(
'ml-2 text-xs font-medium transition',
isActive ? 'text-slate-800/80' : 'text-slate-200/60',
)}
>
{solution.tagline}
</span>
</button>
)
})}
</div>
<div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)]">
<div className="space-y-4">
{activeSolution.tagline ? (
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-sky-200/80">
{activeSolution.tagline}
</p>
) : null}
<h2 className="text-3xl font-semibold text-white sm:text-4xl">{activeSolution.title}</h2>
{activeSolution.description ? (
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-sky-200/70"></p>
<p className="text-sm text-slate-200/90 sm:text-base">{activeSolution.description}</p>
</div>
) : null}
{activeSolution.bodyHtml ? (
<div
className="prose prose-invert max-w-none text-sm text-slate-200/90 [&_strong]:text-white"
dangerouslySetInnerHTML={{ __html: activeSolution.bodyHtml }}
/>
) : null}
</div>
<div className="space-y-5">
{activeSolution.features.length ? (
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-sky-200/70"></p>
<ul className="space-y-2 text-sm text-slate-100">
{activeSolution.features.map((feature) => (
<li key={feature} className="flex items-start gap-3">
<span className="mt-1 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-sky-400" aria-hidden />
<span>{feature}</span>
</li>
))}
</ul>
</div>
) : null}
{ctas.length ? (
<div className="flex flex-wrap gap-3">
{ctas.map(({ href, label, variant }) => (
<Link
key={label}
prefetch={false}
href={href}
className={clsx(
'inline-flex items-center justify-center rounded-full px-5 py-2 text-sm font-semibold transition',
variant === 'primary'
? 'bg-sky-400 text-slate-950 shadow-lg shadow-sky-500/40 hover:bg-sky-300'
: variant === 'secondary'
? 'border border-white/40 text-white hover:border-white'
: 'border border-white/10 text-slate-100 hover:border-white/40',
)}
>
{label}
</Link>
))}
</div>
) : null}
</div>
</div>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{solutions.map((solution, index) => {
const isActive = index === activeIndex
return (
<button
key={solution.slug}
type="button"
onClick={() => setActiveIndex(index)}
className={clsx(
'relative overflow-hidden rounded-3xl border bg-white/5 p-6 text-left transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-200/80',
isActive
? 'border-sky-400/80 bg-sky-400/10 text-white shadow-lg shadow-sky-900/40'
: 'border-white/10 text-slate-200 hover:border-white/30 hover:bg-white/10',
)}
>
<p className="text-sm font-semibold uppercase tracking-[0.25em] text-sky-300/80">{solution.tagline}</p>
<h3 className="mt-3 text-xl font-semibold text-white">{solution.title}</h3>
{solution.description ? (
<p className="mt-2 text-sm text-slate-200/80">{solution.description}</p>
) : null}
<span
className={clsx(
'mt-4 inline-flex items-center text-sm font-semibold',
isActive ? 'text-sky-200' : 'text-sky-300/90',
)}
>
{isActive ? '正在专题展示' : '点击专题展示'}
</span>
</button>
)
})}
</div>
</section>
)
}

View File

@ -3,10 +3,12 @@ title: XCloudFlow
tagline: 多云 IaC
order: 1
description: 通过声明式模型统一编排多云基础设施,自动化落地资源策略与合规标准。
primaryCtaLabel: 了解 XCloudFlow
primaryCtaHref: /products/xcloudflow
secondaryCtaLabel: 产品文档
secondaryCtaHref: /docs/xcloudflow
primaryCtaLabel: 立刻体验
primaryCtaHref: /demo?product=xcloudflow
secondaryCtaLabel: 下载链接
secondaryCtaHref: /download?product=xcloudflow
tertiaryCtaLabel: 文档链接
tertiaryCtaHref: /docs/xcloudflow
features:
- 跨云资源蓝图与参数化交付
- GitOps 工作流驱动基础设施变更

View File

@ -3,10 +3,12 @@ title: XControl 平台
tagline: 云原生治理中枢
order: 3
description: 为多团队提供统一的权限、策略与工作流编排,让交付与治理协同无缝衔接。
primaryCtaLabel: 申请试用
primaryCtaHref: /trial
secondaryCtaLabel: 查看能力矩阵
secondaryCtaHref: /products/xcontrol#capabilities
primaryCtaLabel: 立刻体验
primaryCtaHref: /demo?product=xcontrol
secondaryCtaLabel: 下载链接
secondaryCtaHref: /download?product=xcontrol
tertiaryCtaLabel: 文档链接
tertiaryCtaHref: /docs/xcontrol
features:
- 一站式权限与合规策略中心
- 工作流自动化驱动跨团队协作

View File

@ -3,10 +3,12 @@ title: XScopeHub
tagline: AI & 可观察性
order: 2
description: 利用 AI 驱动的分析工作台,统一日志、指标与追踪,快速定位异常并推荐修复路径。
primaryCtaLabel: 探索 XScopeHub
primaryCtaHref: /products/xscopehub
secondaryCtaLabel: 体验 Demo
secondaryCtaHref: /demo/xscopehub
primaryCtaLabel: 立刻体验
primaryCtaHref: /demo?product=xscopehub
secondaryCtaLabel: 下载链接
secondaryCtaHref: /download?product=xscopehub
tertiaryCtaLabel: 文档链接
tertiaryCtaHref: /docs/xscopehub
features:
- 全栈可观察性数据联邦检索
- 智能告警关联与根因分析

View File

@ -3,10 +3,12 @@ title: XStream
tagline: 网络加速器
order: 4
description: 按需构建全球传输网络,保障跨地域应用与数据同步的稳定低时延体验。
primaryCtaLabel: 查看加速方案
primaryCtaHref: /products/xstream
secondaryCtaLabel: 下载白皮书
secondaryCtaHref: /resources/xstream-whitepaper
primaryCtaLabel: 立刻体验
primaryCtaHref: /demo?product=xstream
secondaryCtaLabel: 下载链接
secondaryCtaHref: /download?product=xstream
tertiaryCtaLabel: 文档链接
tertiaryCtaHref: /docs/xstream
features:
- 动态最优路径与带宽调度
- 内置零信任安全与访问控制

View File

@ -26,6 +26,8 @@ export interface HeroSolution {
primaryCtaHref?: string
secondaryCtaLabel?: string
secondaryCtaHref?: string
tertiaryCtaLabel?: string
tertiaryCtaHref?: string
}
export interface HomepagePost {
@ -153,6 +155,8 @@ export async function getHeroSolutions(): Promise<HeroSolution[]> {
primaryCtaHref: ensureString(file.metadata.primaryCtaHref),
secondaryCtaLabel: ensureString(file.metadata.secondaryCtaLabel),
secondaryCtaHref: ensureString(file.metadata.secondaryCtaHref),
tertiaryCtaLabel: ensureString(file.metadata.tertiaryCtaLabel),
tertiaryCtaHref: ensureString(file.metadata.tertiaryCtaHref),
order: ensureNumber(file.metadata.order),
}))
} catch (error) {