From 2997e971a623b4a5b95954ff31f9f20ab303b4d0 Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Sat, 25 Apr 2026 18:50:54 +0800 Subject: [PATCH] docs: add community and sponsors pages with dynamic GitHub contributor integration --- docs-site/docusaurus.config.ts | 28 +- docs-site/i18n/zh-CN/code.json | 92 +++- .../docusaurus-theme-classic/footer.json | 10 +- .../docusaurus-theme-classic/navbar.json | 8 + .../components/HomepageCommunity/index.tsx | 329 ++++++++++++ .../HomepageCommunity/styles.module.css | 429 ++++++++++++++++ .../src/components/HomepageFeatures/index.tsx | 2 +- .../HomepageFeatures/styles.module.css | 51 +- .../src/components/HomepageShowcase/index.tsx | 2 +- .../HomepageShowcase/styles.module.css | 109 ++-- docs-site/src/css/custom.css | 54 +- docs-site/src/pages/community.tsx | 19 + docs-site/src/pages/index.module.css | 469 +++++++++++++----- docs-site/src/pages/index.tsx | 119 +++-- docs-site/src/pages/sponsors.tsx | 39 ++ 15 files changed, 1526 insertions(+), 234 deletions(-) create mode 100644 docs-site/src/components/HomepageCommunity/index.tsx create mode 100644 docs-site/src/components/HomepageCommunity/styles.module.css create mode 100644 docs-site/src/pages/community.tsx create mode 100644 docs-site/src/pages/sponsors.tsx diff --git a/docs-site/docusaurus.config.ts b/docs-site/docusaurus.config.ts index acbda64..2f0d964 100644 --- a/docs-site/docusaurus.config.ts +++ b/docs-site/docusaurus.config.ts @@ -6,7 +6,7 @@ import type * as Preset from '@docusaurus/preset-classic'; // https://awuqing.github.io/BackupX/ const config: Config = { title: 'BackupX', - tagline: 'Self-hosted server backup management — one binary, one command', + tagline: 'Self-hosted backup orchestration for servers, databases, storage targets and remote agents', favicon: 'img/favicon.ico', future: { @@ -76,6 +76,16 @@ const config: Config = { label: 'Downloads', position: 'left', }, + { + to: '/community', + label: 'Community', + position: 'left', + }, + { + to: '/sponsors', + label: 'Sponsors', + position: 'left', + }, { type: 'localeDropdown', position: 'right', @@ -115,6 +125,22 @@ const config: Config = { {label: 'Issues', href: 'https://github.com/Awuqing/BackupX/issues'}, ], }, + { + title: 'Community', + items: [ + {label: 'Contributors', href: 'https://github.com/Awuqing/BackupX/graphs/contributors'}, + {label: 'Pull Requests', href: 'https://github.com/Awuqing/BackupX/pulls'}, + {label: 'Sponsor', to: '/sponsors'}, + ], + }, + { + title: 'Sponsors', + items: [ + {label: 'Sponsor BackupX', href: 'https://github.com/sponsors/Awuqing'}, + {label: 'Partnership', href: 'https://github.com/Awuqing/BackupX/issues/new/choose'}, + {label: 'Sponsor tiers', to: '/sponsors'}, + ], + }, ], copyright: `Copyright © ${new Date().getFullYear()} BackupX · Apache License 2.0`, }, diff --git a/docs-site/i18n/zh-CN/code.json b/docs-site/i18n/zh-CN/code.json index 57998b0..e95c02f 100644 --- a/docs-site/i18n/zh-CN/code.json +++ b/docs-site/i18n/zh-CN/code.json @@ -1,22 +1,22 @@ { "home.badge": { - "message": "开源 · v1.6.0", + "message": "开源备份控制平面 · v2.2.1", "description": "Version badge on the hero" }, "home.title.part1": { - "message": "为每一台服务器提供", + "message": "面向自托管服务器的", "description": "Hero title, first line" }, "home.title.part2": { - "message": "自托管备份管理。", + "message": "备份编排平台。", "description": "Hero title accent second line" }, "home.tagline": { - "message": "一个二进制,一条命令。文件 / 数据库 / SAP HANA 备份直送 70+ 存储后端。", + "message": "在一个清爽控制台中管理文件、数据库、SAP HANA 和远程节点备份。控制平面自己掌握,存储后端灵活选择。", "description": "Tagline on the home page" }, "home.pageTitle": { - "message": "自托管备份管理", + "message": "面向自托管服务器的备份编排", "description": "Page element on the home page" }, "home.getStarted": { @@ -28,13 +28,26 @@ "description": "Hero metric label: storage backends" }, "home.metric.backupTypes": { - "message": "备份类型", + "message": "远程执行", "description": "Hero metric label: backup types" }, "home.metric.license": { "message": "开源协议", "description": "Hero metric label: license" }, + "home.visual.eyebrow": {"message": "BackupX 控制台"}, + "home.visual.title": {"message": "运维概览"}, + "home.visual.status": {"message": "健康"}, + "home.visual.success": {"message": "成功率"}, + "home.visual.nodes": {"message": "活跃节点"}, + "home.visual.targets": {"message": "存储目标"}, + "home.visual.row1.title": {"message": "PostgreSQL 夜间备份"}, + "home.visual.row1.desc": {"message": "加密归档已上传至 S3"}, + "home.visual.row2.title": {"message": "SAP HANA 快照"}, + "home.visual.row2.desc": {"message": "正在 agent-shanghai-02 上运行"}, + "home.visual.row3.title": {"message": "保留策略清理"}, + "home.visual.row3.desc": {"message": "下一次执行在 4 小时后"}, + "home.command.title": {"message": "使用 Docker 启动"}, "section.features.tag": { "message": "核心能力", @@ -78,5 +91,70 @@ "showcase.storage.desc": {"message": "阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV — 加上每一种 rclone 后端。测试连接、收藏、查看实时容量。"}, "showcase.nodes.title": {"message": "几分钟搭起 Master-Agent"}, "showcase.nodes.desc": {"message": "创建节点、复制令牌、在任意远程主机启动 Agent。路由到节点的任务在本地执行并直接上传到存储 — 无需反向连通性。"}, - "showcase.cta": {"message": "开始阅读文档"} + "showcase.cta": {"message": "开始阅读文档"}, + + "community.tag": {"message": "社区"}, + "community.pageTitle": {"message": "社区、赞助商与贡献者"}, + "community.pageDescription": {"message": "赞助 BackupX,了解贡献者,并找到务实的参与方式。"}, + "community.title": {"message": "开放协作,面向长期运维"}, + "community.subtitle": {"message": "备份软件的信任来自透明发布、真实部署反馈,以及足够务实的贡献路径。"}, + "community.sponsor.kicker": {"message": "赞助商"}, + "community.sponsor.wallTitle": {"message": "赞助商"}, + "community.sponsor.title": {"message": "支持你依赖的备份基础设施"}, + "community.sponsor.cta": {"message": "赞助 BackupX"}, + "community.sponsor.openSlot": {"message": "赞助席位开放"}, + "community.sponsor.logo.project": {"message": "项目赞助"}, + "community.sponsor.logo.cloud": {"message": "云服务伙伴"}, + "community.sponsor.logo.object": {"message": "对象存储"}, + "community.sponsor.logo.cdn": {"message": "CDN 伙伴"}, + "community.sponsor.logo.database": {"message": "数据库伙伴"}, + "community.sponsor.logo.security": {"message": "安全审计"}, + "community.sponsor.logo.agent": {"message": "远程节点实验室"}, + "community.sponsor.logo.docs": {"message": "文档赞助"}, + "community.sponsor.logo.release": {"message": "发布赞助"}, + "community.sponsor.logo.s3": {"message": "S3 兼容"}, + "community.sponsor.logo.webdav": {"message": "WebDAV 伙伴"}, + "community.sponsor.logo.sftp": {"message": "SFTP 伙伴"}, + "community.sponsor.logo.docker": {"message": "容器伙伴"}, + "community.sponsor.logo.mirror": {"message": "镜像伙伴"}, + "community.sponsor.logo.restore": {"message": "恢复演练"}, + "community.sponsor.logo.qa": {"message": "测试实验室"}, + "community.sponsor.logo.oss": {"message": "开源支持"}, + "community.sponsor.logo.open": {"message": "赞助席位开放"}, + "community.sponsor.infrastructure.label": {"message": "基础设施"}, + "community.sponsor.infrastructure.title": {"message": "云与存储生态伙伴"}, + "community.sponsor.infrastructure.desc": {"message": "帮助 BackupX 覆盖对象存储、WebDAV、SFTP 以及区域云平台的真实验证。"}, + "community.sponsor.security.label": {"message": "安全"}, + "community.sponsor.security.title": {"message": "审计与可靠性支持者"}, + "community.sponsor.security.desc": {"message": "支持加密、恢复演练、发布签名和运维检查等强化工作。"}, + "community.sponsor.community.label": {"message": "社区"}, + "community.sponsor.community.title": {"message": "开源支持者"}, + "community.sponsor.community.desc": {"message": "支持文档、示例、平台测试和贡献者引导。"}, + "community.sponsor.tier.backer.name": {"message": "Backer"}, + "community.sponsor.tier.backer.amount": {"message": "适合个人与小团队"}, + "community.sponsor.tier.backer.desc": {"message": "支持文档、Issue 分流、兼容性测试和小型体验改进。"}, + "community.sponsor.tier.partner.name": {"message": "Partner"}, + "community.sponsor.tier.partner.amount": {"message": "适合存储与基础设施厂商"}, + "community.sponsor.tier.partner.desc": {"message": "支持 Provider 验证、部署示例、基准说明和集成指南。"}, + "community.sponsor.tier.enterprise.name": {"message": "Enterprise"}, + "community.sponsor.tier.enterprise.amount": {"message": "适合生产环境使用方"}, + "community.sponsor.tier.enterprise.desc": {"message": "赞助恢复演练、发布加固、审计和长期维护等可靠性工作。"}, + "community.contributor.kicker": {"message": "贡献者"}, + "community.contributor.all": {"message": "查看全部"}, + "community.contributor.source": {"message": "浏览器端通过 GitHub contributors API 获取。"}, + "community.contributor.botRole": {"message": "自动化贡献者"}, + "community.contributor.githubRole": {"message": "GitHub 贡献者"}, + "community.contributor.contributions": {"message": "{count} 次贡献"}, + "community.path.kicker": {"message": "贡献路径"}, + "community.path.issues.title": {"message": "反馈生产问题"}, + "community.path.issues.desc": {"message": "提交日志、部署拓扑和恢复预期。"}, + "community.path.docs.title": {"message": "完善文档与示例"}, + "community.path.docs.desc": {"message": "贡献存储、Agent 和数据库部署指南。"}, + "community.path.code.title": {"message": "提交聚焦的 PR"}, + "community.path.code.desc": {"message": "保持改动小而可测,并贴合现有架构。"}, + "sponsors.pageTitle": {"message": "赞助商"}, + "sponsors.pageDescription": {"message": "赞助 BackupX 的可靠性、文档、存储兼容性和长期维护。"}, + "sponsors.tag": {"message": "赞助商"}, + "sponsors.title": {"message": "赞助 BackupX 生态"}, + "sponsors.subtitle": {"message": "赞助帮助 BackupX 更贴近真实运维:经过验证的存储 Provider、可靠发布、恢复信心和更完善的文档。"} } diff --git a/docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json b/docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json index 07576ba..1009cfb 100644 --- a/docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json +++ b/docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json @@ -2,6 +2,8 @@ "link.title.Docs": {"message": "文档"}, "link.title.Features": {"message": "功能"}, "link.title.More": {"message": "更多"}, + "link.title.Community": {"message": "社区"}, + "link.title.Sponsors": {"message": "赞助商"}, "link.item.label.Introduction": {"message": "简介"}, "link.item.label.Quick Start": {"message": "快速开始"}, "link.item.label.Installation": {"message": "安装"}, @@ -11,5 +13,11 @@ "link.item.label.GitHub": {"message": "GitHub"}, "link.item.label.Releases": {"message": "Releases"}, "link.item.label.Docker Hub": {"message": "Docker Hub"}, - "link.item.label.Issues": {"message": "Issues"} + "link.item.label.Issues": {"message": "Issues"}, + "link.item.label.Contributors": {"message": "贡献者"}, + "link.item.label.Pull Requests": {"message": "Pull Requests"}, + "link.item.label.Sponsor": {"message": "赞助"}, + "link.item.label.Sponsor BackupX": {"message": "赞助 BackupX"}, + "link.item.label.Partnership": {"message": "合作伙伴"}, + "link.item.label.Sponsor tiers": {"message": "赞助层级"} } diff --git a/docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json b/docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json index 78dbace..96c2044 100644 --- a/docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json +++ b/docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json @@ -7,6 +7,14 @@ "message": "下载", "description": "Navbar item: Downloads" }, + "item.label.Community": { + "message": "社区", + "description": "Navbar item: Community" + }, + "item.label.Sponsors": { + "message": "赞助商", + "description": "Navbar item: Sponsors" + }, "item.label.GitHub": { "message": "GitHub", "description": "Navbar item: GitHub" diff --git a/docs-site/src/components/HomepageCommunity/index.tsx b/docs-site/src/components/HomepageCommunity/index.tsx new file mode 100644 index 0000000..b363eee --- /dev/null +++ b/docs-site/src/components/HomepageCommunity/index.tsx @@ -0,0 +1,329 @@ +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 styles from './styles.module.css'; + +type SponsorSlot = { + brand: ReactNode; + name: ReactNode; + href?: string; +}; + +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 = { + title: ReactNode; + description: ReactNode; + href: string; +}; + +const SPONSOR_SLOTS: SponsorSlot[] = [ + { + brand: 'BackupX', + name: <Translate id="community.sponsor.logo.project">Project backer</Translate>, + href: 'https://github.com/sponsors/Awuqing', + }, + { + brand: 'Cloud', + name: <Translate id="community.sponsor.logo.cloud">Cloud partner</Translate>, + }, + { + brand: 'Object', + name: <Translate id="community.sponsor.logo.object">Object storage</Translate>, + }, + { + brand: 'CDN', + name: <Translate id="community.sponsor.logo.cdn">CDN partner</Translate>, + }, + { + brand: 'DB', + name: <Translate id="community.sponsor.logo.database">Database partner</Translate>, + }, + { + brand: 'Security', + name: <Translate id="community.sponsor.logo.security">Security audit</Translate>, + }, + { + brand: 'Agent', + name: <Translate id="community.sponsor.logo.agent">Remote node lab</Translate>, + }, + { + brand: 'Docs', + name: <Translate id="community.sponsor.logo.docs">Docs sponsor</Translate>, + }, + { + brand: 'Release', + name: <Translate id="community.sponsor.logo.release">Release sponsor</Translate>, + }, + { + brand: 'S3', + name: <Translate id="community.sponsor.logo.s3">S3 compatible</Translate>, + }, + { + brand: 'WebDAV', + name: <Translate id="community.sponsor.logo.webdav">WebDAV partner</Translate>, + }, + { + brand: 'SFTP', + name: <Translate id="community.sponsor.logo.sftp">SFTP partner</Translate>, + }, + { + brand: 'Docker', + name: <Translate id="community.sponsor.logo.docker">Container partner</Translate>, + }, + { + brand: 'Mirror', + name: <Translate id="community.sponsor.logo.mirror">Mirror partner</Translate>, + }, + { + brand: 'Restore', + name: <Translate id="community.sponsor.logo.restore">Restore drill</Translate>, + }, + { + brand: 'QA', + name: <Translate id="community.sponsor.logo.qa">Test lab</Translate>, + }, + { + brand: 'OSS', + name: <Translate id="community.sponsor.logo.oss">Open source</Translate>, + }, + { + brand: 'Open Slot', + name: <Translate id="community.sponsor.logo.open">Sponsor slot open</Translate>, + }, +]; + +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[] = [ + { + 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', + }, + { + 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', + }, + { + title: <Translate id="community.path.code.title">Ship focused PRs</Translate>, + description: <Translate id="community.path.code.desc">Keep changes small, tested and aligned with the existing architecture.</Translate>, + href: 'https://github.com/Awuqing/BackupX/pulls', + }, +]; + +function SponsorLogoCard({brand, name, href}: SponsorSlot) { + return ( + <Link className={styles.sponsorLogoTile} to={href ?? 'https://github.com/sponsors/Awuqing'}> + <span className={styles.sponsorLogoMark}>{brand}</span> + <span className={styles.sponsorLogoName}>{name}</span> + </Link> + ); +} + +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=12', { + 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 ContributorCard({login, avatarUrl, contributions, type, href}: Contributor) { + return ( + <Link className={styles.contributorCard} 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}> + <strong>{login}</strong> + <span> + {type === 'Bot' ? ( + <Translate id="community.contributor.botRole">Automation contributor</Translate> + ) : ( + <Translate id="community.contributor.githubRole">GitHub contributor</Translate> + )} + </span> + <em> + <Translate id="community.contributor.contributions" values={{count: contributions}}> + {'{count} contributions'} + </Translate> + </em> + </span> + </Link> + ); +} + +export function HomepageSponsors(): ReactNode { + return ( + <div className={styles.sponsorWall}> + <div className={styles.sponsorWallHeader}> + <Heading as="h3" className={styles.sponsorWallTitle}> + <Translate id="community.sponsor.wallTitle">Sponsors</Translate> + </Heading> + <Link className={styles.sponsorWallAction} to="https://github.com/sponsors/Awuqing"> + <Translate id="community.sponsor.cta">Sponsor BackupX</Translate> + <span aria-hidden="true">-></span> + </Link> + </div> + + <div className={styles.sponsorLogoGrid}> + {SPONSOR_SLOTS.map((slot, index) => ( + <SponsorLogoCard key={index} {...slot} /> + ))} + </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 className={styles.sectionTag}> + <Translate id="community.tag">COMMUNITY</Translate> + </div> + <Heading as="h2" className={styles.sectionTitle}> + <Translate id="community.title">Built in the open, ready for long-term operators</Translate> + </Heading> + <p className={styles.sectionSubtitle}> + <Translate id="community.subtitle"> + Backup software earns trust through transparent releases, real deployment feedback and a contributor path that stays practical. + </Translate> + </p> + </div> + + <HomepageSponsors /> + + <div className={styles.communityGrid}> + <div className={styles.panel}> + <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> + </Link> + </div> + <div className={styles.panelNote}> + <Translate id="community.contributor.source">Loaded from GitHub contributors API in the browser.</Translate> + </div> + <div className={styles.contributorList}> + {contributors.map(contributor => ( + <ContributorCard key={contributor.login} {...contributor} /> + ))} + </div> + </div> + + <div className={styles.panel}> + <div className={styles.panelHeader}> + <span> + <Translate id="community.path.kicker">Contributor paths</Translate> + </span> + </div> + <div className={styles.pathList}> + {COMMUNITY_PATHS.map((path, index) => ( + <Link key={index} className={styles.pathItem} to={path.href}> + <span className={styles.pathIndex}>{String(index + 1).padStart(2, '0')}</span> + <span> + <strong>{path.title}</strong> + <em>{path.description}</em> + </span> + </Link> + ))} + </div> + </div> + </div> + </div> + </section> + ); +} diff --git a/docs-site/src/components/HomepageCommunity/styles.module.css b/docs-site/src/components/HomepageCommunity/styles.module.css new file mode 100644 index 0000000..3753a33 --- /dev/null +++ b/docs-site/src/components/HomepageCommunity/styles.module.css @@ -0,0 +1,429 @@ +.section { + padding: 5.5rem 0 6rem; + background: + linear-gradient(180deg, rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0.86) 100%), + var(--ifm-background-color); +} + +[data-theme='dark'] .section { + background: + linear-gradient(180deg, rgba(15, 17, 21, 0) 0%, rgba(255, 255, 255, 0.03) 100%), + var(--ifm-background-color); +} + +.sectionHead { + max-width: 760px; + margin: 0 auto 2.5rem; + text-align: center; +} + +.sectionTag { + display: inline-flex; + align-items: center; + min-height: 28px; + margin-bottom: 1rem; + padding: 4px 10px; + color: #00a870; + background: rgba(0, 180, 42, 0.1); + border: 1px solid rgba(0, 180, 42, 0.18); + border-radius: 8px; + font-size: 12px; + font-weight: 750; + letter-spacing: 0; +} + +.sectionTitle { + margin: 0 0 1rem; + color: var(--ifm-heading-color); + font-size: 2.35rem; + font-weight: 750; + letter-spacing: 0; + line-height: 1.2; +} + +.sectionSubtitle { + margin: 0; + color: var(--ifm-color-content-secondary); + font-size: 1.04rem; + line-height: 1.7; +} + +.sponsorWall { + overflow: hidden; + margin-bottom: 1rem; + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + box-shadow: 0 12px 28px rgba(29, 33, 41, 0.06); +} + +[data-theme='dark'] .sponsorWall { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +.sponsorWallHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 60px; + padding: 0 1.25rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .sponsorWallHeader { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.sponsorWallTitle { + position: relative; + margin: 0; + padding-left: 14px; + color: var(--ifm-heading-color); + font-size: 1.05rem; + font-weight: 750; + letter-spacing: 0; +} + +.sponsorWallTitle::before { + position: absolute; + top: 50%; + left: 0; + width: 3px; + height: 18px; + content: ""; + background: #52c41a; + border-radius: 3px; + transform: translateY(-50%); +} + +.sponsorWallAction { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 36px; + padding: 0 12px; + color: #52c41a; + background: rgba(82, 196, 26, 0.08); + border: 1px solid rgba(82, 196, 26, 0.2); + border-radius: 8px; + font-size: 13px; + font-weight: 700; + text-decoration: none !important; + white-space: nowrap; + transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; +} + +.sponsorWallAction:hover, +.sponsorWallAction:focus-visible { + color: #389e0d; + background: rgba(82, 196, 26, 0.14); + border-color: #52c41a; + transform: translateY(-1px); +} + +.sponsorLogoGrid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + background: var(--ifm-color-emphasis-200); + gap: 1px; + padding: 1px; +} + +[data-theme='dark'] .sponsorLogoGrid { + background: rgba(255, 255, 255, 0.08); +} + +.sponsorLogoTile { + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + min-height: 106px; + padding: 14px 10px; + flex-direction: column; + color: inherit; + background: var(--ifm-background-color); + text-align: center; + text-decoration: none !important; + transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +[data-theme='dark'] .sponsorLogoTile { + background: rgba(15, 17, 21, 0.78); +} + +.sponsorLogoTile:hover, +.sponsorLogoTile:focus-visible { + z-index: 1; + color: inherit; + background: rgba(82, 196, 26, 0.04); + box-shadow: inset 0 0 0 1px rgba(82, 196, 26, 0.5); + transform: translateY(-1px); +} + +.sponsorLogoMark { + display: block; + max-width: 100%; + overflow-wrap: anywhere; + color: var(--ifm-color-primary); + font-size: 1.45rem; + font-weight: 850; + letter-spacing: 0; + line-height: 1.1; +} + +.sponsorLogoTile:nth-child(2n) .sponsorLogoMark { + color: #ff7d00; +} + +.sponsorLogoTile:nth-child(3n) .sponsorLogoMark { + color: #14c9c9; +} + +.sponsorLogoTile:nth-child(4n) .sponsorLogoMark { + color: #722ed1; +} + +.sponsorLogoTile:nth-child(5n) .sponsorLogoMark { + color: #52c41a; +} + +.sponsorLogoName { + display: block; + max-width: 100%; + margin-top: 10px; + color: var(--ifm-color-content-secondary); + font-size: 0.86rem; + font-weight: 600; + line-height: 1.35; +} + +.panel { + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + box-shadow: 0 12px 28px rgba(29, 33, 41, 0.06); +} + +[data-theme='dark'] .panel { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +.communityGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.panel { + min-width: 0; + padding: 1.25rem; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + color: var(--ifm-color-content-secondary); + font-size: 12px; + font-weight: 750; + letter-spacing: 0; + text-transform: uppercase; +} + +.panelHeader a { + color: var(--ifm-color-primary); + text-decoration: none !important; +} + +.panelNote { + margin: -0.35rem 0 1rem; + color: var(--ifm-color-content-secondary); + font-size: 0.82rem; + line-height: 1.5; +} + +.contributorList, +.pathList { + display: grid; + gap: 10px; +} + +.contributorCard, +.pathItem { + display: grid; + min-width: 0; + color: inherit; + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + text-decoration: none !important; + transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease; +} + +.contributorCard:hover, +.contributorCard:focus-visible, +.pathItem:hover, +.pathItem:focus-visible { + color: inherit; + background: var(--ifm-background-color); + border-color: var(--ifm-color-primary); + transform: translateY(-1px); +} + +[data-theme='dark'] .contributorCard, +[data-theme='dark'] .pathItem { + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.08); +} + +.contributorCard { + grid-template-columns: auto minmax(0, 1fr); + gap: 12px; + align-items: center; + padding: 12px; +} + +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + color: #fff; + background: #165dff; + border-radius: 8px; + font-size: 13px; + font-weight: 800; +} + +.avatarImage { + width: 44px; + height: 44px; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + object-fit: cover; +} + +.contributorCard:nth-child(2) .avatar { + background: #00a870; +} + +.contributorCard:nth-child(3) .avatar { + background: #ff7d00; +} + +.contributorBody { + display: grid; + min-width: 0; + gap: 2px; +} + +.contributorBody strong { + color: var(--ifm-heading-color); + font-size: 0.98rem; +} + +.contributorBody span { + color: var(--ifm-color-content); + font-size: 0.88rem; +} + +.contributorBody em, +.pathItem em { + color: var(--ifm-color-content-secondary); + font-size: 0.82rem; + font-style: normal; + line-height: 1.45; +} + +.pathItem { + grid-template-columns: auto minmax(0, 1fr); + gap: 12px; + padding: 14px; +} + +.pathIndex { + color: var(--ifm-color-primary); + font-family: var(--ifm-font-family-monospace); + font-size: 0.86rem; + font-weight: 800; +} + +.pathItem strong { + display: block; + margin-bottom: 4px; + color: var(--ifm-heading-color); + font-size: 0.96rem; +} + +@media (max-width: 996px) { + .section { + padding: 4rem 0; + } + + .sectionTitle { + font-size: 2rem; + } + + .communityGrid { + grid-template-columns: 1fr; + } + + .sponsorLogoGrid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .sponsorLogoTile { + min-height: 96px; + } +} + +@media (max-width: 640px) { + .section { + padding: 3.25rem 0; + } + + .sectionTitle { + font-size: 1.75rem; + } + + .sponsorWallHeader { + display: grid; + min-height: auto; + padding: 1rem; + } + + .sponsorWallAction { + justify-content: center; + width: 100%; + } + + .sponsorLogoGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sponsorLogoMark { + font-size: 1.15rem; + } + + .panel { + padding: 1rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .sponsorWallAction, + .sponsorLogoTile, + .contributorCard, + .pathItem { + transition: none; + } +} diff --git a/docs-site/src/components/HomepageFeatures/index.tsx b/docs-site/src/components/HomepageFeatures/index.tsx index 41010c2..d0a4202 100644 --- a/docs-site/src/components/HomepageFeatures/index.tsx +++ b/docs-site/src/components/HomepageFeatures/index.tsx @@ -129,7 +129,7 @@ function Feature({title, description, icon, link}: FeatureItem) { {link && ( <span className={styles.featureLink}> <Translate id="feat.learnMore">Learn more</Translate> - <span className={styles.featureArrow} aria-hidden="true">→</span> + <span className={styles.featureArrow} aria-hidden="true">-></span> </span> )} </> diff --git a/docs-site/src/components/HomepageFeatures/styles.module.css b/docs-site/src/components/HomepageFeatures/styles.module.css index cb50d11..9659f03 100644 --- a/docs-site/src/components/HomepageFeatures/styles.module.css +++ b/docs-site/src/components/HomepageFeatures/styles.module.css @@ -1,5 +1,6 @@ .section { - padding: 6rem 0 4rem; + padding: 5.5rem 0 4.25rem; + background: var(--ifm-background-color); } .sectionHead { @@ -9,14 +10,17 @@ } .sectionTag { - display: inline-block; + display: inline-flex; + align-items: center; + min-height: 28px; font-size: 12px; - font-weight: 600; - letter-spacing: 0.15em; + font-weight: 750; + letter-spacing: 0; color: var(--ifm-color-primary); padding: 4px 12px; background: rgba(22, 93, 255, 0.08); - border-radius: 4px; + border: 1px solid rgba(22, 93, 255, 0.16); + border-radius: 8px; margin-bottom: 1rem; } @@ -26,10 +30,10 @@ } .sectionTitle { - font-size: clamp(1.8rem, 3vw, 2.5rem); + font-size: 2.35rem; line-height: 1.2; - letter-spacing: -0.02em; - font-weight: 700; + letter-spacing: 0; + font-weight: 750; margin: 0 0 1rem; color: var(--ifm-heading-color); } @@ -51,6 +55,9 @@ .section { padding: 3.5rem 0 2rem; } + .sectionTitle { + font-size: 2rem; + } .grid { grid-template-columns: 1fr; } @@ -70,7 +77,7 @@ padding: 1.75rem; background: var(--ifm-background-color); border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 12px; + border-radius: 8px; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; text-decoration: none !important; color: inherit; @@ -78,7 +85,7 @@ } .featureCardLink:hover { - transform: translateY(-3px); + transform: translateY(-2px); border-color: var(--ifm-color-primary); box-shadow: 0 12px 30px -8px rgba(22, 93, 255, 0.18); color: inherit; @@ -99,26 +106,26 @@ .iconWrap { width: 48px; height: 48px; - border-radius: 10px; + border-radius: 8px; display: flex; align-items: center; justify-content: center; - background: linear-gradient(135deg, rgba(22, 93, 255, 0.1) 0%, rgba(143, 75, 255, 0.08) 100%); + background: linear-gradient(135deg, rgba(22, 93, 255, 0.1) 0%, rgba(20, 201, 201, 0.12) 100%); color: var(--ifm-color-primary); margin-bottom: 1.25rem; } [data-theme='dark'] .iconWrap { - background: linear-gradient(135deg, rgba(96, 126, 255, 0.15) 0%, rgba(143, 75, 255, 0.12) 100%); + background: linear-gradient(135deg, rgba(96, 126, 255, 0.15) 0%, rgba(20, 201, 201, 0.12) 100%); color: var(--ifm-color-primary-lighter); } .featureTitle { font-size: 1.15rem; - font-weight: 600; + font-weight: 700; margin: 0 0 0.6rem; color: var(--ifm-heading-color); - letter-spacing: -0.01em; + letter-spacing: 0; } .featureDesc { @@ -146,3 +153,17 @@ .featureCardLink:hover .featureArrow { transform: translateX(4px); } + +@media (max-width: 640px) { + .sectionTitle { + font-size: 1.75rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .featureCard, + .featureCardLink, + .featureArrow { + transition: none; + } +} diff --git a/docs-site/src/components/HomepageShowcase/index.tsx b/docs-site/src/components/HomepageShowcase/index.tsx index 4a869ca..44378ab 100644 --- a/docs-site/src/components/HomepageShowcase/index.tsx +++ b/docs-site/src/components/HomepageShowcase/index.tsx @@ -110,7 +110,7 @@ export default function HomepageShowcase(): ReactNode { <p className={styles.captionDesc}>{current.description}</p> <Link to="/docs/getting-started/quick-start" className={styles.captionLink}> <Translate id="showcase.cta">Explore the docs</Translate> - <span aria-hidden="true"> →</span> + <span aria-hidden="true"> -></span> </Link> </div> </div> diff --git a/docs-site/src/components/HomepageShowcase/styles.module.css b/docs-site/src/components/HomepageShowcase/styles.module.css index c40b229..22de943 100644 --- a/docs-site/src/components/HomepageShowcase/styles.module.css +++ b/docs-site/src/components/HomepageShowcase/styles.module.css @@ -1,10 +1,14 @@ .section { - padding: 4rem 0 6rem; - background: linear-gradient(180deg, transparent 0%, rgba(22, 93, 255, 0.03) 100%); + padding: 4.5rem 0 5.5rem; + background: + linear-gradient(180deg, rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0.72) 100%), + var(--ifm-background-color); } [data-theme='dark'] .section { - background: linear-gradient(180deg, transparent 0%, rgba(64, 128, 255, 0.04) 100%); + background: + linear-gradient(180deg, rgba(15, 17, 21, 0) 0%, rgba(255, 255, 255, 0.03) 100%), + var(--ifm-background-color); } .sectionHead { @@ -14,26 +18,30 @@ } .sectionTag { - display: inline-block; + display: inline-flex; + align-items: center; + min-height: 28px; font-size: 12px; - font-weight: 600; - letter-spacing: 0.15em; - color: #8f4bff; + font-weight: 750; + letter-spacing: 0; + color: #0e7490; padding: 4px 12px; - background: rgba(143, 75, 255, 0.08); - border-radius: 4px; + background: rgba(20, 201, 201, 0.1); + border: 1px solid rgba(20, 201, 201, 0.2); + border-radius: 8px; margin-bottom: 1rem; } [data-theme='dark'] .sectionTag { - background: rgba(143, 75, 255, 0.18); + background: rgba(20, 201, 201, 0.16); + color: #67e8f9; } .sectionTitle { - font-size: clamp(1.8rem, 3vw, 2.5rem); + font-size: 2.35rem; line-height: 1.2; - letter-spacing: -0.02em; - font-weight: 700; + letter-spacing: 0; + font-weight: 750; margin: 0 0 1rem; color: var(--ifm-heading-color); } @@ -49,34 +57,39 @@ .tabs { display: flex; justify-content: center; - gap: 8px; + gap: 6px; margin-bottom: 2rem; flex-wrap: wrap; + padding: 6px; + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; } .tabBtn { + min-height: 40px; padding: 8px 18px; background: transparent; - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 999px; + border: 1px solid transparent; + border-radius: 8px; color: var(--ifm-color-content-secondary); font-size: 14px; - font-weight: 500; + font-weight: 650; cursor: pointer; - transition: all 0.2s ease; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; } .tabBtn:hover { color: var(--ifm-color-primary); - border-color: var(--ifm-color-primary); + background: var(--ifm-background-color); } .tabBtnActive, .tabBtnActive:hover { - background: linear-gradient(90deg, #165dff 0%, #4080ff 100%); - color: #fff !important; - border-color: transparent; - box-shadow: 0 4px 14px rgba(22, 93, 255, 0.3); + background: var(--ifm-background-color); + color: var(--ifm-color-primary) !important; + border-color: rgba(22, 93, 255, 0.18); + box-shadow: 0 6px 16px rgba(22, 93, 255, 0.12); } /* Stage */ @@ -96,10 +109,10 @@ .browser { background: var(--ifm-background-color); - border-radius: 12px; + border-radius: 8px; overflow: hidden; box-shadow: - 0 30px 60px -20px rgba(22, 93, 255, 0.25), + 0 24px 58px -22px rgba(22, 93, 255, 0.28), 0 0 0 1px var(--ifm-color-emphasis-200); } @@ -137,7 +150,7 @@ margin: 0 auto; padding: 3px 14px; background: var(--ifm-background-color); - border-radius: 999px; + border-radius: 8px; font-size: 12px; color: var(--ifm-color-content-secondary); font-family: 'SFMono-Regular', Menlo, monospace; @@ -169,8 +182,8 @@ .captionTitle { font-size: 1.7rem; line-height: 1.2; - letter-spacing: -0.02em; - font-weight: 700; + letter-spacing: 0; + font-weight: 750; margin: 0 0 1rem; color: var(--ifm-heading-color); } @@ -186,11 +199,49 @@ display: inline-flex; align-items: center; gap: 4px; - font-weight: 500; + min-height: 40px; + padding: 0 12px; + border: 1px solid rgba(22, 93, 255, 0.18); + border-radius: 8px; + font-weight: 650; color: var(--ifm-color-primary); text-decoration: none !important; + transition: border-color 0.2s ease, background 0.2s ease; } .captionLink:hover { color: var(--ifm-color-primary-dark); + background: rgba(22, 93, 255, 0.06); + border-color: var(--ifm-color-primary); +} + +@media (max-width: 996px) { + .sectionTitle { + font-size: 2rem; + } +} + +@media (max-width: 640px) { + .section { + padding: 3.25rem 0 4rem; + } + + .sectionTitle { + font-size: 1.75rem; + } + + .tabs { + justify-content: stretch; + } + + .tabBtn { + flex: 1 1 130px; + } +} + +@media (prefers-reduced-motion: reduce) { + .tabBtn, + .captionLink { + transition: none; + } } diff --git a/docs-site/src/css/custom.css b/docs-site/src/css/custom.css index 981db85..1721433 100644 --- a/docs-site/src/css/custom.css +++ b/docs-site/src/css/custom.css @@ -16,14 +16,15 @@ /* Surfaces */ --ifm-background-color: #ffffff; --ifm-background-surface-color: #ffffff; - --ifm-color-emphasis-100: #f7f9fc; - --ifm-color-emphasis-200: #eef1f6; - --ifm-color-emphasis-300: #dde3ec; + --ifm-color-emphasis-100: #f5f7fa; + --ifm-color-emphasis-200: #e5e6eb; + --ifm-color-emphasis-300: #c9cdd4; + --ifm-color-emphasis-400: #a9aeb8; /* Typography */ --ifm-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; --ifm-font-family-monospace: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - --ifm-heading-font-weight: 600; + --ifm-heading-font-weight: 700; --ifm-code-font-size: 92%; --ifm-h1-font-size: 2.25rem; --ifm-h2-font-size: 1.75rem; @@ -33,10 +34,11 @@ --ifm-color-content: #1d2129; --ifm-color-content-secondary: #4e5969; --ifm-heading-color: #1d2129; + --ifm-global-radius: 8px; /* Navbar */ --ifm-navbar-height: 64px; - --ifm-navbar-background-color: rgba(255, 255, 255, 0.82); + --ifm-navbar-background-color: rgba(255, 255, 255, 0.9); --ifm-navbar-link-color: #4e5969; --ifm-navbar-link-hover-color: var(--ifm-color-primary); @@ -64,15 +66,16 @@ --ifm-background-color: #0f1115; --ifm-background-surface-color: #16181d; - --ifm-color-emphasis-100: #1a1d23; - --ifm-color-emphasis-200: #23272f; - --ifm-color-emphasis-300: #2e343d; + --ifm-color-emphasis-100: #1d2129; + --ifm-color-emphasis-200: #272e3b; + --ifm-color-emphasis-300: #384252; + --ifm-color-emphasis-400: #4e5969; --ifm-color-content: #e6e9ef; --ifm-color-content-secondary: #9aa3b2; --ifm-heading-color: #f0f2f5; - --ifm-navbar-background-color: rgba(15, 17, 21, 0.82); + --ifm-navbar-background-color: rgba(15, 17, 21, 0.9); --ifm-navbar-link-color: #c9d1db; --ifm-menu-color: #c9d1db; @@ -97,7 +100,7 @@ .navbar__title { font-weight: 700; - letter-spacing: -0.01em; + letter-spacing: 0; } .navbar__link { @@ -105,10 +108,26 @@ font-size: 14px; } +.navbar__link, +.button, +a { + transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +.button { + border-radius: 8px; + font-weight: 650; +} + +:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; +} + /* Sidebar tweaks */ .menu__link { font-size: 14px; - border-radius: 6px; + border-radius: 8px; padding: 6px 10px; line-height: 1.4; } @@ -226,9 +245,20 @@ code { } ::-webkit-scrollbar-thumb:hover { - background: var(--ifm-color-emphasis-400, #adb5bd); + background: var(--ifm-color-emphasis-400); } [data-theme='dark'] ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/docs-site/src/pages/community.tsx b/docs-site/src/pages/community.tsx new file mode 100644 index 0000000..4e2b08c --- /dev/null +++ b/docs-site/src/pages/community.tsx @@ -0,0 +1,19 @@ +import type {ReactNode} from 'react'; +import {translate} from '@docusaurus/Translate'; +import Layout from '@theme/Layout'; +import HomepageCommunity from '@site/src/components/HomepageCommunity'; + +export default function Community(): ReactNode { + return ( + <Layout + title={translate({id: 'community.pageTitle', message: 'Community, sponsors and contributors'})} + description={translate({ + id: 'community.pageDescription', + message: 'Sponsor BackupX, meet contributors, and find practical ways to contribute.', + })}> + <main> + <HomepageCommunity /> + </main> + </Layout> + ); +} diff --git a/docs-site/src/pages/index.module.css b/docs-site/src/pages/index.module.css index bf2d549..80d6410 100644 --- a/docs-site/src/pages/index.module.css +++ b/docs-site/src/pages/index.module.css @@ -1,48 +1,42 @@ -/* ── Hero ───────────────────────────────────────────── */ +/* Hero */ .hero { position: relative; - padding: 7rem 0 6rem; overflow: hidden; - background: var(--bx-hero-bg); + padding: 7rem 0 5.5rem; + background: + linear-gradient(180deg, rgba(22, 93, 255, 0.08) 0%, rgba(255, 255, 255, 0) 72%), + linear-gradient(90deg, rgba(20, 201, 201, 0.08) 0%, rgba(250, 173, 20, 0.08) 100%), + var(--ifm-background-color); } -.heroBg { +.hero::before { position: absolute; inset: 0; - background: - radial-gradient(circle at 15% 20%, rgba(104, 127, 255, 0.18) 0%, transparent 45%), - radial-gradient(circle at 85% 70%, rgba(22, 93, 255, 0.15) 0%, transparent 50%), - linear-gradient(180deg, #f7f9ff 0%, #ffffff 100%); - z-index: 0; + content: ""; + pointer-events: none; + background-image: + linear-gradient(rgba(22, 93, 255, 0.06) 1px, transparent 1px), + linear-gradient(90deg, rgba(22, 93, 255, 0.06) 1px, transparent 1px); + background-size: 44px 44px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.75), transparent 82%); } -[data-theme='dark'] .heroBg { +[data-theme='dark'] .hero { background: - radial-gradient(circle at 15% 20%, rgba(96, 126, 255, 0.22) 0%, transparent 45%), - radial-gradient(circle at 85% 70%, rgba(118, 70, 255, 0.18) 0%, transparent 50%), - linear-gradient(180deg, #0f1115 0%, #0b0d10 100%); + linear-gradient(180deg, rgba(64, 128, 255, 0.16) 0%, rgba(15, 17, 21, 0) 72%), + linear-gradient(90deg, rgba(20, 201, 201, 0.1) 0%, rgba(250, 173, 20, 0.08) 100%), + var(--ifm-background-color); } .heroInner { position: relative; z-index: 1; display: grid; - grid-template-columns: 1.1fr 1fr; + grid-template-columns: minmax(0, 1fr) minmax(420px, 0.9fr); gap: 4rem; align-items: center; } -@media (max-width: 996px) { - .hero { - padding: 4rem 0 3rem; - } - .heroInner { - grid-template-columns: 1fr; - gap: 2.5rem; - text-align: left; - } -} - .heroContent { display: flex; flex-direction: column; @@ -54,137 +48,144 @@ display: inline-flex; align-items: center; gap: 8px; - padding: 4px 14px; - background: rgba(22, 93, 255, 0.08); - border: 1px solid rgba(22, 93, 255, 0.15); - border-radius: 999px; - font-size: 13px; + min-height: 32px; + padding: 5px 12px; color: var(--ifm-color-primary); - font-weight: 500; + background: rgba(22, 93, 255, 0.09); + border: 1px solid rgba(22, 93, 255, 0.2); + border-radius: 8px; + font-size: 13px; + font-weight: 600; } [data-theme='dark'] .badge { - background: rgba(96, 126, 255, 0.15); - border-color: rgba(96, 126, 255, 0.3); + background: rgba(64, 128, 255, 0.16); + border-color: rgba(64, 128, 255, 0.3); color: var(--ifm-color-primary-lighter); } .badgeDot { - width: 6px; - height: 6px; - background: var(--ifm-color-primary); + width: 7px; + height: 7px; + background: #00b42a; border-radius: 50%; - box-shadow: 0 0 0 4px rgba(22, 93, 255, 0.18); - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + box-shadow: 0 0 0 4px rgba(0, 180, 42, 0.12); } .heroTitle { - font-size: clamp(2.25rem, 4vw, 3.4rem); - line-height: 1.15; - letter-spacing: -0.025em; - font-weight: 700; margin: 0; color: var(--ifm-heading-color); + font-size: 3.45rem; + font-weight: 750; + letter-spacing: 0; + line-height: 1.08; } .heroTitleAccent { display: block; - background: linear-gradient(90deg, #4080ff 0%, #8f4bff 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-top: 6px; + margin-top: 8px; + color: var(--ifm-color-primary); } .heroSubtitle { - font-size: 1.15rem; - line-height: 1.65; - color: var(--ifm-color-content-secondary); - max-width: 540px; + max-width: 640px; margin: 0; + color: var(--ifm-color-content-secondary); + font-size: 1.15rem; + line-height: 1.72; } .actions { display: flex; + flex-wrap: wrap; gap: 12px; margin-top: 8px; - flex-wrap: wrap; +} + +.primaryBtn, +.secondaryBtn { + min-height: 46px; + border-radius: 8px; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; } .primaryBtn { - background: linear-gradient(90deg, #165dff 0%, #4080ff 100%); - border: none; - color: #fff; display: inline-flex; align-items: center; - gap: 6px; - font-weight: 600; - box-shadow: 0 6px 20px rgba(22, 93, 255, 0.3); - transition: transform 0.2s ease, box-shadow 0.2s ease; + gap: 8px; + color: #fff; + background: #165dff; + border: 1px solid #165dff; + box-shadow: 0 10px 24px rgba(22, 93, 255, 0.24); + font-weight: 650; } -.primaryBtn:hover { - transform: translateY(-1px); - box-shadow: 0 10px 25px rgba(22, 93, 255, 0.4); +.primaryBtn:hover, +.primaryBtn:focus-visible { color: #fff; + background: #0e4fe6; + border-color: #0e4fe6; + box-shadow: 0 14px 30px rgba(22, 93, 255, 0.3); + transform: translateY(-1px); } .btnArrow { transition: transform 0.2s ease; } -.primaryBtn:hover .btnArrow { - transform: translateX(4px); +.primaryBtn:hover .btnArrow, +.primaryBtn:focus-visible .btnArrow { + transform: translateX(3px); } .secondaryBtn { - background: var(--ifm-background-color); - border: 1px solid var(--ifm-color-emphasis-300); - color: var(--ifm-font-color-base); display: inline-flex; align-items: center; - font-weight: 500; - transition: all 0.2s ease; + color: var(--ifm-font-color-base); + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-300); + font-weight: 600; } -.secondaryBtn:hover { - border-color: var(--ifm-color-primary); +.secondaryBtn:hover, +.secondaryBtn:focus-visible { color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); background: var(--ifm-background-color); + transform: translateY(-1px); } .metrics { display: flex; align-items: center; - gap: 1.75rem; - padding-top: 1.5rem; + gap: 1.5rem; margin-top: 0.5rem; + padding-top: 1.25rem; } .metric { display: flex; + min-width: 0; flex-direction: column; - gap: 2px; + gap: 4px; } .metricValue { - font-size: 1.6rem; - font-weight: 700; color: var(--ifm-heading-color); + font-size: 1.35rem; + font-weight: 750; + letter-spacing: 0; line-height: 1.1; - letter-spacing: -0.02em; + white-space: nowrap; } .metricLabel { - font-size: 12px; color: var(--ifm-color-content-secondary); + font-size: 12px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.35; text-transform: uppercase; - letter-spacing: 0.05em; } .metricDivider { @@ -193,81 +194,277 @@ background: var(--ifm-color-emphasis-300); } -/* ── Code window (macOS-style) ─────────────────────── */ -.heroCode { - position: relative; +/* Product visual */ +.heroVisual { + display: grid; + gap: 1rem; } -.codeWindow { - background: #0f1622; - border-radius: 12px; - box-shadow: - 0 20px 50px -10px rgba(15, 22, 34, 0.35), - 0 0 0 1px rgba(255, 255, 255, 0.05); +.consolePanel { overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(22, 93, 255, 0.16); + border-radius: 8px; + box-shadow: 0 24px 60px rgba(29, 33, 41, 0.12); } -[data-theme='light'] .codeWindow { - box-shadow: 0 20px 50px -10px rgba(22, 93, 255, 0.2), 0 0 0 1px rgba(22, 93, 255, 0.06); +[data-theme='dark'] .consolePanel { + background: rgba(22, 24, 29, 0.9); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.34); } -.codeHeader { +.consoleHeader { display: flex; - align-items: center; - gap: 6px; - padding: 10px 14px; - background: #161f2e; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1.25rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); } -.codeDot { - width: 11px; - height: 11px; - border-radius: 50%; +[data-theme='dark'] .consoleHeader { + border-bottom-color: rgba(255, 255, 255, 0.08); } -.codeDotRed { background: #ff5f56; } -.codeDotYellow { background: #ffbd2e; } -.codeDotGreen { background: #27c93f; } +.consoleHeader strong { + display: block; + margin-top: 4px; + color: var(--ifm-heading-color); + font-size: 1.2rem; +} -.codeTitle { - margin-left: auto; - font-size: 11px; - color: #7b8696; - letter-spacing: 0.05em; +.consoleEyebrow { + color: var(--ifm-color-content-secondary); + font-size: 12px; + font-weight: 650; + letter-spacing: 0; text-transform: uppercase; } -.codeBody { - margin: 0; - padding: 18px 20px; - font-family: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - font-size: 13px; - line-height: 1.65; - color: #e1e7ef; - background: transparent; +.consoleStatus { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 4px 10px; + color: #00a870; + background: rgba(0, 180, 42, 0.1); + border: 1px solid rgba(0, 180, 42, 0.2); + border-radius: 8px; + font-size: 12px; + font-weight: 700; +} + +.consoleGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .consoleGrid { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.consoleGrid > div { + min-width: 0; + padding: 1.1rem 1.25rem; + border-right: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .consoleGrid > div { + border-right-color: rgba(255, 255, 255, 0.08); +} + +.consoleGrid > div:last-child { + border-right: 0; +} + +.consoleGrid strong { + display: block; + margin-top: 6px; + color: var(--ifm-heading-color); + font-size: 1.45rem; + line-height: 1.1; +} + +.consoleLabel { + display: block; + color: var(--ifm-color-content-secondary); + font-size: 12px; + font-weight: 650; +} + +.timeline { + display: grid; +} + +.timelineRow { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .timelineRow { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.timelineRow:last-child { + border-bottom: 0; +} + +.timelineRow strong, +.timelineRow span { + display: block; +} + +.timelineRow strong { + color: var(--ifm-heading-color); + font-size: 0.95rem; + font-weight: 700; +} + +.timelineRow span { + color: var(--ifm-color-content-secondary); + font-size: 0.85rem; + line-height: 1.5; +} + +.timelineRow em { + color: var(--ifm-color-content-secondary); + font-size: 0.8rem; + font-style: normal; + font-weight: 650; + white-space: nowrap; +} + +.timelineDotOk, +.timelineDotInfo, +.timelineDotWarn { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.timelineDotOk { + background: #00b42a; +} + +.timelineDotInfo { + background: #165dff; +} + +.timelineDotWarn { + background: #ff7d00; +} + +.commandCard { + display: grid; + gap: 8px; + padding: 1rem 1.1rem; + background: #111827; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + box-shadow: 0 16px 34px rgba(17, 24, 39, 0.18); +} + +.commandTitle { + color: #9ca3af; + font-size: 12px; + font-weight: 650; + letter-spacing: 0; + text-transform: uppercase; +} + +.commandCard code { overflow-x: auto; -} - -.codeBody code { + color: #e5e7eb; background: transparent; - padding: 0; border: 0; - color: inherit; + padding: 0; + font-size: 13px; + white-space: nowrap; } -.codePrompt { - color: #4080ff; - margin-right: 6px; - user-select: none; +@media (max-width: 996px) { + .hero { + padding: 4.5rem 0 3.5rem; + } + + .heroInner { + grid-template-columns: 1fr; + gap: 2.25rem; + } + + .heroTitle { + font-size: 2.45rem; + } } -.codeComment { - color: #6e7889; - font-style: italic; +@media (max-width: 640px) { + .hero { + padding: 3.75rem 0 2.75rem; + } + + .heroTitle { + font-size: 2.05rem; + } + + .heroSubtitle { + font-size: 1rem; + } + + .actions { + width: 100%; + } + + .primaryBtn, + .secondaryBtn { + width: 100%; + justify-content: center; + } + + .metrics { + width: 100%; + align-items: stretch; + gap: 0.85rem; + flex-direction: column; + } + + .metricDivider { + width: 100%; + height: 1px; + } + + .consoleHeader, + .timelineRow { + padding: 1rem; + } + + .consoleGrid { + grid-template-columns: 1fr; + } + + .consoleGrid > div { + border-right: 0; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + } + + .consoleGrid > div:last-child { + border-bottom: 0; + } + + [data-theme='dark'] .consoleGrid > div { + border-bottom-color: rgba(255, 255, 255, 0.08); + } } -.codeString { - color: #82d1ff; +@media (prefers-reduced-motion: reduce) { + .primaryBtn, + .secondaryBtn, + .btnArrow { + transition: none; + } } diff --git a/docs-site/src/pages/index.tsx b/docs-site/src/pages/index.tsx index c7049e8..4a8971a 100644 --- a/docs-site/src/pages/index.tsx +++ b/docs-site/src/pages/index.tsx @@ -7,34 +7,34 @@ import Layout from '@theme/Layout'; import Heading from '@theme/Heading'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; import HomepageShowcase from '@site/src/components/HomepageShowcase'; +import HomepageCommunity from '@site/src/components/HomepageCommunity'; import styles from './index.module.css'; function HomepageHeader() { return ( <header className={styles.hero}> - <div className={styles.heroBg} aria-hidden="true" /> <div className={clsx('container', styles.heroInner)}> <div className={styles.heroContent}> <div className={styles.badge}> <span className={styles.badgeDot} /> - <Translate id="home.badge">Open-source · v1.6.0</Translate> + <Translate id="home.badge">Open-source backup control plane · v2.2.1</Translate> </div> <Heading as="h1" className={styles.heroTitle}> - <Translate id="home.title.part1">Self-hosted backup management</Translate> + <Translate id="home.title.part1">Backup orchestration</Translate> <span className={styles.heroTitleAccent}> - <Translate id="home.title.part2">for every server.</Translate> + <Translate id="home.title.part2">for self-hosted servers.</Translate> </span> </Heading> <p className={styles.heroSubtitle}> <Translate id="home.tagline"> - One binary, one command. File / database / SAP HANA backups routed to 70+ storage backends. + Run file, database, SAP HANA and remote-node backups from one clean console. Keep the control plane yours, keep the storage flexible. </Translate> </p> <div className={styles.actions}> <Link className={clsx('button button--primary button--lg', styles.primaryBtn)} to="/docs/getting-started/quick-start"> <Translate id="home.getStarted">Get Started</Translate> - <span className={styles.btnArrow} aria-hidden="true">→</span> + <span className={styles.btnArrow} aria-hidden="true">-></span> </Link> <Link className={clsx('button button--lg', styles.secondaryBtn)} to="https://github.com/Awuqing/BackupX"> <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{marginRight: 6}}> @@ -52,9 +52,9 @@ function HomepageHeader() { </div> <div className={styles.metricDivider} /> <div className={styles.metric}> - <div className={styles.metricValue}>5</div> + <div className={styles.metricValue}>Agent</div> <div className={styles.metricLabel}> - <Translate id="home.metric.backupTypes">Backup types</Translate> + <Translate id="home.metric.backupTypes">Remote execution</Translate> </div> </div> <div className={styles.metricDivider} /> @@ -66,29 +66,85 @@ function HomepageHeader() { </div> </div> </div> - <div className={styles.heroCode}> - <div className={styles.codeWindow}> - <div className={styles.codeHeader}> - <span className={clsx(styles.codeDot, styles.codeDotRed)} /> - <span className={clsx(styles.codeDot, styles.codeDotYellow)} /> - <span className={clsx(styles.codeDot, styles.codeDotGreen)} /> - <span className={styles.codeTitle}>bash</span> + <div className={styles.heroVisual}> + <div className={styles.consolePanel}> + <div className={styles.consoleHeader}> + <div> + <span className={styles.consoleEyebrow}> + <Translate id="home.visual.eyebrow">BackupX Console</Translate> + </span> + <strong> + <Translate id="home.visual.title">Operations overview</Translate> + </strong> + </div> + <span className={styles.consoleStatus}> + <Translate id="home.visual.status">Healthy</Translate> + </span> </div> - <pre className={styles.codeBody}> - <code> - <span className={styles.codeComment}># Docker one-liner</span>{'\n'} - <span className={styles.codePrompt}>$</span> docker run -d --name backupx \{'\n'} - {' '}-p 8340:8340 \{'\n'} - {' '}-v backupx-data:/app/data \{'\n'} - {' '}awuqing/backupx:latest{'\n'} - {'\n'} - <span className={styles.codeComment}># Open http://localhost:8340</span>{'\n'} - <span className={styles.codeComment}># Deploy an Agent on a remote host</span>{'\n'} - <span className={styles.codePrompt}>$</span> backupx agent \{'\n'} - {' '}--master <span className={styles.codeString}>http://master:8340</span> \{'\n'} - {' '}--token <span className={styles.codeString}><token></span> - </code> - </pre> + <div className={styles.consoleGrid}> + <div> + <span className={styles.consoleLabel}> + <Translate id="home.visual.success">Success rate</Translate> + </span> + <strong>99.4%</strong> + </div> + <div> + <span className={styles.consoleLabel}> + <Translate id="home.visual.nodes">Active nodes</Translate> + </span> + <strong>12</strong> + </div> + <div> + <span className={styles.consoleLabel}> + <Translate id="home.visual.targets">Storage targets</Translate> + </span> + <strong>8</strong> + </div> + </div> + <div className={styles.timeline}> + <div className={styles.timelineRow}> + <span className={styles.timelineDotOk} /> + <div> + <strong> + <Translate id="home.visual.row1.title">PostgreSQL nightly</Translate> + </strong> + <span> + <Translate id="home.visual.row1.desc">Encrypted archive uploaded to S3</Translate> + </span> + </div> + <em>02:10</em> + </div> + <div className={styles.timelineRow}> + <span className={styles.timelineDotInfo} /> + <div> + <strong> + <Translate id="home.visual.row2.title">SAP HANA snapshot</Translate> + </strong> + <span> + <Translate id="home.visual.row2.desc">Running on agent-shanghai-02</Translate> + </span> + </div> + <em>68%</em> + </div> + <div className={styles.timelineRow}> + <span className={styles.timelineDotWarn} /> + <div> + <strong> + <Translate id="home.visual.row3.title">Retention cleanup</Translate> + </strong> + <span> + <Translate id="home.visual.row3.desc">Next run in 4 hours</Translate> + </span> + </div> + <em>queued</em> + </div> + </div> + </div> + <div className={styles.commandCard}> + <div className={styles.commandTitle}> + <Translate id="home.command.title">Start with Docker</Translate> + </div> + <code>docker run -d -p 8340:8340 awuqing/backupx:v2.2.1</code> </div> </div> </div> @@ -100,12 +156,13 @@ export default function Home(): ReactNode { const {siteConfig} = useDocusaurusContext(); return ( <Layout - title={translate({id: 'home.pageTitle', message: 'Self-hosted backup management'})} + title={translate({id: 'home.pageTitle', message: 'Backup orchestration for self-hosted servers'})} description={siteConfig.tagline}> <HomepageHeader /> <main> <HomepageFeatures /> <HomepageShowcase /> + <HomepageCommunity /> </main> </Layout> ); diff --git a/docs-site/src/pages/sponsors.tsx b/docs-site/src/pages/sponsors.tsx new file mode 100644 index 0000000..5626991 --- /dev/null +++ b/docs-site/src/pages/sponsors.tsx @@ -0,0 +1,39 @@ +import type {ReactNode} from 'react'; +import {translate} from '@docusaurus/Translate'; +import Translate from '@docusaurus/Translate'; +import Layout from '@theme/Layout'; +import Heading from '@theme/Heading'; +import {HomepageSponsors} from '@site/src/components/HomepageCommunity'; +import styles from '@site/src/components/HomepageCommunity/styles.module.css'; + +export default function Sponsors(): ReactNode { + return ( + <Layout + title={translate({id: 'sponsors.pageTitle', message: 'Sponsors'})} + description={translate({ + id: 'sponsors.pageDescription', + message: 'Sponsor BackupX reliability, documentation, storage compatibility and long-term maintenance.', + })}> + <main> + <section className={styles.section}> + <div className="container"> + <div className={styles.sectionHead}> + <div className={styles.sectionTag}> + <Translate id="sponsors.tag">SPONSORS</Translate> + </div> + <Heading as="h1" className={styles.sectionTitle}> + <Translate id="sponsors.title">Sponsor the BackupX ecosystem</Translate> + </Heading> + <p className={styles.sectionSubtitle}> + <Translate id="sponsors.subtitle"> + Sponsorship helps keep BackupX practical for real operators: tested storage providers, reliable releases, restore confidence and better documentation. + </Translate> + </p> + </div> + <HomepageSponsors /> + </div> + </section> + </main> + </Layout> + ); +}