diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index d01e5a2ad..6fdbd2a40 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -189,14 +189,13 @@ class GPU { cleanup(); return; } - if (wgpu::AdapterInfo info; - adapter.GetInfo(&info) != wgpu::Status::Success || - info.backendType == wgpu::BackendType::Null) { + if (wgpu::AdapterInfo info; adapter.GetInfo(&info) != wgpu::Status::Success || + info.backendType == wgpu::BackendType::Null) { IMG2NUM_LOG_INFO("No usable WebGPU adapter found. Falling back to CPU."); cleanup(); return; } - + // --------------------------------------------------------- // 2. Get Device // --------------------------------------------------------- diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 0b058d6be..20b2a9638 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -193,6 +193,7 @@ const config = { }, ], "./plugins/github-stats/index.js", + "./plugins/hallOfFame/index.js", [ "docusaurus-plugin-copy-page-button", { diff --git a/docs/lib/hallOfFame.js b/docs/lib/hallOfFame.js new file mode 100644 index 000000000..45c3a2f22 --- /dev/null +++ b/docs/lib/hallOfFame.js @@ -0,0 +1,109 @@ +/* + *IMPORTANT: GitHub rate limits unauthenticated requests, so + *this may break during development. To fix it, authenticate manually + */ + +import hallOfFameInput from "../src/data/hall-of-fame.json"; + +const REPO = "Ryan-Millard/Img2Num"; +const API = "https://api.github.com"; +const PER_PAGE = 50; +const MAX_PAGES = 10; + +const isBot = (user) => user?.type === "Bot" || user?.login?.endsWith("[bot]"); + +const withAvatarSize = (url) => (url ? `${url}${url.includes("?") ? "&" : "?"}s=96` : null); + +async function fetchAllContributors(headers) { + const all = []; + + try { + for (let page = 1; page <= MAX_PAGES; page++) { + const res = await fetch(`${API}/repos/${REPO}/contributors?per_page=${PER_PAGE}&page=${page}`, { headers }); + if (!res.ok) break; + + const batch = await res.json(); + if (!Array.isArray(batch) || batch.length === 0) break; + + all.push(...batch); + if (batch.length < PER_PAGE) break; + } + } catch {} + + return all + .filter((c) => !isBot(c)) + .map((c) => ({ + username: c.login, + avatarUrl: withAvatarSize(c.avatar_url), + profileUrl: c.html_url, + contributions: c.contributions, + })); +} + +async function fetchFeaturedMember(entry, headers) { + const fallback = { + username: entry.username, + name: entry.username, + avatarUrl: null, + profileUrl: `https://github.com/${entry.username}`, + year: entry.year, + blurb: entry.blurb || "", + mergedPRCount: null, + recentPRs: [], + }; + + try { + const userRes = await fetch(`${API}/users/${entry.username}`, { headers }); + if (!userRes.ok) return fallback; + + const userData = await userRes.json(); + if (isBot(userData)) return null; + + const user = { + ...fallback, + username: userData.login || entry.username, + name: userData.name || userData.login || entry.username, + avatarUrl: withAvatarSize(userData.avatar_url), + profileUrl: userData.html_url || fallback.profileUrl, + }; + + const searchUrl = `${API}/search/issues?q=repo:${REPO}+type:pr+author:${entry.username}+is:merged&sort=created&order=desc&per_page=5`; + const prRes = await fetch(searchUrl, { headers }); + + if (prRes.ok) { + const prData = await prRes.json(); + user.mergedPRCount = prData.total_count ?? 0; + user.recentPRs = (prData.items || []).map((pr) => ({ + title: pr.title, + url: pr.html_url, + number: pr.number, + })); + } + + return user; + } catch { + return fallback; + } +} + +export async function fetchHallOfFameData({ token } = {}) { + const headers = { + "User-Agent": "Img2Num-Docs", + Accept: "application/vnd.github+json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + + const [contributors, featuredRaw] = await Promise.all([fetchAllContributors(headers), Promise.all(hallOfFameInput.map((e) => fetchFeaturedMember(e, headers)))]); + + // Group featured members by year, newest first + const grouped = featuredRaw.filter(Boolean).reduce((acc, item) => { + (acc[item.year] ||= []).push(item); + return acc; + }, {}); + + const featured = Object.keys(grouped) + .sort((a, b) => Number(b) - Number(a)) + .map((year) => ({ year: Number(year), members: grouped[year] })); + + return { featured, contributors }; +} diff --git a/docs/plugins/hallOfFame/index.js b/docs/plugins/hallOfFame/index.js new file mode 100644 index 000000000..70546f5ef --- /dev/null +++ b/docs/plugins/hallOfFame/index.js @@ -0,0 +1,17 @@ +import { fetchHallOfFameData } from "../../lib/hallOfFame.js"; + +export default function hallOfFamePlugin() { + return { + name: "hall-of-fame", + + async loadContent() { + const token = process.env.GITHUB_TOKEN; + + return await fetchHallOfFameData({ token }); + }, + + async contentLoaded({ content, actions }) { + actions.setGlobalData(content); + }, + }; +} diff --git a/docs/src/data/hall-of-fame.json b/docs/src/data/hall-of-fame.json new file mode 100644 index 000000000..e8c7c786c --- /dev/null +++ b/docs/src/data/hall-of-fame.json @@ -0,0 +1,42 @@ +[ + { + "username": "08abhinav", + "year": 2026, + "blurb": "Built this page" + }, + { + "username": "siddharth1407", + "year": 2026, + "blurb": "Built a reusable WebGPU helper layer, ported both kernels to it, and documented gpu_utils" + }, + { + "username": "hjmillard", + "year": 2025, + "blurb": "Wrote the original K-means and thresholding utilities and later split K-means into its own module" + }, + { + "username": "fransafu", + "year": 2025, + "blurb": "Collaborated with @Krasner on the first implementation of the bilateral filter" + }, + { + "username": "krasner", + "year": 2025, + "blurb": "Turned Img2Num into what it is today" + }, + { + "username": "Prachi-Gupta2808", + "year": 2026, + "blurb": "Did a lot...★★★" + }, + { + "username": "dougwithseismic", + "year": "2025", + "blurb": "Set up the Vitest unit-testing infrastructure and co-authored the Docker/CMake cross-platform build" + }, + { + "username": "Ryan-Millard", + "year": 2025, + "blurb": "Did nothing" + } +] diff --git a/docs/src/pages/hall-of-fame.jsx b/docs/src/pages/hall-of-fame.jsx new file mode 100644 index 000000000..2e95a3a20 --- /dev/null +++ b/docs/src/pages/hall-of-fame.jsx @@ -0,0 +1,303 @@ +import React from "react"; +import Layout from "@theme/Layout"; +import { usePluginData } from "@docusaurus/useGlobalData"; +import { GitPullRequest, ExternalLink, GitCommit } from "lucide-react"; + +const CONTRIBUTORS_URL = "https://github.com/Ryan-Millard/Img2Num/graphs/contributors"; + +/** + * Each featured card has 4 stacked parts: header, blurb, PR count, PR list. + * Cards use CSS subgrid across these rows, so the same part lines up across + * every card in a row regardless of how much content each card has. + */ +const ROWS_PER_CARD = 4; + +const cardBase = { + backgroundColor: "var(--ifm-card-background-color)", + border: "1px solid var(--ifm-toc-border-color)", + borderRadius: "12px", + boxShadow: "var(--ifm-global-shadow-lw)", +}; + +const sectionHeadingStyle = { + fontSize: "1.75rem", + borderBottom: "2px solid var(--ifm-toc-border-color)", + paddingBottom: "0.5rem", + marginBottom: "1.5rem", +}; + +// Year groups sit side by side when there is room (e.g. 2026 | 2025), and +// stack on narrower screens, so wide monitors are not left mostly empty. +const yearsLayoutStyle = { + display: "grid", + // 664px = two 320px cards + the 24px gap between them + gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 664px), 1fr))", + columnGap: "clamp(1.5rem, 3vw, 4rem)", + alignItems: "start", + marginBottom: "2rem", +}; + +const featuredGridStyle = { + display: "grid", + // min(100%, 320px) stops a single column overflowing very narrow screens + gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 320px), 1fr))", + columnGap: "1.5rem", + rowGap: 0, // vertical spacing comes from each card's margin-bottom +}; + +const featuredCardStyle = { + ...cardBase, + padding: "1.5rem", + marginBottom: "1.5rem", + display: "grid", + gridRow: `span ${ROWS_PER_CARD}`, + gridTemplateRows: "subgrid", + rowGap: 0, +}; + +const contributorGridStyle = { + display: "grid", + gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 260px), 1fr))", + gap: "1rem", +}; + +/** Round avatar with an initial-letter fallback when no image URL exists. */ +function Avatar({ src, name, size }) { + if (src) { + return {`${name}'s; + } + + return ( + + ); +} + +/** + * A featured member card. Always renders all 4 grid rows (empty ones as + * placeholders) so the subgrid stays aligned with neighbouring cards. + */ +function FeaturedCard({ member }) { + const prs = member.recentPRs.slice(0, 3); + + return ( +
+ {/* Row 1: profile header */} +
+ + +
+ + {member.name} + + +
@{member.username}
+
+
+ + {/* Row 2: blurb */} + {member.blurb ? ( +

+ "{member.blurb}" +

+ ) : ( +