This commit is contained in:
Ayres Vitor
2025-07-29 17:16:19 -03:00
parent effe332035
commit 39e3c75e3e
13 changed files with 572 additions and 269 deletions

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ pnpm-debug.log*
# macOS-specific files
.DS_Store
_openCollectiveData.json

View File

@@ -1,66 +0,0 @@
---
import { Image } from 'astro:assets';
interface Props {
sponsor: Sponsor;
}
export type Sponsor = {
id: string;
name: string;
avatarUrl: string;
profileUrl?: string;
tier?: Tier;
};
export type Tier = 'platinum' | 'gold' | 'silver' | 'bronze';
export const IMAGE_DIMENSION = 128;
const { sponsor } = Astro.props;
---
{
sponsor.tier == 'platinum' && (
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer">
<Image
src={sponsor.avatarUrl}
alt={sponsor.name}
width={IMAGE_DIMENSION}
height={IMAGE_DIMENSION}
class="image"
/>
</a>
)
}
{
sponsor.tier == 'gold' && (
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer">
<Image
src={sponsor.avatarUrl}
alt={sponsor.name}
width={IMAGE_DIMENSION}
height={IMAGE_DIMENSION}
class="image"
/>
</a>
)
}
{
sponsor.tier == 'silver' && (
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer">
{sponsor.name}
</a>
)
}
{sponsor.tier == 'bronze' && sponsor.name}
<style define:vars={{ dimension: `${IMAGE_DIMENSION}px` }}>
.image {
width: var(--dimension);
aspect-ratio: 1;
}
</style>

View File

