refactor - use package

This commit is contained in:
Ayres Vitor
2025-07-31 14:16:27 -03:00
parent 9ad65b01fc
commit e29a987986
16 changed files with 774 additions and 449 deletions

View File

@@ -0,0 +1,10 @@
export const OPEN_COLLECTIVE_FILE = '../../src/data/openCollectiveData.json';
export const GITHUB_SPONSORS_FILE = '../../src/data/githubSponsorsData.json';
export const GITHUB_CONTRIBUTORS_FILE = '../../src/data/githubContributorsData.json';
export const PLATINUM_THRESHOLD = 5_000;
export const GOLD_THRESHOLD = 500;
export const SILVER_THRESHOLD = 100;
export const GH_IMAGE_DIMENSION = 64;
export const OC_IMAGE_DIMENSION = 256;

View File

@@ -1,266 +1,266 @@
import { Octokit } from '@octokit/core';
import { paginateGraphQL, type PageInfoForward } from '@octokit/plugin-paginate-graphql';
import { paginateRest } from '@octokit/plugin-paginate-rest';
import { retry } from '@octokit/plugin-retry';
import type { Endpoints } from '@octokit/types';
import { throttling } from '@octokit/plugin-throttling';
import path from 'node:path';
import { writeFileSync } from 'node:fs';
// todo: write once to use on dev mode / prs
// then re fetch on prod
// todo: move to package
export const DATA_FILE = path.resolve('./src/components/sponsors/_githubContributorsData.json');
export interface Contributor {
login: string;
avatar_url: string;
total_contributions: number;
}
type APIData<T extends keyof Endpoints> = Endpoints[T]['response']['data'];
type Repo = APIData<'GET /orgs/{org}/repos'>[number];
interface Review {
login: string | undefined;
avatarUrl: string | undefined;
prNumber: number;
labels: string[];
}
interface AugmentedRepo extends Repo {
reviewComments: any[];
issues: any[];
reviews: Review[];
}
const OctokitWithPlugins = Octokit.plugin(paginateRest, paginateGraphQL, retry, throttling);
export class StatsCollector {
#org: string;
#app: InstanceType<typeof OctokitWithPlugins>;
#contributionThreshold: number;
constructor(opts: { org: string; token: string | undefined; contributionThreshold: number }) {
this.#org = opts.org;
if (!opts.token) {
throw new Error('GITHUB_TOKEN is required');
}
this.#app = new OctokitWithPlugins({
auth: opts.token,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
if (retryCount < 1) {
// only retries once
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onSecondaryRateLimit: (retryAfter, options, octokit) => {
// does not retry, only logs a warning
octokit.log.warn(
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
);
},
},
});
this.#contributionThreshold = opts.contributionThreshold;
}
async run() {
const repos = await this.#getReposWithExtraStats();
const contributors: Record<string, Contributor> = {};
for (const repo of repos) {
for (const issue of repo.issues) {
const { user, pull_request } = issue;
if (!user) {
continue;
}
const { avatar_url, login } = user;
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url, login }));
if (pull_request) {
contributor.total_contributions++;
if (pull_request.merged_at) {
contributor.total_contributions++;
}
} else {
// is issue
contributor.total_contributions++;
}
}
/** Temporary store for deduplicating multiple reviews on the same PR. */
const reviewedPRs: Record<string, Set<number>> = {};
for (const review of repo.reviewComments) {
const { user, pull_request_url } = review;
const prNumber = parseInt(pull_request_url.split('/').pop()!);
if (!user) {
continue;
}
const { avatar_url, login } = user;
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url, login }));
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
if (!contributorReviews.has(prNumber)) {
contributor.total_contributions++;
contributorReviews.add(prNumber);
}
}
for (const review of repo.reviews) {
const { login, avatarUrl, prNumber } = review;
if (!login || !avatarUrl) {
continue;
}
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url: avatarUrl, login }));
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
if (!contributorReviews.has(prNumber)) {
contributor.total_contributions++;
contributorReviews.add(prNumber);
}
}
}
// Filter contributors based on threshold
const topContributors = Object.values(contributors)
.filter((contributor) => contributor.total_contributions >= this.#contributionThreshold)
.sort((a, b) => b.total_contributions - a.total_contributions);
console.log(
`${topContributors.length} contributors above threshold of ${this.#contributionThreshold} contributions`
);
console.log('saving');
this.#writeData(topContributors);
}
#newContributor({ avatar_url, login }: { avatar_url: string; login: string }): Contributor {
return {
login,
avatar_url,
total_contributions: 0,
};
}
async #getRepos() {
return (
await this.#app.request(`GET /orgs/{org}/repos`, {
org: this.#org,
type: 'sources',
})
).data.filter((repo) => !repo.private);
}
async #getAllIssuesAndPRs(repo: string) {
console.log(`fetching issues and PRs for ${this.#org}/${repo}`);
const issues = await this.#app.paginate('GET /repos/{owner}/{repo}/issues', {
owner: this.#org,
repo,
per_page: 100,
state: 'all',
});
console.log(`found ${issues.length} issues and PRs for ${this.#org}/${repo}`);
return issues;
}
async #getAllReviewComments(repo: string) {
console.log(`fetching PR review comments for ${this.#org}/${repo}`);
const reviews = await this.#app.paginate('GET /repos/{owner}/{repo}/pulls/comments', {
owner: this.#org,
repo,
per_page: 100,
});
console.log(`found ${reviews.length} PR review comments for ${this.#org}/${repo}`);
return reviews;
}
async #getAllReviews(repo: string) {
console.log(`fetching PR reviews for ${this.#org}/${repo}`);
const {
repository: {
pullRequests: { nodes: pullRequests },
},
} = await this.#app.graphql.paginate<{
repository: {
pullRequests: {
pageInfo: PageInfoForward;
nodes: Array<{
number: number;
labels: { nodes: Array<{ name: string }> };
latestReviews: {
nodes: Array<{ author: null | { login: string; avatarUrl: string } }>;
};
}>;
};
};
}>(
`
query ($org: String!, $repo: String!, $cursor: String) {
repository(owner: $org, name: $repo) {
pullRequests(first: 100, after: $cursor) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
latestReviews(first: 15) {
nodes {
author {
login
avatarUrl
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
{ org: this.#org, repo }
);
const reviews: Review[] = [];
for (const { number, labels, latestReviews } of pullRequests) {
for (const { author } of latestReviews.nodes) {
reviews.push({
prNumber: number,
labels: labels.nodes.map(({ name }) => name),
login: author?.login,
avatarUrl: author?.avatarUrl,
});
}
}
console.log(`found ${reviews.length} PR reviews for ${this.#org}/${repo}`);
return reviews;
}
async #getReposWithExtraStats() {
const repos = await this.#getRepos();
console.log(`found ${repos.length} repos`);
const reposWithStats: AugmentedRepo[] = [];
for (const repo of repos.slice(0, 2)) {
reposWithStats.push({
...repo,
issues: await this.#getAllIssuesAndPRs(repo.name),
reviewComments: await this.#getAllReviewComments(repo.name),
reviews: await this.#getAllReviews(repo.name),
});
}
return reposWithStats;
}
#writeData(data: Contributor[]) {
return writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
}
}
import { Octokit } from '@octokit/core';
import { paginateGraphQL, type PageInfoForward } from '@octokit/plugin-paginate-graphql';
import { paginateRest } from '@octokit/plugin-paginate-rest';
import { retry } from '@octokit/plugin-retry';
import type { Endpoints } from '@octokit/types';
import { throttling } from '@octokit/plugin-throttling';
import path from 'node:path';
import { writeFileSync } from 'node:fs';
// todo: write once to use on dev mode / prs
// then re fetch on prod
// todo: move to package
export const DATA_FILE = path.resolve('./src/components/sponsors/_githubContributorsData.json');
export interface Contributor {
login: string;
avatar_url: string;
total_contributions: number;
}
type APIData<T extends keyof Endpoints> = Endpoints[T]['response']['data'];
type Repo = APIData<'GET /orgs/{org}/repos'>[number];
interface Review {
login: string | undefined;
avatarUrl: string | undefined;
prNumber: number;
labels: string[];
}
interface AugmentedRepo extends Repo {
reviewComments: any[];
issues: any[];
reviews: Review[];
}
const OctokitWithPlugins = Octokit.plugin(paginateRest, paginateGraphQL, retry, throttling);
export class StatsCollector {
#org: string;
#app: InstanceType<typeof OctokitWithPlugins>;
#contributionThreshold: number;
constructor(opts: { org: string; token: string | undefined; contributionThreshold: number }) {
this.#org = opts.org;
if (!opts.token) {
throw new Error('GITHUB_TOKEN is required');
}
this.#app = new OctokitWithPlugins({
auth: opts.token,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
if (retryCount < 1) {
// only retries once
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onSecondaryRateLimit: (retryAfter, options, octokit) => {
// does not retry, only logs a warning
octokit.log.warn(
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
);
},
},
});
this.#contributionThreshold = opts.contributionThreshold;
}
async run() {
const repos = await this.#getReposWithExtraStats();
const contributors: Record<string, Contributor> = {};
for (const repo of repos) {
for (const issue of repo.issues) {
const { user, pull_request } = issue;
if (!user) {
continue;
}
const { avatar_url, login } = user;
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url, login }));
if (pull_request) {
contributor.total_contributions++;
if (pull_request.merged_at) {
contributor.total_contributions++;
}
} else {
// is issue
contributor.total_contributions++;
}
}
/** Temporary store for deduplicating multiple reviews on the same PR. */
const reviewedPRs: Record<string, Set<number>> = {};
for (const review of repo.reviewComments) {
const { user, pull_request_url } = review;
const prNumber = parseInt(pull_request_url.split('/').pop()!);
if (!user) {
continue;
}
const { avatar_url, login } = user;
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url, login }));
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
if (!contributorReviews.has(prNumber)) {
contributor.total_contributions++;
contributorReviews.add(prNumber);
}
}
for (const review of repo.reviews) {
const { login, avatarUrl, prNumber } = review;
if (!login || !avatarUrl) {
continue;
}
const contributor = (contributors[login] =
contributors[login] || this.#newContributor({ avatar_url: avatarUrl, login }));
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
if (!contributorReviews.has(prNumber)) {
contributor.total_contributions++;
contributorReviews.add(prNumber);
}
}
}
// Filter contributors based on threshold
const topContributors = Object.values(contributors)
.filter((contributor) => contributor.total_contributions >= this.#contributionThreshold)
.sort((a, b) => b.total_contributions - a.total_contributions);
console.log(
`${topContributors.length} contributors above threshold of ${this.#contributionThreshold} contributions`
);
console.log('saving');
this.#writeData(topContributors);
}
#newContributor({ avatar_url, login }: { avatar_url: string; login: string }): Contributor {
return {
login,
avatar_url,
total_contributions: 0,
};
}
async #getRepos() {
return (
await this.#app.request(`GET /orgs/{org}/repos`, {
org: this.#org,
type: 'sources',
})
).data.filter((repo) => !repo.private);
}
async #getAllIssuesAndPRs(repo: string) {
console.log(`fetching issues and PRs for ${this.#org}/${repo}`);
const issues = await this.#app.paginate('GET /repos/{owner}/{repo}/issues', {
owner: this.#org,
repo,
per_page: 100,
state: 'all',
});
console.log(`found ${issues.length} issues and PRs for ${this.#org}/${repo}`);
return issues;
}
async #getAllReviewComments(repo: string) {
console.log(`fetching PR review comments for ${this.#org}/${repo}`);
const reviews = await this.#app.paginate('GET /repos/{owner}/{repo}/pulls/comments', {
owner: this.#org,
repo,
per_page: 100,
});
console.log(`found ${reviews.length} PR review comments for ${this.#org}/${repo}`);
return reviews;
}
async #getAllReviews(repo: string) {
console.log(`fetching PR reviews for ${this.#org}/${repo}`);
const {
repository: {
pullRequests: { nodes: pullRequests },
},
} = await this.#app.graphql.paginate<{
repository: {
pullRequests: {
pageInfo: PageInfoForward;
nodes: Array<{
number: number;
labels: { nodes: Array<{ name: string }> };
latestReviews: {
nodes: Array<{ author: null | { login: string; avatarUrl: string } }>;
};
}>;
};
};
}>(
`
query ($org: String!, $repo: String!, $cursor: String) {
repository(owner: $org, name: $repo) {
pullRequests(first: 100, after: $cursor) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
latestReviews(first: 15) {
nodes {
author {
login
avatarUrl
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
{ org: this.#org, repo }
);
const reviews: Review[] = [];
for (const { number, labels, latestReviews } of pullRequests) {
for (const { author } of latestReviews.nodes) {
reviews.push({
prNumber: number,
labels: labels.nodes.map(({ name }) => name),
login: author?.login,
avatarUrl: author?.avatarUrl,
});
}
}
console.log(`found ${reviews.length} PR reviews for ${this.#org}/${repo}`);
return reviews;
}
async #getReposWithExtraStats() {
const repos = await this.#getRepos();
console.log(`found ${repos.length} repos`);
const reposWithStats: AugmentedRepo[] = [];
for (const repo of repos.slice(0, 2)) {
reposWithStats.push({
...repo,
issues: await this.#getAllIssuesAndPRs(repo.name),
reviewComments: await this.#getAllReviewComments(repo.name),
reviews: await this.#getAllReviews(repo.name),
});
}
return reposWithStats;
}
#writeData(data: Contributor[]) {
return writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
}
}

