first pass

This commit is contained in:
Michael Freno
2025-12-18 23:22:22 -05:00
parent 84bcd554e5
commit 2fb355994f
6 changed files with 516 additions and 13 deletions

View File

@@ -0,0 +1,105 @@
import { Component, For, createMemo } from "solid-js";
interface ContributionDay {
date: string;
count: number;
}
export const ActivityHeatmap: Component<{
contributions: ContributionDay[] | undefined;
title: string;
}> = (props) => {
// Generate last 12 weeks of days
const weeks = createMemo(() => {
const today = new Date();
const weeksData: { date: string; count: number }[][] = [];
// Start from 12 weeks ago
const startDate = new Date(today);
startDate.setDate(startDate.getDate() - 84); // 12 weeks
// Create a map for quick lookup
const contributionMap = new Map<string, number>();
props.contributions?.forEach((c) => {
contributionMap.set(c.date, c.count);
});
// Generate weeks
for (let week = 0; week < 12; week++) {
const weekData: { date: string; count: number }[] = [];
for (let day = 0; day < 7; day++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + week * 7 + day);
const dateStr = date.toISOString().split("T")[0];
const count = contributionMap.get(dateStr) || 0;
weekData.push({ date: dateStr, count });
}
weeksData.push(weekData);
}
return weeksData;
});
const getColor = (count: number) => {
if (count === 0) return "var(--color-surface0)";
if (count <= 2) return "var(--color-green)";
if (count <= 5) return "var(--color-teal)";
if (count <= 10) return "var(--color-blue)";
return "var(--color-mauve)";
};
const getOpacity = (count: number) => {
if (count === 0) return 0.3;
if (count <= 2) return 0.4;
if (count <= 5) return 0.6;
if (count <= 10) return 0.8;
return 1;
};
return (
<div class="flex flex-col gap-3">
<h3 class="text-subtext0 text-sm font-semibold">{props.title}</h3>
<div class="flex gap-[2px] overflow-x-auto">
<For each={weeks()}>
{(week) => (
<div class="flex flex-col gap-[2px]">
<For each={week}>
{(day) => (
<div
class="h-2 w-2 rounded-[2px] transition-all hover:scale-125"
style={{
"background-color": getColor(day.count),
opacity: getOpacity(day.count)
}}
title={`${day.date}: ${day.count} contributions`}
/>
)}
</For>
</div>
)}
</For>
</div>
<div class="flex items-center gap-2 text-[10px]">
<span class="text-subtext1">Less</span>
<div class="flex gap-1">
<For each={[0, 2, 5, 10, 15]}>
{(count) => (
<div
class="h-2 w-2 rounded-[2px]"
style={{
"background-color": getColor(count),
opacity: getOpacity(count)
}}
/>
)}
</For>
</div>
<span class="text-subtext1">More</span>
</div>
</div>
);
};

View File

