Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1b245c5
feat: add VibeCoding and Incremental Checkpoint blogs
faweizhao26 Jul 30, 2026
0255a25
fix: add missing image 6
faweizhao26 Jul 30, 2026
207053f
docs: define expert committee page design
faweizhao26 Aug 4, 2026
c777903
docs: plan expert committee implementation
faweizhao26 Aug 4, 2026
c1efa86
chore: ignore local agent workspaces
faweizhao26 Aug 4, 2026
e0e564b
feat: add expert committee profile data
faweizhao26 Aug 4, 2026
6da0939
assets: add expert committee portraits
faweizhao26 Aug 4, 2026
3696d66
feat: add expert advisory committee page
faweizhao26 Aug 4, 2026
f42131e
feat: link expert committee from resources
faweizhao26 Aug 4, 2026
1d3a715
fix: keep expert bios above cards
faweizhao26 Aug 4, 2026
ba62b0d
docs: specify expert committee content update
faweizhao26 Aug 5, 2026
ddc4627
docs: plan expert committee content update
faweizhao26 Aug 5, 2026
4af985d
content: update and alphabetize expert roster
faweizhao26 Aug 5, 2026
57ea06e
fix: validate expert roster order
faweizhao26 Aug 5, 2026
cf30e70
assets: update expert committee portraits
faweizhao26 Aug 5, 2026
e708337
feat: add expert committee join callout
faweizhao26 Aug 5, 2026
670796c
fix: close expert committee review gaps
faweizhao26 Aug 5, 2026
b368bb3
docs: specify expert portrait framing
faweizhao26 Aug 5, 2026
b6da9bb
docs: plan expert portrait framing
faweizhao26 Aug 5, 2026
32580e0
fix: improve expert portrait framing
faweizhao26 Aug 5, 2026
4e3ae85
test: cover expert portrait framing
faweizhao26 Aug 5, 2026
06db977
test: verify rendered portrait framing
faweizhao26 Aug 5, 2026
9e1abc0
fix: harden expert committee presentation
faweizhao26 Aug 5, 2026
3ad2a26
docs: specify expert portrait replacements
faweizhao26 Aug 5, 2026
f391282
docs: plan expert portrait replacements
faweizhao26 Aug 5, 2026
d7d5d2a
fix: refresh expert committee portraits
faweizhao26 Aug 9, 2026
dce3f7e
chore: remove unrelated blog changes
faweizhao26 Aug 9, 2026
b34e640
chore: keep expert page production files only
faweizhao26 Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ const config = {
label: 'Events',
to: '/events',
},
{
label: 'Expert Advisory Committee',
to: '/expert-advisory-committee',
},
{
label: 'Partners',
to: '/partners-page',
Expand Down
4 changes: 4 additions & 0 deletions i18n/zh-CN/docusaurus-theme-classic/navbar.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"message": "资源",
"description": "Navbar item with label Resources"
},
"item.label.Expert Advisory Committee": {
"message": "专家顾问委员会",
"description": "Navbar item with label Expert Advisory Committee"
},
"item.label.Online Trial": {
"message": "在线试用",
"description": "Navbar item with label Online Trial"
Expand Down
473 changes: 473 additions & 0 deletions src/data/expertCommittee.json

Large diffs are not rendered by default.

327 changes: 327 additions & 0 deletions src/pages/expert-advisory-committee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
import Layout from '@theme/Layout';
import useBaseUrl from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import experts from '../data/expertCommittee.json';
import {
getAvatarFramingStyle,
getFocusTrapTarget,
isDialogMode,
} from '../utils/expertCommitteeInteraction.cjs';
import getExpertPopoverPosition from '../utils/getExpertPopoverPosition.cjs';
import styles from './expert-advisory-committee.module.css';

const COPY = {
en: {
pageTitle: 'Expert Advisory Committee',
pageDescription: 'Meet the experts guiding IvorySQL technology and community development.',
eyebrow: 'Community Leadership',
heroTitle: 'Expert Advisory Committee',
heroDescription:
'Bringing together experienced database specialists and open-source leaders from around the world to provide long-term guidance for IvorySQL technology, ecosystem, and community development.',
expertCount: 'experts',
globalPerspective: 'Global perspective',
unordered: 'No ranking implied',
sectionTitle: 'Committee Members',
sectionDescription: 'Hover over or select a portrait or name to read a short biography.',
orderNote: 'Experts are listed alphabetically by English name, with no ranking implied.',
joinTitle: 'Join the IvorySQL Expert Advisory Committee',
joinDescription:
'We welcome more database experts to help advance the open-source database ecosystem with us.',
joinAction: 'Contact us by email',
hoverHint: 'Hover or select to view profile',
profileLabel: 'Expert profile',
close: 'Close expert profile',
emptyAvatar: 'Portrait coming soon',
},
zh: {
pageTitle: '专家顾问委员会',
pageDescription: '了解为 IvorySQL 产品技术与社区发展提供指导的专家委员。',
eyebrow: 'Community Leadership',
heroTitle: '专家顾问委员会',
heroDescription:
'汇聚全球数据库领域的资深专家与开源领袖,为 IvorySQL 的产品技术、生态建设与社区发展提供长期、专业的战略指导。',
expertCount: '位专家',
globalPerspective: '全球技术视野',
unordered: '排名不分先后',
sectionTitle: '委员会成员',
sectionDescription: '将鼠标移至或选择头像、姓名,即可查看专家简介。',
orderNote: '专家按英文姓名首字母顺序排列,排名不分先后。',
joinTitle: '欢迎加入 IvorySQL 专家顾问委员会',
joinDescription: '欢迎更多数据库专家加入 IvorySQL 专家顾问委员会,与我们共同推动开源数据库生态发展。',
joinAction: '发送邮件联系我们',
hoverHint: '悬停或选择查看简介',
profileLabel: '专家简介',
close: '关闭专家简介',
emptyAvatar: '头像待补充',
},
};

function EmptyAvatar() {
return (
<span className={styles.emptyAvatar} aria-hidden="true">
<svg viewBox="0 0 64 64" role="presentation">
<circle cx="32" cy="24" r="11" />
<path d="M14 54c2-11 8-17 18-17s16 6 18 17" />
</svg>
</span>
);
}

function ExpertCard({ expert, locale, copy, onOpen }) {
const popoverRef = useRef(null);
const [popoverPosition, setPopoverPosition] = useState(null);
const resolvedAvatar = useBaseUrl(expert.avatar || '/');
const name = expert.name[locale];
const title = expert.title[locale];
const biography = expert.bio[locale];
const popoverId = `${expert.id}-bio`;

const openInDialogMode = (event) => {
if (typeof window !== 'undefined' && isDialogMode(window.matchMedia.bind(window))) {
onOpen(expert, event.currentTarget);
}
};

const positionPopover = (event) => {
if (!popoverRef.current) return;

const trigger = event.currentTarget.getBoundingClientRect();
const popover = popoverRef.current.getBoundingClientRect();
setPopoverPosition(
getExpertPopoverPosition(trigger, popover, {
width: window.innerWidth,
height: window.innerHeight,
}),
);
};

return (
<article className={styles.card} data-expert-card="true">
<button
type="button"
className={styles.profileTrigger}
aria-label={name}
aria-describedby={popoverId}
onClick={openInDialogMode}
onFocus={positionPopover}
onMouseEnter={positionPopover}
>
<span
className={styles.avatarFrame}
data-avatar-empty={expert.avatar ? undefined : 'true'}
>
{expert.avatar ? (
<img
src={resolvedAvatar}
alt={name}
width="188"
height="188"
loading="lazy"
style={getAvatarFramingStyle(expert)}
/>
) : (
<EmptyAvatar />
)}
</span>
<span className={styles.name}>{name}</span>
</button>

<span className={styles.title}>{title}</span>

<aside
ref={popoverRef}
id={popoverId}
role="tooltip"
className={styles.bioPopover}
data-side={popoverPosition?.side}
style={
popoverPosition
? { left: popoverPosition.left, top: popoverPosition.top }
: undefined
}
>
<span className={styles.profileLabel}>{copy.profileLabel}</span>
<strong>{name}</strong>
<span className={styles.popoverTitle}>{title}</span>
<p>{biography}</p>
</aside>
</article>
);
}

function MobileBioDialog({ expert, locale, copy, onClose, returnFocus }) {
const dialogRef = useRef(null);
const closeButtonRef = useRef(null);

useEffect(() => {
if (!expert || !dialogRef.current) return undefined;

const dialog = dialogRef.current;
const previousFocus = returnFocus.current || document.activeElement;
const backgroundElements = [...document.querySelectorAll('main, nav, footer')]
.filter((element) => !element.contains(dialog));
const backgroundState = backgroundElements.map((element) => ({
element,
inert: element.getAttribute('inert'),
ariaHidden: element.getAttribute('aria-hidden'),
}));
const previousOverflow = document.body.style.overflow;

backgroundElements.forEach((element) => {
element.setAttribute('inert', '');
element.setAttribute('aria-hidden', 'true');
});
document.body.style.overflow = 'hidden';

const closeOnEscapeOrTrapFocus = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
onClose();
return;
}
if (event.key !== 'Tab') return;

const focusableElements = [...dialog.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
)];
const targetIndex = getFocusTrapTarget(
focusableElements.indexOf(document.activeElement),
focusableElements.length,
event.shiftKey,
);
if (targetIndex === null) return;

event.preventDefault();
focusableElements[targetIndex].focus();
};
const containFocus = (event) => {
if (!dialog.contains(event.target)) closeButtonRef.current?.focus();
};

document.addEventListener('keydown', closeOnEscapeOrTrapFocus);
document.addEventListener('focusin', containFocus);
closeButtonRef.current?.focus();

return () => {
document.removeEventListener('keydown', closeOnEscapeOrTrapFocus);
document.removeEventListener('focusin', containFocus);
backgroundState.forEach(({ element, inert, ariaHidden }) => {
if (inert === null) element.removeAttribute('inert');
else element.setAttribute('inert', inert);
if (ariaHidden === null) element.removeAttribute('aria-hidden');
else element.setAttribute('aria-hidden', ariaHidden);
});
document.body.style.overflow = previousOverflow;
if (previousFocus && document.contains(previousFocus)) previousFocus.focus();
};
}, [expert, onClose, returnFocus]);