View File

@@ -0,0 +1,39 @@
import { GH_IMAGE_DIMENSION } from './config';
import type { GitHubSponsor } from './types';
import { q } from './utils';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
export async function fetchGitHubSponsors() {
if (!GITHUB_TOKEN) {
console.error('GITHUB_TOKEN not set');
return [];
}
// https://docs.github.com/graphql
const query = `query {
organization(login:"tauri-apps") {
sponsors(first: 100) {
nodes {
... on Actor {
login,
avatarUrl(size: ${GH_IMAGE_DIMENSION})
}
}
}
}
}`;
const data = await q(query, 'https://api.opencollective.com/graphql/v2', 'Open Collective', {
Authorization: `bearer ${GITHUB_TOKEN}`,
});
return data.organization.sponsors.nodes
.map(
(node: any): GitHubSponsor => ({
name: node.login,
avatarUrl: node.avatarUrl,
})
)
.sort((a: GitHubSponsor, b: GitHubSponsor) => a.name.localeCompare(b.name));
}

View File

@@ -0,0 +1,12 @@
import { GITHUB_SPONSORS_FILE, OPEN_COLLECTIVE_FILE } from './config';
import { fetchGitHubSponsors } from './githubSponsors';
import { fetchOpenCollectiveData } from './openCollective';
import { checkAndWriteData } from './utils';
async function main() {
await checkAndWriteData(OPEN_COLLECTIVE_FILE, fetchOpenCollectiveData);
await checkAndWriteData(GITHUB_SPONSORS_FILE, fetchGitHubSponsors);
// todo: contributors
}
main();

