diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index c768916f9..6c0091a4a 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,6 +48,9 @@ jobs: if: github.event_name != 'pull_request' run: yarn crowdin:sync + - name: Generate footer stars + run: yarn generate:footer-stars + - name: Build run: yarn build diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml index 8c68a5795..3024e6cec 100644 --- a/.github/workflows/pr_ci.yml +++ b/.github/workflows/pr_ci.yml @@ -22,5 +22,8 @@ jobs: - name: Install dependencies run: yarn install + - name: Generate footer stars + run: yarn generate:footer-stars + - name: Build website run: yarn build --locale en diff --git a/docusaurus.config.js b/docusaurus.config.js index fb91717f2..518e4524e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -234,72 +234,8 @@ module.exports = { }, { title: "More", - items: [ - { - html: ` - - GitHub Repo stars - -            - - GitHub Repo stars - - `, - }, - { - html: ` - - GitHub Repo stars - -    - - GitHub Repo stars - - `, - }, - { - html: ` - - GitHub Repo stars - -         - - GitHub Repo stars - - `, - }, - { - html: ` - - GitHub Repo stars - -      - - GitHub Repo stars - - `, - }, - { - html: ` - - Twitter Follow - - `, - }, - { - html: ` - -`, - }, - ], + className: "footer-more-column", + items: [], }, ], logo: { diff --git a/package.json b/package.json index 153182a17..d63b6281e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", + "generate:footer-stars": "node scripts/generate-footer-stars.js", "start": "node scripts/start.js", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/scripts/generate-footer-stars.js b/scripts/generate-footer-stars.js new file mode 100644 index 000000000..0ab1a51a0 --- /dev/null +++ b/scripts/generate-footer-stars.js @@ -0,0 +1,90 @@ +const fs = require("fs"); +const path = require("path"); + +const badges = require("../src/components/FooterMoreBadges/badges.json"); + +const outputPath = path.join( + __dirname, + "..", + "src", + "components", + "FooterMoreBadges", + "stars.json" +); + +function getRepoKey(owner, repo) { + return `${owner}/${repo}`; +} + +function readExistingStars() { + if (!fs.existsSync(outputPath)) { + return null; + } + + try { + return JSON.parse(fs.readFileSync(outputPath, "utf8")); + } catch { + return null; + } +} + +async function fetchStars() { + const headers = { + Accept: "application/vnd.github+json", + "User-Agent": "casbin-website-footer-stars", + }; + + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + const results = await Promise.all( + badges.map(async({owner, repo}) => { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { + headers, + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error(`Failed to fetch stars for ${owner}/${repo}: ${response.status} ${body}`); + } + + const data = await response.json(); + + if (typeof data.stargazers_count !== "number") { + throw new Error(`Missing stargazers_count for ${owner}/${repo}`); + } + + return [getRepoKey(owner, repo), data.stargazers_count]; + }) + ); + + return Object.fromEntries(results); +} + +async function main() { + const existingStars = readExistingStars(); + + try { + const stars = await fetchStars(); + const payload = { + generatedAt: new Date().toISOString(), + stars, + }; + + fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`); + process.stdout.write(`Updated footer stars at ${outputPath}\n`); + } catch (error) { + if (existingStars?.stars) { + process.stderr.write(`${error.message}\nUsing existing footer stars JSON.\n`); + return; + } + + throw error; + } +} + +main().catch((error) => { + process.stderr.write(`${error.stack || error}\n`); + process.exit(1); +}); diff --git a/src/components/FooterMoreBadges/badges.json b/src/components/FooterMoreBadges/badges.json new file mode 100644 index 000000000..d26952cd5 --- /dev/null +++ b/src/components/FooterMoreBadges/badges.json @@ -0,0 +1,50 @@ +[ + { + "label": "Casbin", + "owner": "casbin", + "repo": "casbin", + "href": "https://github.com/casbin/casbin" + }, + { + "label": "jCasbin", + "owner": "casbin", + "repo": "jcasbin", + "href": "https://github.com/casbin/jcasbin" + }, + { + "label": "Node-Casbin", + "owner": "casbin", + "repo": "node-casbin", + "href": "https://github.com/casbin/node-casbin" + }, + { + "label": "PHP-Casbin", + "owner": "php-casbin", + "repo": "php-casbin", + "href": "https://github.com/php-casbin/php-casbin" + }, + { + "label": "PyCasbin", + "owner": "casbin", + "repo": "pycasbin", + "href": "https://github.com/casbin/pycasbin" + }, + { + "label": "Casbin.NET", + "owner": "casbin", + "repo": "Casbin.NET", + "href": "https://github.com/casbin/Casbin.NET" + }, + { + "label": "Casbin-CPP", + "owner": "casbin", + "repo": "casbin-cpp", + "href": "https://github.com/casbin/casbin-cpp" + }, + { + "label": "Casbin-RS", + "owner": "casbin", + "repo": "casbin-rs", + "href": "https://github.com/casbin/casbin-rs" + } +] diff --git a/src/components/FooterMoreBadges/index.js b/src/components/FooterMoreBadges/index.js new file mode 100644 index 000000000..412108190 --- /dev/null +++ b/src/components/FooterMoreBadges/index.js @@ -0,0 +1,90 @@ +import React from "react"; +import styles from "./styles.module.css"; +import githubBadges from "./badges.json"; +import starsData from "./stars.json"; + +const GITHUB_BADGES = githubBadges; +const STAR_COUNTS = starsData.stars ?? {}; +const STAR_FORMATTER = new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + maximumFractionDigits: 1, +}); + +function getRepoKey(owner, repo) { + return `${owner}/${repo}`; +} + +function formatStars(count) { + return STAR_FORMATTER.format(count).replace(/\.0(?=[A-Za-z])/u, "").toLowerCase(); +} + +function BadgeLink({href, ariaLabel, children}) { + return ( + + {children} + + ); +} + +function GitHubStarBadge({label, stars}) { + const safeStars = Number.isFinite(stars) ? stars : 0; + const formattedStars = formatStars(safeStars); + + return ( +