Skip to content
Merged

Stats #776

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
86 changes: 86 additions & 0 deletions docs/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { parse } from '@layerstack/utils';

export type ApiOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: any;
headers?: Record<string, string>;
fetch?: typeof globalThis.fetch;
parse?<T>(data: string): T;
};

export async function api<Data = any>(
origin: string,
resource: string,
options: ApiOptions = {}
): Promise<Data | null> {
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<Data>(text) : parse<Data>(text);
} catch {
console.error(`API ${method} ${url} returned invalid JSON: ${text.slice(0, 200)}`);
return null;
}
}

export async function graphql<Data = any>(
endpoint: string,
query: string,
variables: Record<string, any> = {},
options: ApiOptions = {}
): Promise<Data | null> {
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;
}
}
88 changes: 88 additions & 0 deletions docs/src/lib/components/Stats.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import { ScrollingValue } from 'svelte-ux';
import { IsInViewport, useInterval } from 'runed';
import { format } from '@layerstack/utils';
import { getStats } from '$lib/stats.remote';

const statsPromise = getStats();

let npmEl = $state<HTMLElement>();
const inViewport = new IsInViewport(() => npmEl);

const interval = useInterval(8000);
$effect(() => {
if (inViewport.current) {
interval.resume();
} else {
interval.pause();
}
});
</script>

{#await statsPromise}
<div
class="block sm:flex items-center justify-evenly px-1 py-2 rounded outline m-4 outline-surface-100 h-18 animate-pulse"
></div>
{: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)}

<div
class="block sm:flex items-center justify-evenly px-1 py-2 rounded-xl outline m-4 outline-surface-100"
>
{#each stats as { label, value, link, intervals }, index (value)}
<a
href={link}
target="_blank"
class="flex flex-col justify-center items-center rounded-xl border border-transparent hover:bg-surface-100/50 hover:border-primary/20 whitespace-nowrap"
>
{#if intervals}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="h-14 w-36 text-center pt-1"
bind:this={npmEl}
onpointerenter={() => interval.pause()}
onpointerleave={() => interval.resume()}
>
<ScrollingValue value={interval.counter} axis="y">
{@const intervalIndex = interval.counter % intervals.length}
<div class="text-lg font-bold text-surface-content">
{format(npmDownloads[intervalIndex]!, 'metric', {
fractionDigits: 1
}).toLowerCase() + '+'}
</div>
<div class="text-xs text-surface-content/60 text-center">
{intervals[intervalIndex]}
{label}
</div>
</ScrollingValue>
</div>
{:else}
<div class="flex flex-col items-center px-4 py-2 text-lg">
<span class="font-bold text-surface-content">
{format(value as number, 'metric', { fractionDigits: 1 }).toLowerCase() + '+'}
</span>
<span class="text-xs text-surface-content/60">{label}</span>
</div>
{/if}
</a>

{#if index < stats.length - 1}
<div class="sm:w-0.5 sm:h-12 bg-surface-100"></div>
{/if}
{/each}
</div>
{/await}
45 changes: 45 additions & 0 deletions docs/src/lib/stats.remote.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = { 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 };
});
4 changes: 4 additions & 0 deletions docs/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { Button, MenuButton, ThemeSelect, Tooltip } from 'svelte-ux';
import Stats from '$lib/components/Stats.svelte';
import { cls } from '@layerstack/tailwind';

import ExampleLink from '$lib/components/ExampleLink.svelte';
Expand All @@ -11,6 +12,7 @@
import CustomBluesky from '~icons/custom-brands/bluesky';
import CustomDiscord from '~icons/custom-brands/discord';


const links = [
{ label: 'Home', href: '/' },
{ label: 'Docs', href: '/docs' }
Expand Down Expand Up @@ -252,6 +254,8 @@
{/each}
</div>

<Stats />

<footer class="flex justify-between px-4 py-8 border-t text-surface-content/50 text-sm">
<div>
Made by <a href="https://github.com/techniq" target="_blank" class="text-surface-content">
Expand Down
Loading