View File

@@ -1,12 +1,6 @@
import {
IMAGE_DIMENSION,
type Sponsor,
type Tier,
} from '@components/sponsors/OpenCollective/_types';
export const PLATINUM_THRESHOLD = 5_000;
export const GOLD_THRESHOLD = 500;
export const SILVER_THRESHOLD = 100;
import { GOLD_THRESHOLD, PLATINUM_THRESHOLD, SILVER_THRESHOLD, OC_IMAGE_DIMENSION } from './config';
import { type OpenCollectiveSponsor, type Tier } from './types';
import { q } from './utils';
export async function fetchOpenCollectiveData() {
const filteredSlugs = ['github-sponsors'];
@@ -19,7 +13,7 @@ export async function fetchOpenCollectiveData() {
account {
name
type
imageUrl(height: ${IMAGE_DIMENSION}, format: jpg)
imageUrl(height: ${OC_IMAGE_DIMENSION}, format: jpg)
slug
isIncognito
}
@@ -31,25 +25,10 @@ export async function fetchOpenCollectiveData() {
}
}
}`;
const res = await fetch('https://api.opencollective.com/graphql/v2', {
method: 'POST',
body: JSON.stringify({ query }),
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw Error(
`Open Collective query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
);
}
const data = await q(query, 'https://api.opencollective.com/graphql/v2', 'Open Collective');
// TODO: handle currency
const openCollectiveData = (await res.json()).data;
return openCollectiveData.collective.contributors.nodes
return data.collective.contributors.nodes
.filter(
(node: any) =>
!node.account.isIncognito &&
@@ -58,7 +37,7 @@ export async function fetchOpenCollectiveData() {
node.account.name != 'Guest'
)
.sort((a: any, b: any) => b.totalAmountContributed.value - a.totalAmountContributed.value)
.map((node: any): Sponsor => {
.map((node: any): OpenCollectiveSponsor => {
let tier: Tier;
let amount = node.totalAmountContributed.value;
if (amount >= PLATINUM_THRESHOLD) {

View File

@@ -0,0 +1,25 @@
{
"name": "fetch-sponsors",
"version": "1.0.0",
"private": "true",
"type": "module",
"description": "",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"packageManager": "pnpm@10.13.1",
"dependencies": {
"@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-graphql": "^6.0.0",
"@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-retry": "^8.0.1",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/types": "^14.1.0",
"@types/node": "^22.15.21",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -1,4 +1,10 @@
export type Sponsor = {
export type GitHubSponsor = {
name: string;
avatarUrl: string;
profileUrl?: string;
};
export type OpenCollectiveSponsor = {
id: string;
name: string;
avatarUrl: string;
@@ -8,4 +14,3 @@ export type Sponsor = {
};
export type Tier = 'platinum' | 'gold' | 'silver' | 'bronze';
export const IMAGE_DIMENSION = 256;

View File

@@ -0,0 +1,33 @@
import fs from 'node:fs';
export async function q(query: string, url: string, name: string, headers?: any) {
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify({ query }),
headers: {
'Content-Type': 'application/json',
...headers,
},
});
if (!res.ok) {
throw Error(
`${name} query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
);
}
const data = (await res.json()).data;
return data;
}
// TODO: override on prod
export async function checkAndWriteData(filePath: string, fetcher: any) {
let data = [];
if (fs.existsSync(filePath)) {
data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} else {
data = await fetcher();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
return data;
}

164
pnpm-lock.yaml generated
View File

@@ -120,6 +120,33 @@ importers:
specifier: ^5.3.3
version: 5.5.4
packages/fetch-sponsors:
dependencies:
'@octokit/core':
specifier: ^7.0.3
version: 7.0.3
'@octokit/plugin-paginate-graphql':
specifier: ^6.0.0
version: 6.0.0(@octokit/core@7.0.3)
'@octokit/plugin-paginate-rest':
specifier: ^13.1.1
version: 13.1.1(@octokit/core@7.0.3)
'@octokit/plugin-retry':
specifier: ^8.0.1
version: 8.0.1(@octokit/core@7.0.3)
'@octokit/plugin-throttling':
specifier: ^11.0.1
version: 11.0.1(@octokit/core@7.0.3)
'@octokit/types':
specifier: ^14.1.0
version: 14.1.0
'@types/node':
specifier: ^22.15.21
version: 22.15.21
typescript:
specifier: ^5.8.3
version: 5.8.3
packages/js-api-generator:
dependencies:
github-slugger:
@@ -1163,6 +1190,60 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@octokit/auth-token@6.0.0':
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
engines: {node: '>= 20'}
'@octokit/core@7.0.3':
resolution: {integrity: sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==}
engines: {node: '>= 20'}
'@octokit/endpoint@11.0.0':
resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==}
engines: {node: '>= 20'}
'@octokit/graphql@9.0.1':
resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==}
engines: {node: '>= 20'}
'@octokit/openapi-types@25.1.0':
resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==}
'@octokit/plugin-paginate-graphql@6.0.0':
resolution: {integrity: sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-paginate-rest@13.1.1':
resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-retry@8.0.1':
resolution: {integrity: sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=7'
'@octokit/plugin-throttling@11.0.1':
resolution: {integrity: sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': ^7.0.0
'@octokit/request-error@7.0.0':
resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==}
engines: {node: '>= 20'}
'@octokit/request@10.0.3':
resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==}
engines: {node: '>= 20'}
'@octokit/types@14.1.0':
resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==}
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
@@ -1754,12 +1835,18 @@ packages:
bcp-47@2.1.0:
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
before-after-hook@4.0.0:
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
blob-to-buffer@1.2.9:
resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
boxen@8.0.1:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
@@ -2241,6 +2328,9 @@ packages:
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
fast-content-type-parse@3.0.0:
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -3732,6 +3822,9 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -5234,6 +5327,69 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@octokit/auth-token@6.0.0': {}
'@octokit/core@7.0.3':
dependencies:
'@octokit/auth-token': 6.0.0
'@octokit/graphql': 9.0.1
'@octokit/request': 10.0.3
'@octokit/request-error': 7.0.0
'@octokit/types': 14.1.0
before-after-hook: 4.0.0
universal-user-agent: 7.0.3
'@octokit/endpoint@11.0.0':
dependencies:
'@octokit/types': 14.1.0
universal-user-agent: 7.0.3
'@octokit/graphql@9.0.1':
dependencies:
'@octokit/request': 10.0.3
'@octokit/types': 14.1.0
universal-user-agent: 7.0.3
'@octokit/openapi-types@25.1.0': {}
'@octokit/plugin-paginate-graphql@6.0.0(@octokit/core@7.0.3)':
dependencies:
'@octokit/core': 7.0.3
'@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.3)':
dependencies:
'@octokit/core': 7.0.3
'@octokit/types': 14.1.0
'@octokit/plugin-retry@8.0.1(@octokit/core@7.0.3)':
dependencies:
'@octokit/core': 7.0.3
'@octokit/request-error': 7.0.0
'@octokit/types': 14.1.0
bottleneck: 2.19.5
'@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.3)':
dependencies:
'@octokit/core': 7.0.3
'@octokit/types': 14.1.0
bottleneck: 2.19.5
'@octokit/request-error@7.0.0':
dependencies:
'@octokit/types': 14.1.0
'@octokit/request@10.0.3':
dependencies:
'@octokit/endpoint': 11.0.0
'@octokit/request-error': 7.0.0
'@octokit/types': 14.1.0
fast-content-type-parse: 3.0.0
universal-user-agent: 7.0.3
'@octokit/types@14.1.0':
dependencies:
'@octokit/openapi-types': 25.1.0
'@oslojs/encoding@1.1.0': {}
'@pagefind/darwin-arm64@1.3.0':
@@ -5879,10 +6035,14 @@ snapshots:
is-alphanumerical: 2.0.1
is-decimal: 2.0.1
before-after-hook@4.0.0: {}
blob-to-buffer@1.2.9: {}
boolbase@1.0.0: {}
bottleneck@2.19.5: {}
boxen@8.0.1:
dependencies:
ansi-align: 3.0.1
@@ -6376,6 +6536,8 @@ snapshots:
extend@3.0.2: {}
fast-content-type-parse@3.0.0: {}
fast-deep-equal@3.1.3: {}
fast-glob@3.3.2:
@@ -8492,6 +8654,8 @@ snapshots:
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
universal-user-agent@7.0.3: {}
universalify@2.0.1: {}
unstorage@1.16.1:

View File

@@ -1,7 +1,7 @@
---
// TODO
import { readFileSync } from 'fs';
import { DATA_FILE, StatsCollector } from '../../../utils/fetchGitHubContributorsData';
import { readFileSync } from 'node:fs';
import { DATA_FILE, StatsCollector } from 'packages/fetch-sponsors/fetchGitHubContributorsData';
// todo: existsSync check or do everything on the script

View File

@@ -1,122 +0,0 @@
---
// todo: share this with open collective or at least move to fetchGitHubSponsorsData.ts
import { Image } from 'astro:assets';
const GITHUB_TOKEN = import.meta.env.GITHUB_TOKEN;
type Sponsor = {
name: string;
avatarUrl: string;
profileUrl?: string;
};
const IMAGE_DIMENSION = 64;
const gitHubSponsors = await getGitHubSponsors();
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
async function getGitHubSponsors(): Promise<any[]> {
if (!GITHUB_TOKEN) {
console.error('GITHUB_TOKEN not set');
return [];
}
// https://docs.github.com/graphql
const gitHubQuery = `query {
organization(login:"tauri-apps") {
sponsors(first: 100) {
nodes {
... on Actor {
login,
avatarUrl(size: ${IMAGE_DIMENSION})
}
}
}
}
}`;
const res = await fetch('https://api.github.com/graphql', {
method: 'POST',
body: JSON.stringify({ query: gitHubQuery }),
headers: {
Authorization: `bearer ${GITHUB_TOKEN}`,
},
});
if (!res.ok) {
throw Error(
`gh query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
);
}
const gitHubSponsorData = (await res.json()).data;
return gitHubSponsorData.organization.sponsors.nodes
.map(
(node: any): Sponsor => ({
name: node.login,
avatarUrl: node.avatarUrl,
})
)
.sort((a: Sponsor, b: Sponsor) => a.name.localeCompare(b.name));
}
---
{
gitHubSponsorsLoaded && (
<div class="container">
{gitHubSponsors.map((sponsor) => (
<div>
{sponsor.avatarUrl && (
<a
href={`https://github.com/${sponsor.name}`}
target="_blank"
rel="noopener noreferrer"
>
<Image class="avatar" inferSize src={sponsor.avatarUrl} alt={sponsor.name} />
</a>
)}
</div>
))}
</div>
)
}
<style>
/* todo: extract shared css */
:root {
--overlap: 16px;
}
.container {
display: flex;
flex-wrap: wrap;
}
.container a {
display: block;
}
.container {
margin-inline-start: var(--overlap);
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
margin-inline-start: calc(var(--overlap) * -1);
aspect-ratio: 1;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 1s ease;
will-change: transform;
position: relative;
}
.avatar:hover {
transform: scale(1.2);
z-index: 10;
}
</style>

View File

@@ -0,0 +1,74 @@
---
import { Image } from 'astro:assets';
import { existsSync, readFileSync } from 'node:fs';
import { GITHUB_SPONSORS_FILE } from 'packages/fetch-sponsors/config';
import type { GitHubSponsor } from 'packages/fetch-sponsors/types';
let gitHubSponsors = [];
if (existsSync(GITHUB_SPONSORS_FILE)) {
gitHubSponsors = JSON.parse(readFileSync(GITHUB_SPONSORS_FILE, 'utf-8'));
} else {
throw new Error('GitHub sponsors data file not found.');
}
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
---
{
gitHubSponsorsLoaded && (
<div class="container">
{gitHubSponsors.map((sponsor: GitHubSponsor) => (
<div>
{sponsor.avatarUrl && (
<a
href={`https://github.com/${sponsor.name}`}
target="_blank"
rel="noopener noreferrer"
>
<Image class="avatar" inferSize src={sponsor.avatarUrl} alt={sponsor.name} />
</a>
)}
</div>
))}
</div>
)
}
<style>
/* todo: extract shared css */
:root {
--overlap: 16px;
}
.container {
display: flex;
flex-wrap: wrap;
}
.container a {
display: block;
}
.container {
margin-inline-start: var(--overlap);
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
margin-inline-start: calc(var(--overlap) * -1);
aspect-ratio: 1;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 1s ease;
will-change: transform;
position: relative;
}
.avatar:hover {
transform: scale(1.2);
z-index: 10;
}
</style>

View File

@@ -1,23 +1,20 @@
---
import fs from 'fs';
import path from 'path';
import Contributor from './Contributor.astro';
import { existsSync, readFileSync } from 'node:fs';
import {
fetchOpenCollectiveData,
GOLD_THRESHOLD,
OPEN_COLLECTIVE_FILE,
PLATINUM_THRESHOLD,
GOLD_THRESHOLD,
SILVER_THRESHOLD,
} from '@utils/fetchOpenCollectiveData';
import type { Sponsor } from './_types';
} from 'packages/fetch-sponsors/config';
import { type OpenCollectiveSponsor as Sponsor } from 'packages/fetch-sponsors/types';
const DATA_FILE = path.resolve('./src/components/sponsors/_openCollectiveData.json');
import OcAvatar from './OcAvatar.astro';
let openCollectiveSponsors = [];
if (fs.existsSync(DATA_FILE)) {
openCollectiveSponsors = JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
if (existsSync(OPEN_COLLECTIVE_FILE)) {
openCollectiveSponsors = JSON.parse(readFileSync(OPEN_COLLECTIVE_FILE, 'utf-8'));
} else {
openCollectiveSponsors = await fetchOpenCollectiveData();
fs.writeFileSync(DATA_FILE, JSON.stringify(openCollectiveSponsors, null, 2));
throw new Error('Open Collective data file not found.');
}
const sponsorsByTier = {
@@ -32,10 +29,10 @@ const goldSponsors = sponsorsByTier.gold;
const silverSponsors = sponsorsByTier.silver;
// disabled
const bronzeSponsors = sponsorsByTier.bronze
.filter((sponsor: Sponsor) => sponsor.name !== 'Incognito')
.slice(0, 50);
const totalBronzeSponsors = sponsorsByTier.bronze.length;
// const bronzeSponsors = sponsorsByTier.bronze
// .filter((sponsor: Sponsor) => sponsor.name !== 'Incognito')
// .slice(0, 50);
// const totalBronzeSponsors = sponsorsByTier.bronze.length;
---
{
@@ -43,8 +40,8 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
<div class="tier-section">
<h4>Platinum Sponsors (${PLATINUM_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container platinum">
{platinumSponsors.map((sponsor) => (
<Contributor {sponsor} />
{platinumSponsors.map((sponsor: Sponsor) => (
<OcAvatar {sponsor} />
))}
</div>
</div>
@@ -56,8 +53,8 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
<div class="tier-section">
<h4>Gold Sponsors (${GOLD_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container gold">
{goldSponsors.map((sponsor) => (
<Contributor {sponsor} />
{goldSponsors.map((sponsor: Sponsor) => (
<OcAvatar {sponsor} />
))}
</div>
</div>
@@ -69,9 +66,9 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
<div class="tier-section">
<h4>Silver Sponsors (${SILVER_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container silver">
{silverSponsors.map((sponsor) => (
{silverSponsors.map((sponsor: Sponsor) => (
<div>
<Contributor {sponsor} />
<OcAvatar {sponsor} />
</div>
))}
</div>
@@ -85,7 +82,7 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
<h4>Bronze Sponsors</h4>
<div class="sponsor-list">
{bronzeSponsors.map((sponsor, index) => (
<Contributor {sponsor} needComma={index < bronzeSponsors.length - 1} />
<OcAvatar {sponsor} needComma={index < bronzeSponsors.length - 1} />
))}
{totalBronzeSponsors > bronzeSponsors.length && (
<span class="more-sponsors">

View File

@@ -1,10 +1,10 @@
---
import { Image } from 'astro:assets';
import { IMAGE_DIMENSION, type Sponsor } from './_types';
import { OC_IMAGE_DIMENSION } from 'packages/fetch-sponsors/config';
import { type OpenCollectiveSponsor as Sponsor } from 'packages/fetch-sponsors/types';
interface Props {
sponsor: Sponsor;
needComma?: boolean;
}
@@ -24,8 +24,8 @@ const roundingStyle: Record<Sponsor['type'], string> = {
loading={'lazy'}
src={sponsor.avatarUrl}
alt={sponsor.name}
width={IMAGE_DIMENSION}
height={IMAGE_DIMENSION}
width={OC_IMAGE_DIMENSION}
height={OC_IMAGE_DIMENSION}
class={`image ${sponsor.tier} ${roundingStyle[sponsor.type]}`}
/>
</a>

View File

@@ -1,6 +1,6 @@
---
import OpenCollective from './OpenCollective/Main.astro';
import GitHubSponsor from './GitHub/Main.astro';
import OpenCollective from './OpenCollective/Collective.astro';
import GitHubSponsor from './GitHub/Sponsors.astro';
import ServiceProvider from './ServiceProviders/Main.astro';
---