@@ -8,11 +8,25 @@ import GitHub from "./icons/GitHub";
import LinkedIn from "./icons/LinkedIn";
import MoonIcon from "./icons/MoonIcon";
import SunIcon from "./icons/SunIcon";
import { RecentCommits } from "./RecentCommits";
import { ActivityHeatmap } from "./ActivityHeatmap";
export function RightBarContent() {
const [isDark, setIsDark] = createSignal(false);
const [githubCommits, setGithubCommits] = createSignal<any[] | undefined>(
undefined
);
const [giteaCommits, setGiteaCommits] = createSignal<any[] | undefined>(
undefined
);
const [githubActivity, setGithubActivity] = createSignal<any[] | undefined>(
undefined
);
const [giteaActivity, setGiteaActivity] = createSignal<any[] | undefined>(
undefined
);
onMount(() => {
onMount(async () => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
@@ -26,6 +40,44 @@ export function RightBarContent() {
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
}
// Fetch GitHub activity
try {
const commits = await api.gitActivity.getGitHubCommits.query({
limit: 3
});
setGithubCommits(commits as any[]);
} catch (error) {
console.error("Failed to fetch GitHub commits:", error);
setGithubCommits([]);
}
try {
const activity = await api.gitActivity.getGitHubActivity.query();
setGithubActivity(activity as any[]);
} catch (error) {
console.error("Failed to fetch GitHub activity:", error);
setGithubActivity([]);
}
// Fetch Gitea activity
try {
const commits = await api.gitActivity.getGiteaCommits.query({
limit: 3
});
setGiteaCommits(commits as any[]);
} catch (error) {
console.error("Failed to fetch Gitea commits:", error);
setGiteaCommits([]);
}
try {
const activity = await api.gitActivity.getGiteaActivity.query();
setGiteaActivity(activity as any[]);
} catch (error) {
console.error("Failed to fetch Gitea activity:", error);
setGiteaActivity([]);
}
});
const toggleDarkMode = () => {
@@ -42,8 +94,8 @@ export function RightBarContent() {
};
return (
<div class="text-text flex h-full flex-col justify-between">
<Typewriter keepAlive={false} class="z-50 px-4">
<div class="text-text flex h-full flex-col gap-6 overflow-y-auto pb-6">
<Typewriter keepAlive={false} class="z-50 px-4 pt-4">
<ul class="flex flex-col gap-4">
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/contact">Contact Me</a>
@@ -95,8 +147,31 @@ export function RightBarContent() {
</li>
</ul>
</Typewriter>
{/* Git Activity Section */}
<div class="border-overlay0 flex flex-col gap-6 border-t px-4 pt-6">
<RecentCommits
commits={githubCommits()}
title="Recent GitHub Commits"
loading={githubCommits() === undefined}
/>
<ActivityHeatmap
contributions={githubActivity()}
title="GitHub Activity"
/>
<RecentCommits
commits={giteaCommits()}
title="Recent Gitea Commits"
loading={giteaCommits() === undefined}
/>
<ActivityHeatmap
contributions={giteaActivity()}
title="Gitea Activity"
/>
</div>
{/* Dark Mode Toggle */}
<div class="border-overlay0 border-t p-4">
<div class="border-overlay0 mt-auto border-t px-4 pt-6">
<button
onClick={toggleDarkMode}
class="hover:bg-surface0 flex w-full items-center gap-3 rounded-lg p-3 transition-all duration-200 ease-in-out hover:scale-105"

View File

@@ -0,0 +1,84 @@
import { Component, For, Show } from "solid-js";
interface Commit {
sha: string;
message: string;
author: string;
date: string;
repo: string;
url: string;
}
export const RecentCommits: Component<{
commits: Commit[] | undefined;
title: string;
loading?: boolean;
}> = (props) => {
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) {
return `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else if (diffDays < 7) {
return `${diffDays}d ago`;
} else {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric"
});
}
};
return (
<div class="flex flex-col gap-3">
<h3 class="text-subtext0 text-sm font-semibold">{props.title}</h3>
<Show
when={!props.loading && props.commits && props.commits.length > 0}
fallback={
<div class="text-subtext1 text-xs">
{props.loading ? "Loading..." : "No recent commits"}
</div>
}
>
<div class="flex flex-col gap-2">
<For each={props.commits}>
{(commit) => (
<a
href={commit.url}
target="_blank"
rel="noreferrer"
class="hover:bg-surface0 group rounded-md p-2 transition-all duration-200 ease-in-out hover:scale-[1.02]"
>
<div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<span class="text-text line-clamp-2 flex-1 text-xs leading-tight font-medium">
{commit.message}
</span>
<span class="text-subtext1 shrink-0 text-[10px]">
{formatDate(commit.date)}
</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-surface1 rounded px-1.5 py-0.5 font-mono text-[10px]">
{commit.sha}
</span>
<span class="text-subtext0 truncate text-[10px]">
{commit.repo}
</span>
</div>
</div>
</a>
)}
</For>
</div>
</Show>
</div>
);
};