@@ -1,201 +0,0 @@
---
import { type Sponsor as SponsorType, IMAGE_DIMENSION, type Tier } from './Sponsor.astro';
import Sponsor from './Sponsor.astro';
const GITHUB_TOKEN = import.meta.env.GITHUB_TOKEN;
const IS_PRODUCTION = import.meta.env.NETLIFY != undefined;
let gitHubSponsors: SponsorType[] = [];
if (GITHUB_TOKEN || IS_PRODUCTION) {
gitHubSponsors = await getGitHubSponsors();
}
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
const openCollectiveSponsors = await getOpenCollectiveSponsors();
async function getGitHubSponsors(): Promise<SponsorType[]> {
if (!GITHUB_TOKEN)
throw Error('Error generator sponsor list: GITHUB_TOKEN is invalid or not set');
// https://docs.github.com/graphql
const gitHubQuery = `query {
organization(login:"tauri-apps") {
sponsors(first: 100) {
nodes {
... on Actor {
login,
avatarUrl(size: ${IMAGE_DIMENSION})
}
}
}
}
}`;
const gitHubSponsorResponse = await fetch('https://api.github.com/graphql', {
method: 'POST',
body: JSON.stringify({ query: gitHubQuery }),
headers: {
Authorization: `bearer ${GITHUB_TOKEN}`,
},
});
if (!gitHubSponsorResponse.ok)
throw Error(
`There was an issue with the GitHub sponsors query: ${gitHubSponsorResponse.status}: ${gitHubSponsorResponse.statusText}`
);
const gitHubSponsorData = (await gitHubSponsorResponse.json()).data;
return gitHubSponsorData.organization.sponsors.nodes
.map(
(node: any): SponsorType => ({
id: node.login,
name: node.login,
avatarUrl: node.avatarUrl,
})
)
.sort((a: SponsorType, b: SponsorType) => a.name.localeCompare(b.name));
}
async function getOpenCollectiveSponsors(): Promise<SponsorType[]> {
const filteredSlugs = ['github-sponsors'];
// Documentation at https://graphql-docs-v2.opencollective.com/welcome
const openCollectiveQuery = `query account {
collective(slug: "tauri") {
contributors(limit: 1000) {
nodes {
name
image(height: ${IMAGE_DIMENSION})
totalAmountDonated
collectiveSlug
isIncognito
}
}
}
}`;
const openCollectiveResponse = await fetch('https://api.opencollective.com/graphql/v2', {
method: 'POST',
body: JSON.stringify({ query: openCollectiveQuery }),
headers: {
'Content-Type': 'application/json',
},
});
if (!openCollectiveResponse.ok)
throw Error(
`There was an issue with the Open Collective sponsors query: ${openCollectiveResponse.status} ${openCollectiveResponse.statusText}`
);
const openCollectiveData = (await openCollectiveResponse.json()).data;
return openCollectiveData.collective.contributors.nodes
.filter(
(node: any) =>
!node.isIncognito &&
node.totalAmountDonated > 0 &&
!filteredSlugs.includes(node.collectiveSlug) &&
node.name != 'Guest'
)
.sort((a: any, b: any) => b.totalAmountDonated - a.totalAmountDonated)
.map((node: any): SponsorType => {
let tier: Tier;
let amount = node.totalAmountDonated / 100;
if (amount >= 5_000) {
tier = 'platinum';
} else if (amount >= 500) {
tier = 'gold';
} else if (amount >= 100) {
tier = 'silver';
} else {
tier = 'bronze';
}
return {
name: node.name,
id: node.name,
avatarUrl: node.image,
profileUrl: `https://opencollective.com/${node.collectiveSlug}`,
tier,
};
});
}
const openCollectiveSilverOpt1 = openCollectiveSponsors.filter(
(sponsor) => sponsor.tier == 'silver'
);
const openCollectiveSilverOpt2 = openCollectiveSponsors
.filter((sponsor) => sponsor.tier == 'silver')
.map((sponsor) => sponsor.name)
.join(', ');
const openCollectiveBronze = openCollectiveSponsors
.filter((sponsor) => sponsor.tier == 'bronze')
.map((sponsor) => sponsor.name)
.join(', ');
---
<h1 id="sponsors">Sponsors</h1>
<h2>Open Collective</h2>
<div class="sponsor-grid">
{
openCollectiveSponsors
.filter((sponsor) => sponsor.tier == 'platinum')
.map((sponsor) => <Sponsor {sponsor} />)
}
</div>
<div class="sponsor-grid">
{
openCollectiveSponsors
.filter((sponsor) => sponsor.tier == 'gold')
.map((sponsor) => <Sponsor {sponsor} />)
}
</div>
<!-- <div>
{
openCollectiveSilverOpt1.map((sponsor) => (
<>
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer">
{sponsor.name}
</a>{' '}
</>
))
}
</div> -->
<div>
{openCollectiveSilverOpt2}
</div>
<!-- <div>
{openCollectiveBronze}
</div> -->
<h2>GitHub</h2>
{
!gitHubSponsorsLoaded && (
<p>
<code>GITHUB_TOKEN</code> environment variable not set so GitHub sponsors could not be loaded.
</p>
)
}
{
gitHubSponsorsLoaded && (
<ul class="sponsor-grid">
{gitHubSponsors.map((sponsor) => (
<li>
<Sponsor {sponsor} />
</li>
))}
</ul>
)
}
<style>
.sponsor-grid {
display: flex;
flex-wrap: wrap;
gap: 0 1rem;
}
.sponsor-grid > li {
list-style: none;
}
</style>

View File

