| 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> | |
| ); | |
| } |