blob: 76d0ae08cd31eddb0adbefbf2853ef1ab3f2a14e [file]
import Translate, { translate } from '@docusaurus/Translate';
import React, { JSX, useEffect, useId, useMemo, useState } from 'react';
import Link from '@site/src/components/home-next/LocalizedLink';
import { LayoutNext } from '../home-next/LayoutNext';
import { keyFeatureCards, localizedKeyFeatureCards, type KeyFeatureCard } from '@site/src/generated/key-features';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { featureTagLabels } from './feature-tags';
import './KeyFeaturesNext.scss';
function useRevealObserver(): void {
useEffect(() => {
const items = document.querySelectorAll<HTMLElement>('[data-reveal]');
if (!('IntersectionObserver' in window)) {
items.forEach((item) => item.classList.add('is-visible'));
return undefined;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
);
items.forEach((item) => observer.observe(item));
return () => observer.disconnect();
}, []);
}
function BoltIcon({ size = 24, color = 'var(--brand-accent)' }: { size?: number | string; color?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M13 2L3 14h7l-1 8 11-13h-7l1-7z"
fill={color}
stroke={color}
strokeWidth="0.5"
strokeLinejoin="round"
/>
</svg>
);
}
function matches(entry: KeyFeatureCard, query: string): boolean {
if (!query) return true;
const needle = query.trim().toLowerCase();
if (!needle) return true;
if (entry.title.toLowerCase().includes(needle)) return true;
if (entry.description.toLowerCase().includes(needle)) return true;
if (entry.tags?.some((tag) => tag.toLowerCase().includes(needle))) return true;
return false;
}
function themeFor(index: number): string {
const themes = ['t-green', 't-cream', 't-green-deep', 't-cream-warm'];
return themes[index % themes.length];
}
type TileSize = 'sm' | 'wide' | 'tall' | 'big';
const SIZE_PATTERN: TileSize[] = [
'wide', 'sm', 'sm', 'tall',
'sm', 'big', 'sm', 'wide',
'sm', 'sm', 'tall', 'sm',
'wide', 'sm', 'sm', 'sm',
'tall', 'sm', 'wide', 'sm',
];
function sizeFor(index: number): TileSize {
return SIZE_PATTERN[index % SIZE_PATTERN.length];
}
function KeyFeaturesHero(): JSX.Element {
return (
<section className="kf-next__hero" id="hero">
<div className="kf-next__hero-bg" aria-hidden="true" />
<div className="kf-next__hero-grid" aria-hidden="true" />
<div className="kf-next__container">
<div className="kf-next__hero-stack">
<h1 className="kf-next__hero-title" data-reveal data-reveal-delay="1">
<Translate id="marketing.features.hero" values={{
lineBreak: <br />,
doris: <span className="kf-next__accent">Apache Doris<span className="kf-next__bolt-inline"><BoltIcon size="0.85em" /></span></span>,
}}>
{'Everything you need to know{lineBreak}about {doris}'}
</Translate>
</h1>
<p className="kf-next__hero-sub" data-reveal data-reveal-delay="2">
{translate({ id: "marketing.features.003", message: "A reference index for the core technologies and key features of Apache Doris" })}</p>
</div>
</div>
</section>
);
}
function GlossarySection(): JSX.Element {
const { i18n: { currentLocale } } = useDocusaurusContext();
const cards = useMemo(() => (localizedKeyFeatureCards[currentLocale] ?? keyFeatureCards)
.map(card => ({ ...card, tags: card.tags.map(tag => featureTagLabels[tag] ?? tag) })), [currentLocale]);
const [query, setQuery] = useState('');
const inputId = useId();
const liveId = useId();
const filtered = useMemo(
() =>
cards.map((entry, index) => ({
entry,
originalIdx: index,
visible: matches(entry, query) || matches(keyFeatureCards[index], query),
})),
[cards, query]
);
const visibleCount = filtered.filter((item) => item.visible).length;
return (
<section className="kf-next__section kf-next__section--glossary" id="glossary">
<div className="kf-next__container kf-next__section-inner">
<div className="kf-next__glossary-controls" data-reveal>
<label className="kf-next__search" htmlFor={inputId}>
<svg
className="kf-next__search-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle cx="10.5" cy="10.5" r="6.5" stroke="currentColor" strokeWidth="1.8" />
<path
d="M15.5 15.5L20 20"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
<input
id={inputId}
type="search"
className="kf-next__search-input"
placeholder={translate({ id: "marketing.features.004", message: "Search features - try “lakehouse”, “MPP”, “index”…" })}
value={query}
onChange={(event) => setQuery(event.target.value)}
autoComplete="off"
spellCheck={false}
aria-describedby={liveId}
/>
{query && (
<button
type="button"
className="kf-next__search-clear"
onClick={() => setQuery('')}
aria-label={translate({ id: "marketing.features.005", message: "Clear search" })}
>
×
</button>
)}
</label>
<div className="kf-next__meta" id={liveId} aria-live="polite">
<span className="kf-next__meta-count">
<strong>{String(visibleCount).padStart(2, '0')}</strong>
{' / '}
{String(cards.length).padStart(2, '0')}
</span>
<span className="kf-next__meta-label">{translate({ id: "marketing.features.006", message: "features" })}</span>
</div>
</div>
<ul className="kf-next__grid" data-reveal data-reveal-delay="1">
{filtered.map(({ entry, originalIdx, visible }) => {
const size = sizeFor(originalIdx);
const showDesc = size === 'tall' || size === 'big';
return (
<li
key={entry.id}
className={[
'kf-next__tile',
themeFor(originalIdx),
`kf-next__tile--${size}`,
visible ? '' : 'is-hidden',
]
.filter(Boolean)
.join(' ')}
aria-hidden={visible ? 'false' : 'true'}
>
<Link className="kf-next__tile-link" to={entry.href}>
{entry.badge === 'doc' && (
<span className="kf-next__tile-badge-row">
<span className="kf-next__tile-badge" aria-label={translate({ id: "marketing.features.007", message: "Formal documentation" })}>
{translate({ id: "marketing.features.008", message: "Doc" })}</span>
</span>
)}
<span className="kf-next__tile-top">
<span className="kf-next__tile-name">{entry.title}</span>
{showDesc && entry.description && (
<span className="kf-next__tile-desc">{entry.description}</span>
)}
</span>
<span className="kf-next__tile-meta">
{entry.tags && entry.tags.length > 0 && (
<span className="kf-next__tile-tags">
{entry.tags.slice(0, 2).join(' · ')}
</span>
)}
<span className="kf-next__tile-arrow" aria-hidden="true">
→
</span>
</span>
</Link>
</li>
);
})}
</ul>
{visibleCount === 0 && (
<div className="kf-next__empty" role="status">
<Translate id="marketing.features.noResults" values={{
query: <strong>“{query}”</strong>,
clear: <button type="button" className="kf-next__empty-clear" onClick={() => setQuery('')}>
{translate({ id: 'marketing.features.011', message: 'clear the search' })}
</button>,
}}>
{'No features match {query}. Try a broader term, or {clear}.'}
</Translate>
</div>
)}
</div>
</section>
);
}
function KeyFeaturesContent(): JSX.Element {
useRevealObserver();
return (
<div className="kf-next">
<KeyFeaturesHero />
<GlossarySection />
</div>
);
}
export default function KeyFeaturesNext(): JSX.Element {
return (
<LayoutNext
title={translate({ id: "marketing.features.012", message: "Key Features" })}
description={translate({ id: "marketing.features.013", message: "A reference index for the core technologies and key features of Apache Doris, including real-time analytics, lakehouse federation, and hybrid search." })}
>
<KeyFeaturesContent />
</LayoutNext>
);
}