@@ -0,0 +1,79 @@
---
const GITHUB_TOKEN = import.meta.env.GITHUB_TOKEN;
let gitHubSponsors = [];
export type Sponsor = {
id: string;
name: string;
avatarUrl: string;
profileUrl?: string;
tier?: Tier;
};
export const IMAGE_DIMENSION = 64;
const IS_PRODUCTION = import.meta.env.NETLIFY != undefined;
export type Tier = 'platinum' | 'gold' | 'silver' | 'bronze';
if (GITHUB_TOKEN || IS_PRODUCTION) {
gitHubSponsors = await getGitHubSponsors();
}
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
async function getGitHubSponsors(): Promise<any[]> {
if (!GITHUB_TOKEN)
throw Error('Error generator sponsor list: GITHUB_TOKEN is invalid or not set');
// https://docs.github.com/graphql
const gitHubQuery = `query {
organization(login:"tauri-apps") {
sponsors(first: 100) {
nodes {
... on Actor {
login,
avatarUrl(size: ${IMAGE_DIMENSION})
}
}
}
}
}`;
const gitHubSponsorResponse = await fetch('https://api.github.com/graphql', {
method: 'POST',
body: JSON.stringify({ query: gitHubQuery }),
headers: {
Authorization: `bearer ${GITHUB_TOKEN}`,
},
});
if (!gitHubSponsorResponse.ok)
throw Error(
`There was an issue with the GitHub sponsors query: ${gitHubSponsorResponse.status}: ${gitHubSponsorResponse.statusText}`
);
const gitHubSponsorData = (await gitHubSponsorResponse.json()).data;
return gitHubSponsorData.organization.sponsors.nodes
.map(
(node: any): Sponsor => ({
id: node.login,
name: node.login,
avatarUrl: node.avatarUrl,
})
)
.sort((a: Sponsor, b: Sponsor) => a.name.localeCompare(b.name));
}
---
{!gitHubSponsorsLoaded && <p>_error_loading_</p>}
{
gitHubSponsorsLoaded && (
<div class="sponsor-grid github">
{gitHubSponsors.map((sponsor) => (
<div class="sponsor" />
))}
</div>
)
}

View File

@@ -0,0 +1,122 @@
---
import { Image } from 'astro:assets';
import { IMAGE_DIMENSION, type Sponsor } from './_types';
interface Props {
sponsor: Sponsor;
needComma?: boolean;
}
const { sponsor, needComma } = Astro.props;
const roundingStyle: Record<Sponsor['type'], string> = {
ORGANIZATION: 'rounded-lg',
INDIVIDUAL: 'rounded-full',
};
---
{
(sponsor.tier === 'platinum' || sponsor.tier === 'gold' || sponsor.tier === 'silver') && (
<div class="image-container">
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer">
<Image
src={sponsor.avatarUrl}
alt={sponsor.name}
width={IMAGE_DIMENSION}
height={IMAGE_DIMENSION}
class={`image ${sponsor.tier} ${roundingStyle[sponsor.type]}`}
/>
</a>
</div>
)
}
{
!sponsor.tier && sponsor.avatarUrl && (
<div class="image-container">
fallback
<a
href={sponsor.profileUrl || `https://github.com/${sponsor.name}`}
target="_blank"
rel="noopener noreferrer"
>
<Image
src={sponsor.avatarUrl}
alt={sponsor.name}
width={IMAGE_DIMENSION}
height={IMAGE_DIMENSION}
class={`image ${roundingStyle[sponsor.type]}`}
/>
</a>
</div>
)
}
{
sponsor.tier === 'bronze' && (
<>
<a href={sponsor.profileUrl} target="_blank" rel="noopener noreferrer" class="bronze-sponsor">
{sponsor.name}
</a>
{needComma && <span class="bronze-separator">, </span>}
</>
)
}
<style define:vars={{ dimension: `${IMAGE_DIMENSION}px` }}>
.rounded-full {
border-radius: 50%;
}
.rounded-lg {
border-radius: 8px;
}
.image {
object-fit: cover;
background-color: white;
border: 2px solid var(--sl-color-gray-1);
}
.image-container {
aspect-ratio: 1;
width: fit-content;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 1s ease;
}
.image-container:hover {
transform: scale(1.1);
}
.platinum {
width: 8rem;
height: 8rem;
}
.gold {
width: 6rem;
height: 6rem;
}
.silver {
width: 4rem;
height: 4rem;
}
.bronze-sponsor {
filter: brightness(0.8);
text-decoration: none;
}
.bronze-sponsor:hover {
filter: brightness(1.2);
text-decoration: underline;
}
.bronze-separator {
color: var(--sl-color-text-muted);
padding-inline-end: 2px;
}
</style>

View File

