diff --git a/docs/src/lib/api.ts b/docs/src/lib/api.ts new file mode 100644 index 000000000..e8981e46c --- /dev/null +++ b/docs/src/lib/api.ts @@ -0,0 +1,86 @@ +import { parse } from '@layerstack/utils'; + +export type ApiOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + data?: any; + headers?: Record; + fetch?: typeof globalThis.fetch; + parse?(data: string): T; +}; + +export async function api( + origin: string, + resource: string, + options: ApiOptions = {} +): Promise { + let url = `${origin}/${resource}`; + const method = options?.method ?? 'GET'; + const _fetch = options?.fetch ?? globalThis.fetch; + + if (method === 'GET' && options?.data) { + url += `?${new URLSearchParams(options.data)}`; + } + + const response = await _fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...(method === 'POST' && + options?.data && { + body: JSON.stringify(options.data) + }) + }); + + const text = await response.text(); + + if (!response.ok) { + console.error(`API ${method} ${url} failed: ${response.status} ${response.statusText} - ${text}`); + return null; + } + + try { + return options.parse ? options.parse(text) : parse(text); + } catch { + console.error(`API ${method} ${url} returned invalid JSON: ${text.slice(0, 200)}`); + return null; + } +} + +export async function graphql( + endpoint: string, + query: string, + variables: Record = {}, + options: ApiOptions = {} +): Promise { + const _fetch = options?.fetch ?? globalThis.fetch; + + const response = await _fetch(endpoint, { + method: options?.method ?? 'POST', + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + body: JSON.stringify({ + query, + variables, + ...options.data + }) + }); + + const text = await response.text(); + + if (!response.ok) { + console.error(`GraphQL ${endpoint} failed: ${response.status} ${response.statusText} - ${text}`); + return null; + } + + try { + const json = options.parse ? options.parse(text) : parse(text); + return json.data as Data; + } catch { + console.error(`GraphQL ${endpoint} returned invalid JSON: ${text.slice(0, 200)}`); + return null; + } +} diff --git a/docs/src/lib/components/Stats.svelte b/docs/src/lib/components/Stats.svelte new file mode 100644 index 000000000..622ef7d0d --- /dev/null +++ b/docs/src/lib/components/Stats.svelte @@ -0,0 +1,88 @@ + + +{#await statsPromise} +
+{:then { npmDownloads, githubStars, discordMembers, bskyFollowers }} + {@const stats = [ + { + label: ' Downloads', + value: npmDownloads, + link: 'https://npmjs.com/package/layerchart', + intervals: ['Weekly', 'Monthly', 'Lifetime'] + }, + { label: 'GitHub Stars', value: githubStars, link: 'https://github.com/techniq/layerchart' }, + { label: 'Discord Members', value: discordMembers, link: 'https://discord.gg/697JhMPD3t' }, + { + label: 'Bluesky Followers', + value: bskyFollowers, + link: 'https://bsky.app/profile/techniq.dev' + } + ].filter((s) => s.value != null)} + + +{/await} diff --git a/docs/src/lib/stats.remote.ts b/docs/src/lib/stats.remote.ts new file mode 100644 index 000000000..486dc0ac5 --- /dev/null +++ b/docs/src/lib/stats.remote.ts @@ -0,0 +1,45 @@ +import { query, getRequestEvent } from '$app/server'; +import { env } from '$env/dynamic/private'; +import { api } from './api'; + +export const getStats = query(async () => { + const { fetch } = getRequestEvent(); + + const githubHeaders: Record = { Accept: 'application/vnd.github.v3+json' }; + if (env.GITHUB_API_TOKEN) { + const prefix = env.GITHUB_API_TOKEN.startsWith('ghp_') ? 'token' : 'Bearer'; + githubHeaders['Authorization'] = `${prefix} ${env.GITHUB_API_TOKEN}`; + } + + const [githubData, npmWeeklyData, npmMonthlyData, npmLifetimeData, discordData, bskyData] = + await Promise.all([ + api('https://api.github.com', 'repos/techniq/layerchart', { + fetch, + headers: githubHeaders + }), + api('https://api.npmjs.org', 'downloads/point/last-week/layerchart', { fetch }), + api('https://api.npmjs.org', 'downloads/point/last-month/layerchart', { fetch }), + api('https://api.npmjs.org', 'downloads/point/2020-01-01:2099-12-31/layerchart', { + fetch + }), + api('https://discord.com', 'api/v9/invites/697JhMPD3t?with_counts=true', { fetch }), + api('https://public.api.bsky.app', 'xrpc/app.bsky.actor.getProfile?actor=techniq.dev', { + fetch + }) + ]); + + const githubStars = (githubData?.stargazers_count as number) ?? null; + const npmWeekly = (npmWeeklyData?.downloads as number) ?? null; + const npmMonthly = (npmMonthlyData?.downloads as number) ?? null; + const npmLifetime = (npmLifetimeData?.downloads as number) ?? null; + const bskyFollowers = (bskyData?.followersCount as number) ?? null; + const discordMembers = (discordData?.approximate_member_count as number) ?? null; + + const npmDownloads: [number | null, number | null, number | null] = [ + npmWeekly, + npmMonthly, + npmLifetime + ]; + + return { githubStars, npmDownloads, bskyFollowers, discordMembers }; +}); diff --git a/docs/src/routes/+page.svelte b/docs/src/routes/+page.svelte index 9f3471465..81b094fe9 100644 --- a/docs/src/routes/+page.svelte +++ b/docs/src/routes/+page.svelte @@ -1,5 +1,6 @@