Add floating windows and btop system monitor
This commit is contained in:
1 parent
fd0b3b20a2
commit
201a1e0914
23 files changed
+1049
-36
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import { profile } from '#lib/content';
|
||||
import type { BtopData, GithubSummary, RepoProcess } from '#lib/btop/types';
|
||||
import { fetchContributionCalendar } from './contributions';
|
||||
|
||||
const cacheDuration = 60 * 60 * 1000;
|
||||
const githubHeaders = { Accept: 'application/vnd.github+json', 'User-Agent': 'portfolio-website' };
|
||||
|
||||
interface GithubRepo {
|
||||
name: string;
|
||||
html_url: string;
|
||||
language: string | null;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
size: number;
|
||||
pushed_at: string;
|
||||
fork: boolean;
|
||||
}
|
||||
|
||||
let cached: { data: BtopData; fetchedAt: number } | null = null;
|
||||
|
||||
export function githubUsername(): string | null {
|
||||
const githubUrl = profile.socials.find((social) => social.name === 'GitHub')?.url;
|
||||
return githubUrl?.match(/github\.com\/([^/]+)/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, { headers: githubHeaders });
|
||||
if (!response.ok) throw new Error(`GitHub returned ${response.status} for ${url}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchSummary(username: string): Promise<GithubSummary> {
|
||||
const user = await fetchJson<{ html_url: string; public_repos: number; followers: number }>(
|
||||
`https://api.github.com/users/${username}`
|
||||
);
|
||||
return { username, profileUrl: user.html_url, publicRepos: user.public_repos, followers: user.followers };
|
||||
}
|
||||
|
||||
async function fetchRepos(username: string): Promise<RepoProcess[]> {
|
||||
const repos = await fetchJson<GithubRepo[]>(`https://api.github.com/users/${username}/repos?per_page=100&sort=pushed`);
|
||||
return repos
|
||||
.filter((repo) => !repo.fork)
|
||||
.map((repo) => ({
|
||||
name: repo.name,
|
||||
url: repo.html_url,
|
||||
language: repo.language,
|
||||
stars: repo.stargazers_count,
|
||||
forks: repo.forks_count,
|
||||
sizeKb: repo.size,
|
||||
pushedAt: repo.pushed_at
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getBtopData(): Promise<BtopData | null> {
|
||||
if (cached && Date.now() - cached.fetchedAt < cacheDuration) return cached.data;
|
||||
|
||||
const username = githubUsername();
|
||||
if (!username) return null;
|
||||
|
||||
try {
|
||||
const [github, contributions, repos] = await Promise.all([
|
||||
fetchSummary(username),
|
||||
fetchContributionCalendar(username),
|
||||
fetchRepos(username)
|
||||
]);
|
||||
const data = { github, contributions, repos };
|
||||
cached = { data, fetchedAt: Date.now() };
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('btop data lookup failed:', error);
|
||||
return cached?.data ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ContributionCalendar, ContributionDay } from '#lib/btop/types';
|
||||
|
||||
const dayCellPattern = /data-date="(\d{4}-\d{2}-\d{2})" id="contribution-day-component-(\d+)-(\d+)" data-level="(\d)"/g;
|
||||
const tooltipPattern = /for="contribution-day-component-(\d+-\d+)"[^>]*>(\d+|No) contributions? on/g;
|
||||
const totalPattern = /([\d,]+)\s+contributions?\s+in the last year/;
|
||||
|
||||
function parseCounts(html: string): Map<string, number> {
|
||||
const counts = new Map<string, number>();
|
||||
for (const [, cellId, count] of html.matchAll(tooltipPattern)) {
|
||||
counts.set(cellId, count === 'No' ? 0 : Number(count));
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function parseWeeks(html: string): ContributionDay[][] {
|
||||
const counts = parseCounts(html);
|
||||
const weeks: ContributionDay[][] = [];
|
||||
for (const [, date, weekday, week, level] of html.matchAll(dayCellPattern)) {
|
||||
const weekIndex = Number(week);
|
||||
weeks[weekIndex] ??= [];
|
||||
weeks[weekIndex][Number(weekday)] = { date, level: Number(level), count: counts.get(`${weekday}-${week}`) ?? 0 };
|
||||
}
|
||||
return weeks.map((days) => days.filter(Boolean));
|
||||
}
|
||||
|
||||
function currentStreak(days: ContributionDay[]): number {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const pastDays = days.filter((day) => day.date <= today);
|
||||
if (pastDays.at(-1)?.count === 0) pastDays.pop();
|
||||
|
||||
let streak = 0;
|
||||
for (let index = pastDays.length - 1; index >= 0 && pastDays[index].count > 0; index--) streak++;
|
||||
return streak;
|
||||
}
|
||||
|
||||
export async function fetchContributionCalendar(username: string): Promise<ContributionCalendar> {
|
||||
const response = await fetch(`https://github.com/users/${username}/contributions`, {
|
||||
headers: { 'User-Agent': 'portfolio-website' }
|
||||
});
|
||||
if (!response.ok) throw new Error(`GitHub contributions returned ${response.status}`);
|
||||
|
||||
const html = await response.text();
|
||||
const weeks = parseWeeks(html);
|
||||
const days = weeks.flat().sort((first, second) => first.date.localeCompare(second.date));
|
||||
const total = Number(html.match(totalPattern)?.[1].replace(/,/g, '') ?? days.reduce((sum, day) => sum + day.count, 0));
|
||||
const bestDay = days.reduce<ContributionDay | null>((best, day) => (!best || day.count > best.count ? day : best), null);
|
||||
|
||||
return { total, weeks, currentStreak: currentStreak(days), bestDay };
|
||||
}
|
||||
Reference in new issue
Block a user