if (!expert) return null;

return (
<div className={styles.dialogBackdrop} role="presentation" onClick={onClose}>
<section
ref={dialogRef}
className={styles.mobileDialog}
role="dialog"
aria-modal="true"
aria-labelledby="expert-dialog-title"
onClick={(event) => event.stopPropagation()}
>
<button
ref={closeButtonRef}
type="button"
className={styles.dialogClose}
onClick={onClose}
aria-label={copy.close}
>
×
</button>
<span className={styles.profileLabel}>{copy.profileLabel}</span>
<h2 id="expert-dialog-title">{expert.name[locale]}</h2>
<strong>{expert.title[locale]}</strong>
<p>{expert.bio[locale]}</p>
</section>
</div>
);
}

export default function ExpertAdvisoryCommitteePage() {
const { i18n } = useDocusaurusContext();
const locale = i18n.currentLocale.toLowerCase().startsWith('zh') ? 'zh' : 'en';
const copy = COPY[locale];
const [selectedExpert, setSelectedExpert] = useState(null);
const returnFocusRef = useRef(null);
const openExpert = useCallback((expert, trigger) => {
returnFocusRef.current = trigger;
setSelectedExpert(expert);
}, []);
const closeExpert = useCallback(() => setSelectedExpert(null), []);

return (
<Layout title={copy.pageTitle} description={copy.pageDescription}>
<main className={styles.page}>
<section className={styles.hero}>
<div className="container">
<div className={styles.heroContent}>
<p className={styles.eyebrow}>{copy.eyebrow}</p>
<h1>{copy.heroTitle}</h1>
<p className={styles.heroDescription}>{copy.heroDescription}</p>
<div className={styles.heroMeta}>
<span>{experts.length} {copy.expertCount}</span>
<span>{copy.globalPerspective}</span>
<span>{copy.unordered}</span>
</div>
</div>
</div>
</section>

<section className={styles.membersSection}>
<div className="container">
<div className={styles.sectionHeader}>
<div>
<h2>{copy.sectionTitle}</h2>
<p>
{copy.orderNote} {copy.sectionDescription}
</p>
</div>
<span className={styles.hoverHint}>{copy.hoverHint}</span>
</div>

<div className={styles.grid}>
{experts.map((expert) => (
<ExpertCard
key={expert.id}
expert={expert}
locale={locale}
copy={copy}
onOpen={openExpert}
/>
))}
</div>

<aside className={styles.joinCommittee}>
<div>
<h2>{copy.joinTitle}</h2>
<p>{copy.joinDescription}</p>
</div>
<a className={styles.joinAction} href="mailto:ivorysql1213@gmail.com">
{copy.joinAction}
<span aria-hidden="true">→</span>
</a>
</aside>
</div>
</section>
</main>

<MobileBioDialog
expert={selectedExpert}
locale={locale}
copy={copy}
onClose={closeExpert}
returnFocus={returnFocusRef}
/>
</Layout>
);
}
Loading