@@ -0,0 +1,113 @@
---
import fs from 'fs';
import path from 'path';
import Contributor from './Contributor.astro';
import {
fetchOpenCollectiveData,
GOLD_THRESHOLD,
PLATINUM_THRESHOLD,
SILVER_THRESHOLD,
} from '@utils/fetchOpenCollectiveData';
import type { Sponsor } from './_types';
const DATA_FILE = path.resolve('./src/components/sponsors/_openCollectiveData.json');
let openCollectiveSponsors = [];
if (fs.existsSync(DATA_FILE)) {
openCollectiveSponsors = JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
} else {
openCollectiveSponsors = await fetchOpenCollectiveData();
fs.writeFileSync(DATA_FILE, JSON.stringify(openCollectiveSponsors, null, 2));
}
const sponsorsByTier = {
platinum: openCollectiveSponsors.filter((sponsor: Sponsor) => sponsor.tier === 'platinum'),
gold: openCollectiveSponsors.filter((sponsor: Sponsor) => sponsor.tier === 'gold'),
silver: openCollectiveSponsors.filter((sponsor: Sponsor) => sponsor.tier === 'silver'),
bronze: openCollectiveSponsors.filter((sponsor: Sponsor) => sponsor.tier === 'bronze'),
};
const platinumSponsors = sponsorsByTier.platinum;
const goldSponsors = sponsorsByTier.gold;
const silverSponsors = sponsorsByTier.silver;
const bronzeSponsors = sponsorsByTier.bronze;
---
{
platinumSponsors.length > 0 && (
<div class="tier-section">
<h4>Platinum Sponsors (${PLATINUM_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container platinum">
{platinumSponsors.map((sponsor) => (
<Contributor {sponsor} />
))}
</div>
</div>
)
}
{
goldSponsors.length > 0 && (
<div class="tier-section">
<h4>Gold Sponsors (${GOLD_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container gold">
{goldSponsors.map((sponsor) => (
<Contributor {sponsor} />
))}
</div>
</div>
)
}
{
silverSponsors.length > 0 && (
<div class="tier-section">
<h4>Silver Sponsors (${SILVER_THRESHOLD.toLocaleString()}+)</h4>
<div class="sponsor-container silver">
{silverSponsors.map((sponsor) => (
<Contributor {sponsor} />
))}
</div>
</div>
)
}
{
bronzeSponsors.length > 0 && (
<div class="tier-section">
<h4>Bronze Sponsors</h4>
<div class="sponsor-list">
{bronzeSponsors.map((sponsor, index) => (
<Contributor {sponsor} needComma={index < bronzeSponsors.length - 1} />
))}
</div>
</div>
)
}
<style>
.sponsor-container {
display: flex;
flex-wrap: wrap;
}
.sponsor-container.silver {
gap: 6px;
}
.sponsor-container.gold {
gap: 9px;
}
.sponsor-container.platinum {
gap: 12px;
}
.tier-section {
margin-bottom: 2rem;
}
h4 {
color: var(--sl-color-text-muted);
}
</style>

View File

@@ -0,0 +1,11 @@
export type Sponsor = {
id: string;
name: string;
avatarUrl: string;
profileUrl?: string;
tier?: Tier;
type: 'ORGANIZATION' | 'INDIVIDUAL';
};
export type Tier = 'platinum' | 'gold' | 'silver' | 'bronze';
export const IMAGE_DIMENSION = 256;

View File

@@ -0,0 +1,62 @@
---
import { Image } from 'astro:assets';
interface ServiceProvider {
name: string;
image?: string;
url: string;
}
const serviceProviders: ServiceProvider[] = [
{
name: 'Netlify',
url: 'https://www.netlify.com/',
},
{
name: 'Meilisearch',
url: 'https://www.meilisearch.com/',
},
];
---
{
serviceProviders.length > 0 && (
<div class="service-providers">
{serviceProviders.map((provider) => (
<div class="service-provider not-content">
<a href={provider.url} target="_blank" rel="noopener noreferrer">
{provider.name}
</a>
{provider.image && <Image src={provider.image} inferSize alt={provider.name} />}
</div>
))}
</div>
)
}
<style>
.service-providers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.service-provider {
padding: 1rem;
border: 4px solid var(--sl-color-gray-5);
border-radius: 8px;
background: var(--sl-color-bg-nav);
}
.service-provider a {
font-weight: 600;
color: var(--sl-color-text);
text-decoration: none;
display: block;
margin-bottom: 0.5rem;
}
.service-provider a:hover {
color: var(--sl-color-accent);
}
</style>

View File

@@ -0,0 +1,48 @@
---
import OpenCollective from './OpenCollective/Main.astro';
import GitHub from './GitHub/Main.astro';
import ServiceProvider from './ServiceProviders/Main.astro';
// const codeContributors = [
// // todo: Populate this list with actual code contributors, pull top 10 for each repo?
// ];
---
<div class="not-content">
<h2>Sponsors</h2>
<section class="sponsors-section">
<div class="funding-source">
<h4>GitHub</h4>
<GitHub />
</div>
<div class="funding-source">
<h4>Open Collective</h4>
<OpenCollective />
</div>
</section>
<section class="sponsors-section">
<h4>Partners</h4>
<!-- todo CrabNebula -->
<h4>Services</h4>
<ServiceProvider />
</section>
</div>
<style>
/* todo: add styles for headings h2, h3, h4 */
h4 {
/* border 50% opacity */
border-bottom: 1px solid var(--sl-color-gray-4);
}
.sponsors-section {
margin-bottom: 4rem;
}
.funding-source {
margin-bottom: 4rem;
}
</style>

View File

@@ -25,6 +25,8 @@ hero:
import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
import Cta from '@fragments/cta.mdx';
import SponsorList from '@components/sponsors/SponsorList.astro';
<div class="hero-bg">
<div class="bg-logo"></div>
@@ -67,6 +69,7 @@ import Cta from '@fragments/cta.mdx';
</Card>
</CardGrid> */}
import SponsorList from '@components/SponsorList.astro';
<SponsorList />

View File

@@ -0,0 +1,84 @@
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;
export async function fetchOpenCollectiveData() {
const filteredSlugs = ['github-sponsors'];
// Documentation at https://graphql-docs-v2.opencollective.com/welcome
const query = `query account {
collective(slug: "tauri") {
contributors(limit: 1000) {
nodes {
account {
name
type
imageUrl(height: ${IMAGE_DIMENSION})
slug
isIncognito
}
totalAmountContributed {
value
currency
}
}
}
}
}`;
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)}, `
);
}
// TODO: handle currency
const openCollectiveData = (await res.json()).data;
return openCollectiveData.collective.contributors.nodes
.filter(
(node: any) =>
!node.account.isIncognito &&
node.totalAmountContributed.value > 0 &&
!filteredSlugs.includes(node.account.slug) &&
node.account.name != 'Guest'
)
.sort((a: any, b: any) => b.totalAmountContributed.value - a.totalAmountContributed.value)
.map((node: any): Sponsor => {
let tier: Tier;
let amount = node.totalAmountContributed.value;
if (amount >= PLATINUM_THRESHOLD) {
tier = 'platinum';
} else if (amount >= GOLD_THRESHOLD) {
tier = 'gold';
} else if (amount >= SILVER_THRESHOLD) {
tier = 'silver';
} else {
tier = 'bronze';
}
const { slug, name, type, isIncognito, imageUrl } = node.account;
return {
name,
id: name,
avatarUrl: imageUrl,
profileUrl: `https://opencollective.com/${slug}`,
tier,
type,
};
});
}

46
tauri-docs.code-workspace Normal file
View File

@@ -0,0 +1,46 @@
{
"folders": [
{
"path": ".",
"name": "docs"
},
{
"path": "packages",
"name": "packages",
"folders": [
{
"path": "js-api-generator",
},
{
"path": "config-generator",
},
{
"path": "cli-generator",
},
{
"path": "releases-generator",
},
{
"path": "compatibility-table",
}
]
},
// todo: fix paths so that we can see docs, packages and submodules
{
"path": "packages",
"name": "submodules",
"folders": [
{
"path": "awesome-tauri",
},
{
"path": "tauri",
},
{
"path": "plugins-workspace",
}
]
}
],
"settings": {}
}

View File

@@ -5,7 +5,8 @@
"paths": {
"@components/*": ["src/components/*"],
"@assets/*": ["src/assets/*"],
"@fragments/*": ["src/content/docs/_fragments/*"]
"@fragments/*": ["src/content/docs/_fragments/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": [".astro/types.d.ts", "**/*"],