Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"generate:catalog": "tsx scripts/generate-example-catalog.ts",
"generate:stackblitz": "tsx scripts/build-stackblitz-files.ts",
"generate:releases": "tsx scripts/generate-releases.ts",
"generate:all": "pnpm generate:api && pnpm generate:screenshots && pnpm generate:catalog && pnpm generate:stackblitz"
"generate:showcase": "tsx scripts/generate-showcase-screenshots.ts",
"generate:all": "pnpm generate:api && pnpm generate:screenshots && pnpm generate:catalog && pnpm generate:stackblitz && pnpm generate:showcase"
},
"devDependencies": {
"@content-collections/core": "^0.14.0",
Expand Down
101 changes: 101 additions & 0 deletions docs/scripts/generate-showcase-screenshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Showcase Screenshot Generator
*
* Generates WebP screenshots for featured and supporter sites
* listed in the showcase page, named by slug (e.g. tenzir.webp).
*
* Sites with noregenerate: true are only captured if no image exists yet.
*
* @usage
* pnpm tsx scripts/generate-showcase-screenshots.ts
*/

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { chromium } from 'playwright';
import sharp from 'sharp';
import { featuredSites, sponsorSites } from '../src/routes/docs/showcase/showcase.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const OUTPUT_DIR = path.resolve(__dirname, '../static/showcase');

const VIEWPORT = { width: 800, height: 1600 };

function toSlug(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
}

async function main() {
console.log('Showcase Screenshot Generator');
console.log('==============================\n');

if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

const sites = [...featuredSites, ...sponsorSites];

const browser = await chromium.launch();
const context = await browser.newContext({ viewport: VIEWPORT });
const page = await context.newPage();
page.setDefaultTimeout(30000);

let generated = 0;
let skipped = 0;
let errors = 0;

for (const site of sites) {
const name = site.name ?? site.reponame ?? '';
const slug = toSlug(name);
const outputPath = path.join(OUTPUT_DIR, `${slug}.webp`);

if (site.noregenerate && fs.existsSync(outputPath)) {
console.log(`⊘ Skipped (noregenerate): ${slug}.webp`);
skipped++;
continue;
}

try {
const captureUrl = site.showcaseurl ?? site.homepageurl;
if (!captureUrl) {
console.log(`⊘ Skipped (no homepage): ${slug}.webp`);
skipped++;
continue;
}
console.log(`Capturing ${name} → ${slug}.webp`);
try {
await page.goto(captureUrl, { waitUntil: 'networkidle' });
} catch {
// Fallback for sites that never reach networkidle
await page.goto(captureUrl, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
}

const pngBuffer = await page.screenshot({ fullPage: false });

await sharp(pngBuffer).webp({ quality: 80 }).toFile(outputPath);

console.log(` ✓ Saved ${slug}.webp`);
generated++;
} catch (error) {
console.error(` ✗ Failed ${name}:`, error instanceof Error ? error.message : error);
errors++;
}
}

await browser.close();

console.log('\n==============================');
console.log(`Generated: ${generated}`);
console.log(`Skipped: ${skipped}`);
console.log(`Errors: ${errors}`);
console.log(`\nImages saved to ${OUTPUT_DIR}`);
}

main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
198 changes: 198 additions & 0 deletions docs/src/lib/contribspons.remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { query, getRequestEvent } from '$app/server';
import { env } from '$env/dynamic/private';
import { api, graphql } from './api';
import { SPONSOR_OVERRIDES, TIER_LEVELS } from '../routes/docs/showcase/dependents-overrides';

type Person = {
username: string;
avatar: string;
link?: string;
contributions?: number;
monthlyAmount?: number;
};
type SponsorTier = { sponsor: Person[]; level: string };

export const getContribSponsors = query(async () => {
const { fetch } = getRequestEvent();

const githubHeaders: Record<string, string> = {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'LayerChart docs'
};
if (env.GITHUB_API_TOKEN) {
const prefix = env.GITHUB_API_TOKEN.startsWith('ghp_') ? 'token' : 'Bearer';
githubHeaders['Authorization'] = `${prefix} ${env.GITHUB_API_TOKEN}`;
}

const [contributors, sponsors] = await Promise.all([
fetchContributors(fetch, githubHeaders),
fetchSponsors(fetch, githubHeaders)
]);

return { contributors, sponsors, TIER_LEVELS };
});

async function fetchContributors(
fetch: typeof globalThis.fetch,
headers: Record<string, string>
): Promise<Person[]> {
const contributors: Person[] = [];
let page = 1;

while (true) {
const data = await api<any[]>(
'https://api.github.com',
'repos/techniq/layerchart/contributors',
{ fetch, headers, data: { per_page: '100', page: String(page) } }
);

if (!data || data.length === 0) break;

for (const c of data) {
if (c.type === 'User') {
contributors.push({
username: c.login,
avatar: c.avatar_url,
link: c.html_url,
contributions: c.contributions
});
}
}

if (data.length < 100) break;
page++;
}

return contributors;
}

async function fetchSponsors(
fetch: typeof globalThis.fetch,
headers: Record<string, string>
): Promise<SponsorTier[]> {
if (!env.GITHUB_API_TOKEN) {
console.warn('No GITHUB_API_TOKEN set, skipping sponsors fetch');
return [];
}

type SponsorshipNode = {
sponsorEntity: { login: string; avatarUrl: string; url: string } | null;
tier: { name: string; monthlyPriceInDollars: number } | null;
isActive: boolean;
};
type FlatSponsorNode = { login: string; avatarUrl: string; url: string } | null;

const data = await graphql<{
user: {
sponsorshipsAsMaintainer?: { nodes: SponsorshipNode[] };
sponsors: { nodes: FlatSponsorNode[] };
};
}>(
'https://api.github.com/graphql',
`
query {
user(login: "techniq") {
sponsorshipsAsMaintainer(first: 100, activeOnly: false) {
nodes {
sponsorEntity {
... on User {
login
avatarUrl
url
}
... on Organization {
login
avatarUrl
url
}
}
tier {
name
monthlyPriceInDollars
}
isActive
}
}
sponsors(first: 100) {
nodes {
... on User {
login
avatarUrl
url
}
... on Organization {
login
avatarUrl
url
}
}
}
}
}
`,
{},
{ fetch, headers }
);

if (!data?.user) {
console.warn('Sponsors query returned no user data');
return [];
}

const tieredNodes = data.user.sponsorshipsAsMaintainer?.nodes;
if (tieredNodes && tieredNodes.length > 0) {
console.log(
'Raw sponsors:',
tieredNodes.map((n) => ({
login: n.sponsorEntity?.login,
tier: n.tier?.name,
price: n.tier?.monthlyPriceInDollars,
active: n.isActive
}))
);

const tierMap = new Map<string, Person[]>();
for (const level of [...TIER_LEVELS.map((t) => t.level), 'Past Sponsors']) {
tierMap.set(level, []);
}

for (const node of tieredNodes) {
if (!node.sponsorEntity) continue;

const login = node.sponsorEntity.login;
const price = node.tier?.monthlyPriceInDollars ?? 0;

const person: Person = {
username: login,
avatar: node.sponsorEntity.avatarUrl,
link: node.sponsorEntity.url,
monthlyAmount: price || undefined
};

if (!node.isActive) {
tierMap.get('Past Sponsors')!.push(person);
continue;
}

const level =
SPONSOR_OVERRIDES[login.toLowerCase()]?.tierOverride ??
TIER_LEVELS.find((t) => price >= t.min)?.level ??
'Backers Level';
tierMap.get(level)!.push(person);
}

return [...tierMap.entries()]
.filter(([, sponsors]) => sponsors.length > 0)
.map(([level, sponsor]) => ({ level, sponsor }));
}

const flatNodes = data.user.sponsors?.nodes;
if (!flatNodes || flatNodes.length === 0) return [];

const sponsors: Person[] = flatNodes
.filter((n): n is NonNullable<FlatSponsorNode> => n != null && n.login != null)
.map((n) => ({ username: n.login, avatar: n.avatarUrl, link: n.url }));

if (sponsors.length === 0) return [];
return [{ level: 'Backers Level', sponsor: sponsors }];
}
12 changes: 6 additions & 6 deletions docs/src/routes/docs/showcase/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
import Showcase from './Showcase.svelte';
import { getDependents } from './dependency.remote';

const { featuredSites, supporterSites, popularSites, otherSites } = await getDependents();
const { featuredSites, sponsorSites, popularSites, otherSites } = await getDependents();
</script>

# Showcase

## Supporters
## Sponsors

<Showcase sites={supporterSites} />
<Showcase sites={sponsorSites} hero />

## Featured
[Become a sponsor](https://github.com/techniq/layerchart?tab=readme-ov-file#sponsors)

<Showcase sites={featuredSites} />
## Featured

[Become a sponsor](https://github.com/techniq/layerchart?tab=readme-ov-file#sponsors)
<Showcase sites={featuredSites} hero />

## Popular

Expand Down
Loading
Loading