Files
BackupX/docs-site/src/components/HomepageCommunity/index.tsx
T
Wu Qing 0e615677db feat(docs): 升级文档站界面与导航 (#124)
重构文档站首页、导航与内容布局,统一 SVG 图标和双语文案,并完善响应式、深色模式与可访问性。
2026-08-09 22:32:27 +08:00

349 lines
14 KiB
TypeScript

import type {ReactNode} from 'react';
import {useEffect, useState} from 'react';
import Heading from '@theme/Heading';
import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import DocIcon, {type DocIconName} from '@site/src/components/DocIcon';
import styles from './styles.module.css';
type Contributor = {
login: string;
avatarUrl?: string;
contributions: number;
type: string;
href: string;
};
type GitHubContributor = {
login: string;
avatar_url?: string;
contributions?: number;
html_url?: string;
type?: string;
};
type CommunityPath = {
icon: DocIconName;
title: ReactNode;
description: ReactNode;
href: string;
};
type SponsorFocus = {
id: string;
icon: DocIconName;
label: ReactNode;
title: ReactNode;
description: ReactNode;
};
type SponsorTier = {
id: string;
name: ReactNode;
audience: ReactNode;
description: ReactNode;
};
const FALLBACK_CONTRIBUTORS: Contributor[] = [
{
login: 'Awuqing',
contributions: 0,
type: 'User',
href: 'https://github.com/Awuqing',
},
{
login: 'dependabot[bot]',
contributions: 0,
type: 'Bot',
href: 'https://github.com/dependabot',
},
];
const COMMUNITY_PATHS: CommunityPath[] = [
{
icon: 'wrench',
title: <Translate id="community.path.issues.title">Report production issues</Translate>,
description: <Translate id="community.path.issues.desc">Share logs, deployment topology, and restore expectations.</Translate>,
href: 'https://github.com/Awuqing/BackupX/issues',
},
{
icon: 'bookOpen',
title: <Translate id="community.path.docs.title">Improve docs and examples</Translate>,
description: <Translate id="community.path.docs.desc">Contribute deployment guides for storage, agents, and databases.</Translate>,
href: '/docs/development/contributing',
},
{
icon: 'github',
title: <Translate id="community.path.code.title">Ship focused pull requests</Translate>,
description: <Translate id="community.path.code.desc">Keep changes tested, reviewable, and aligned with the existing architecture.</Translate>,
href: 'https://github.com/Awuqing/BackupX/pulls',
},
];
const SPONSOR_FOCUS: SponsorFocus[] = [
{
id: 'infrastructure',
icon: 'cloud',
label: <Translate id="community.sponsor.infrastructure.label">Infrastructure</Translate>,
title: <Translate id="community.sponsor.infrastructure.title">Cloud and storage compatibility</Translate>,
description: <Translate id="community.sponsor.infrastructure.desc">Support validation across object storage, WebDAV, SFTP, and regional cloud providers.</Translate>,
},
{
id: 'reliability',
icon: 'shield',
label: <Translate id="community.sponsor.security.label">Reliability</Translate>,
title: <Translate id="community.sponsor.security.title">Security and recovery work</Translate>,
description: <Translate id="community.sponsor.security.desc">Fund encryption reviews, restore drills, release signing, and operational checks.</Translate>,
},
{
id: 'community',
icon: 'users',
label: <Translate id="community.sponsor.community.label">Community</Translate>,
title: <Translate id="community.sponsor.community.title">Documentation and contributor support</Translate>,
description: <Translate id="community.sponsor.community.desc">Improve guides, examples, platform testing, and the contributor experience.</Translate>,
},
];
const SPONSOR_TIERS: SponsorTier[] = [
{
id: 'backer',
name: <Translate id="community.sponsor.tier.backer.name">Backer</Translate>,
audience: <Translate id="community.sponsor.tier.backer.amount">For individuals and small teams</Translate>,
description: <Translate id="community.sponsor.tier.backer.desc">Supports documentation, issue triage, compatibility testing, and focused usability work.</Translate>,
},
{
id: 'partner',
name: <Translate id="community.sponsor.tier.partner.name">Partner</Translate>,
audience: <Translate id="community.sponsor.tier.partner.amount">For storage and infrastructure vendors</Translate>,
description: <Translate id="community.sponsor.tier.partner.desc">Supports provider validation, deployment examples, benchmarks, and integration guides.</Translate>,
},
{
id: 'enterprise',
name: <Translate id="community.sponsor.tier.enterprise.name">Enterprise</Translate>,
audience: <Translate id="community.sponsor.tier.enterprise.amount">For production BackupX operators</Translate>,
description: <Translate id="community.sponsor.tier.enterprise.desc">Funds recovery drills, release hardening, audits, and long-term maintenance.</Translate>,
},
];
function getInitials(login: string): string {
return login
.replace(/\[bot\]$/i, '')
.split(/[-_\s]/)
.filter(Boolean)
.slice(0, 2)
.map(part => part[0]?.toUpperCase())
.join('') || login.slice(0, 2).toUpperCase();
}
function normalizeContributor(contributor: GitHubContributor): Contributor | null {
if (!contributor.login) {
return null;
}
return {
login: contributor.login,
avatarUrl: contributor.avatar_url,
contributions: contributor.contributions ?? 0,
type: contributor.type ?? 'User',
href: contributor.html_url ?? `https://github.com/${contributor.login}`,
};
}
function useGitHubContributors(): Contributor[] {
const [contributors, setContributors] = useState<Contributor[]>(FALLBACK_CONTRIBUTORS);
useEffect(() => {
const controller = new AbortController();
fetch('https://api.github.com/repos/Awuqing/BackupX/contributors?per_page=8', {
signal: controller.signal,
headers: {Accept: 'application/vnd.github+json'},
})
.then(response => {
if (!response.ok) {
throw new Error(`GitHub contributors request failed: ${response.status}`);
}
return response.json() as Promise<GitHubContributor[]>;
})
.then(payload => {
const nextContributors = payload
.map(normalizeContributor)
.filter((contributor): contributor is Contributor => Boolean(contributor));
if (nextContributors.length > 0) {
setContributors(nextContributors);
}
})
.catch(error => {
if (error instanceof Error && error.name !== 'AbortError') {
console.warn(error.message);
}
});
return () => controller.abort();
}, []);
return contributors;
}
function ContributorRow({login, avatarUrl, contributions, type, href}: Contributor): ReactNode {
return (
<Link className={styles.contributorRow} to={href}>
{avatarUrl ? (
<img className={styles.avatarImage} src={avatarUrl} alt="" loading="lazy" />
) : (
<span className={styles.avatar} aria-hidden="true">{getInitials(login)}</span>
)}
<span className={styles.contributorBody}>
<span className={styles.contributorName}>{login}</span>
<span className={styles.contributorRole}>
{type === 'Bot' ? (
<Translate id="community.contributor.botRole">Automation contributor</Translate>
) : (
<Translate id="community.contributor.githubRole">GitHub contributor</Translate>
)}
</span>
</span>
<span className={styles.contributionCount}>
<Translate id="community.contributor.contributions" values={{count: contributions}}>
{'{count} contributions'}
</Translate>
</span>
</Link>
);
}
export function HomepageSponsors(): ReactNode {
return (
<div className={styles.sponsorProgram}>
<div className={styles.sponsorProgramHeader}>
<span className={styles.sponsorProgramIcon}><DocIcon name="heart" size={23} /></span>
<div className={styles.sponsorProgramCopy}>
<Heading as="h2" className={styles.sponsorProgramTitle}>
<Translate id="community.sponsor.title">Support reliable backup infrastructure</Translate>
</Heading>
<p>
<Translate id="community.sponsor.programDesc">
Sponsorship is directed toward test coverage, restore confidence, provider compatibility, and documentation that operators can apply directly.
</Translate>
</p>
</div>
<Link className={styles.sponsorAction} to="https://github.com/sponsors/Awuqing">
<DocIcon name="github" size={17} />
<Translate id="community.sponsor.cta">Sponsor BackupX</Translate>
<DocIcon name="external" size={15} />
</Link>
</div>
<div className={styles.sponsorFocusList}>
{SPONSOR_FOCUS.map(focus => (
<div key={focus.id} className={styles.sponsorFocusItem}>
<span className={styles.sponsorFocusIcon}><DocIcon name={focus.icon} size={20} /></span>
<span className={styles.sponsorFocusBody}>
<span className={styles.sponsorFocusLabel}>{focus.label}</span>
<span className={styles.sponsorFocusTitle}>{focus.title}</span>
<span className={styles.sponsorFocusDescription}>{focus.description}</span>
</span>
</div>
))}
</div>
<div className={styles.tierSection}>
<div className={styles.tierSectionHeader}>
<Heading as="h3"><Translate id="community.sponsor.tier.title">Ways to support</Translate></Heading>
<p><Translate id="community.sponsor.tier.subtitle">Choose a level that matches how your team depends on BackupX.</Translate></p>
</div>
<div className={styles.tierGrid}>
{SPONSOR_TIERS.map(tier => (
<div key={tier.id} className={styles.tierItem}>
<span className={styles.tierName}>{tier.name}</span>
<span className={styles.tierAudience}>{tier.audience}</span>
<span className={styles.tierDescription}>{tier.description}</span>
</div>
))}
</div>
</div>
</div>
);
}
export default function HomepageCommunity(): ReactNode {
const contributors = useGitHubContributors();
return (
<section id="community" className={styles.section}>
<div className="container">
<div className={styles.sectionHead}>
<div>
<div className={styles.sectionTag}>
<Translate id="community.tag">Community</Translate>
</div>
<Heading as="h2" className={styles.sectionTitle}>
<Translate id="community.title">Operational knowledge improves in the open</Translate>
</Heading>
</div>
<p className={styles.sectionSubtitle}>
<Translate id="community.subtitle">
Report real deployment constraints, improve the runbooks, or contribute a focused change with reproducible validation.
</Translate>
</p>
</div>
<div className={styles.communityShell}>
<div className={styles.pathPanel}>
<div className={styles.panelHeader}>
<span><Translate id="community.path.kicker">Contribution paths</Translate></span>
<Link to="/docs/development/contributing">
<Translate id="community.path.guide">Contribution guide</Translate>
<DocIcon name="arrowRight" size={15} />
</Link>
</div>
<div className={styles.pathList}>
{COMMUNITY_PATHS.map((path, index) => (
<Link key={path.href} className={styles.pathItem} to={path.href}>
<span className={styles.pathIndex}>{String(index + 1).padStart(2, '0')}</span>
<span className={styles.pathIcon}><DocIcon name={path.icon} size={19} /></span>
<span className={styles.pathBody}>
<span className={styles.pathTitle}>{path.title}</span>
<span className={styles.pathDescription}>{path.description}</span>
</span>
<DocIcon name="arrowRight" size={17} className={styles.rowArrow} />
</Link>
))}
</div>
</div>
<div className={styles.contributorPanel}>
<div className={styles.panelHeader}>
<span><Translate id="community.contributor.kicker">Contributors</Translate></span>
<Link to="https://github.com/Awuqing/BackupX/graphs/contributors">
<Translate id="community.contributor.all">View all</Translate>
<DocIcon name="external" size={14} />
</Link>
</div>
<p className={styles.panelNote}>
<Translate id="community.contributor.source">Loaded from the GitHub contributors API with a local fallback.</Translate>
</p>
<div className={styles.contributorList}>
{contributors.slice(0, 5).map(contributor => (
<ContributorRow key={contributor.login} {...contributor} />
))}
</div>
</div>
<div className={styles.sponsorBand}>
<span className={styles.sponsorBandIcon}><DocIcon name="heart" size={20} /></span>
<span className={styles.sponsorBandBody}>
<span className={styles.sponsorBandTitle}><Translate id="community.sponsor.bandTitle">Support long-term maintenance</Translate></span>
<span className={styles.sponsorBandDescription}><Translate id="community.sponsor.bandDesc">Fund compatibility testing, recovery work, and operator-focused documentation.</Translate></span>
</span>
<Link to="/sponsors">
<Translate id="community.sponsor.learnMore">Sponsorship details</Translate>
<DocIcon name="arrowRight" size={16} />
</Link>
</div>
</div>
</div>
</section>
);
}