-
Notifications
You must be signed in to change notification settings - Fork 7
Community section in the nav with live GitHub and Discord counts #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e5f765d
Add a community section to the nav with live GitHub and Discord counts
kixelated 4973b32
Bypass the HTTP cache when fetching community counts
kixelated 4a0f4f8
Drop sidebar socials and use a hand-drawn underline
kixelated 56a3e8a
Validate cached community counts before rendering
kixelated File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| --- | ||
| // One entry in the sidebar: a hand-drawn icon that wiggles on hover, the | ||
| // matching hand-drawn word, and optionally a line of small print underneath. | ||
| // Both images live in public/layout/ as <icon>-icon.svg and <icon>-word.svg. | ||
| interface Props { | ||
| href: string; | ||
| icon: string; | ||
| word: string; | ||
| } | ||
|
|
||
| const { href, icon, word } = Astro.props; | ||
| --- | ||
|
|
||
| <a href={href} class="group flex items-center gap-2 w-32"> | ||
| <span class="flex w-9 justify-center shrink-0"> | ||
| <img src={`/layout/${icon}-icon.svg`} class="h-9 transition-transform group-hover:-rotate-6" alt="" /> | ||
| </span> | ||
| <span class="flex flex-col items-start"> | ||
| <img src={`/layout/${icon}-word.svg`} class="h-9 w-auto" alt={word} /> | ||
| <slot /> | ||
| </span> | ||
| </a> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Wherever you can find me. Rendered in the <Author /> sign-off at the bottom | ||
| // of each post (and the homepage). The icons in public/social/ are placeholders | ||
| // waiting on hand-drawn versions. | ||
| export const socials = [ | ||
| { name: "Email", icon: "email", href: "mailto:me@kixel.me" }, | ||
| { name: "X", icon: "x", href: "https://x.com/kixelated" }, | ||
| { name: "Bluesky", icon: "bluesky", href: "https://bsky.app/profile/kixel.me" }, | ||
| { name: "LinkedIn", icon: "linkedin", href: "https://www.linkedin.com/in/luke-curley-635a457/" }, | ||
| { name: "Discord", icon: "discord", href: "https://discord.moq.dev" }, | ||
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // Live community numbers for the nav: GitHub stars and Discord members. | ||
| // | ||
| // Fetched in the browser rather than at build time because deploys are manual | ||
| // and infrequent, so a baked-in count would be weeks stale. Both APIs allow | ||
| // anonymous CORS requests; GitHub's limit is 60/hour per IP, which is why the | ||
| // result is cached in localStorage for an hour instead of refetched per page. | ||
|
|
||
| const GITHUB = "https://api.github.com/repos/moq-dev/moq"; | ||
| const DISCORD = "https://discord.com/api/v10/invites/FCYF3p99mr?with_counts=true"; | ||
|
|
||
| const CACHE_KEY = "moq.stats"; | ||
| const CACHE_TTL = 60 * 60 * 1000; | ||
|
|
||
| interface Stats { | ||
| stars?: number; | ||
| chatters?: number; | ||
| } | ||
|
|
||
| interface Cached extends Stats { | ||
| at: number; | ||
| } | ||
|
|
||
| function asCount(value: unknown): number | undefined { | ||
| return typeof value === "number" && Number.isFinite(value) ? value : undefined; | ||
| } | ||
|
|
||
| function readCache(): Stats | undefined { | ||
| try { | ||
| const raw = localStorage.getItem(CACHE_KEY); | ||
| if (!raw) return; | ||
| const cached = JSON.parse(raw) as Cached; | ||
| if (typeof cached.at !== "number" || Date.now() - cached.at > CACHE_TTL) return; | ||
| const stars = asCount(cached.stars); | ||
| const chatters = asCount(cached.chatters); | ||
| if (stars === undefined && chatters === undefined) return; | ||
| return { stars, chatters }; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| function writeCache(stats: Stats) { | ||
| try { | ||
| localStorage.setItem(CACHE_KEY, JSON.stringify({ ...stats, at: Date.now() } satisfies Cached)); | ||
| } catch { | ||
| // Private mode, quota, etc. Not worth surfacing. | ||
| } | ||
| } | ||
|
|
||
| async function fetchNumber(url: string, key: string): Promise<number | undefined> { | ||
| try { | ||
| // Discord echoes the requesting origin in Access-Control-Allow-Origin but | ||
| // marks the response cacheable without `Vary: Origin`, so a response cached | ||
| // for moq.dev fails the CORS check on doc.moq.dev. Skip the HTTP cache; | ||
| // localStorage above is the cache. | ||
| const res = await fetch(url, { cache: "no-store" }); | ||
| if (!res.ok) return; | ||
| const json = await res.json(); | ||
| const value = json[key]; | ||
| return typeof value === "number" ? value : undefined; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| async function fetchStats(): Promise<Stats> { | ||
| const [stars, chatters] = await Promise.all([ | ||
| fetchNumber(GITHUB, "stargazers_count"), | ||
| fetchNumber(DISCORD, "approximate_member_count"), | ||
| ]); | ||
| return { stars, chatters }; | ||
| } | ||
|
|
||
| // Fills every element with a `data-stat="stars"` / `data-stat="chatters"` | ||
| // attribute, e.g. "1,515 stars". Elements stay empty if a fetch fails; the | ||
| // links around them still work. | ||
| export async function renderStats() { | ||
| const cached = readCache(); | ||
| const stats = cached ?? (await fetchStats()); | ||
| if (!cached && (stats.stars !== undefined || stats.chatters !== undefined)) writeCache(stats); | ||
|
|
||
| for (const el of document.querySelectorAll<HTMLElement>("[data-stat]")) { | ||
| const key = el.dataset.stat as keyof Stats; | ||
| const value = stats[key]; | ||
| if (typeof value === "number") el.textContent = `${value.toLocaleString()} ${key}`; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.