first pass
This commit is contained in:
105
src/components/ActivityHeatmap.tsx
Normal file
105
src/components/ActivityHeatmap.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,11 +8,25 @@ import GitHub from "./icons/GitHub";
|
|||||||
import LinkedIn from "./icons/LinkedIn";
|
import LinkedIn from "./icons/LinkedIn";
|
||||||
import MoonIcon from "./icons/MoonIcon";
|
import MoonIcon from "./icons/MoonIcon";
|
||||||
import SunIcon from "./icons/SunIcon";
|
import SunIcon from "./icons/SunIcon";
|
||||||
|
import { RecentCommits } from "./RecentCommits";
|
||||||
|
import { ActivityHeatmap } from "./ActivityHeatmap";
|
||||||
|
|
||||||
export function RightBarContent() {
|
export function RightBarContent() {
|
||||||
const [isDark, setIsDark] = createSignal(false);
|
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(
|
const prefersDark = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)"
|
"(prefers-color-scheme: dark)"
|
||||||
).matches;
|
).matches;
|
||||||
@@ -26,6 +40,44 @@ export function RightBarContent() {
|
|||||||
document.documentElement.classList.add("light");
|
document.documentElement.classList.add("light");
|
||||||
document.documentElement.classList.remove("dark");
|
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 = () => {
|
const toggleDarkMode = () => {
|
||||||
@@ -42,8 +94,8 @@ export function RightBarContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="text-text flex h-full flex-col justify-between">
|
<div class="text-text flex h-full flex-col gap-6 overflow-y-auto pb-6">
|
||||||
<Typewriter keepAlive={false} class="z-50 px-4">
|
<Typewriter keepAlive={false} class="z-50 px-4 pt-4">
|
||||||
<ul class="flex flex-col gap-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">
|
<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>
|
<a href="/contact">Contact Me</a>
|
||||||
@@ -95,8 +147,31 @@ export function RightBarContent() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Typewriter>
|
</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 */}
|
{/* Dark Mode Toggle */}
|
||||||
<div class="border-overlay0 border-t p-4">
|
<div class="border-overlay0 mt-auto border-t px-4 pt-6">
|
||||||
<button
|
<button
|
||||||
onClick={toggleDarkMode}
|
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"
|
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"
|
||||||
|
|||||||
84
src/components/RecentCommits.tsx
Normal file
84
src/components/RecentCommits.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/env/server.ts
vendored
16
src/env/server.ts
vendored
@@ -22,19 +22,16 @@ const serverEnvSchema = z.object({
|
|||||||
TURSO_LINEAGE_TOKEN: z.string().min(1),
|
TURSO_LINEAGE_TOKEN: z.string().min(1),
|
||||||
TURSO_DB_API_TOKEN: z.string().min(1),
|
TURSO_DB_API_TOKEN: z.string().min(1),
|
||||||
LINEAGE_OFFLINE_SERIALIZATION_SECRET: z.string().min(1),
|
LINEAGE_OFFLINE_SERIALIZATION_SECRET: z.string().min(1),
|
||||||
|
GITEA_URL: z.string().min(1),
|
||||||
|
GITEA_TOKEN: z.string().min(1),
|
||||||
|
GITHUB_API_TOKEN: z.string().min(1),
|
||||||
// Client-side variables accessible on server
|
// Client-side variables accessible on server
|
||||||
VITE_DOMAIN: z.string().min(1).optional(),
|
VITE_DOMAIN: z.string().min(1).optional(),
|
||||||
VITE_AWS_BUCKET_STRING: z.string().min(1).optional(),
|
VITE_AWS_BUCKET_STRING: z.string().min(1).optional(),
|
||||||
VITE_GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
VITE_GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
||||||
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional(),
|
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional(),
|
||||||
VITE_GITHUB_CLIENT_ID: z.string().min(1).optional(),
|
VITE_GITHUB_CLIENT_ID: z.string().min(1).optional(),
|
||||||
VITE_WEBSOCKET: z.string().min(1).optional(),
|
VITE_WEBSOCKET: z.string().min(1).optional()
|
||||||
// Aliases for backward compatibility
|
|
||||||
NEXT_PUBLIC_DOMAIN: z.string().min(1).optional(),
|
|
||||||
NEXT_PUBLIC_AWS_BUCKET_STRING: z.string().min(1).optional(),
|
|
||||||
NEXT_PUBLIC_GITHUB_CLIENT_ID: z.string().min(1).optional(),
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientEnvSchema = z.object({
|
const clientEnvSchema = z.object({
|
||||||
@@ -252,7 +249,10 @@ export const getMissingEnvVars = (): {
|
|||||||
"TURSO_LINEAGE_URL",
|
"TURSO_LINEAGE_URL",
|
||||||
"TURSO_LINEAGE_TOKEN",
|
"TURSO_LINEAGE_TOKEN",
|
||||||
"TURSO_DB_API_TOKEN",
|
"TURSO_DB_API_TOKEN",
|
||||||
"LINEAGE_OFFLINE_SERIALIZATION_SECRET"
|
"LINEAGE_OFFLINE_SERIALIZATION_SECRET",
|
||||||
|
"GITEA_URL",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"GITHUB_API_TOKEN"
|
||||||
];
|
];
|
||||||
|
|
||||||
const requiredClientVars = [
|
const requiredClientVars = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { lineageRouter } from "./routers/lineage";
|
|||||||
import { miscRouter } from "./routers/misc";
|
import { miscRouter } from "./routers/misc";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
import { blogRouter } from "./routers/blog";
|
import { blogRouter } from "./routers/blog";
|
||||||
|
import { gitActivityRouter } from "./routers/git-activity";
|
||||||
import { createTRPCRouter } from "./utils";
|
import { createTRPCRouter } from "./utils";
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
@@ -14,7 +15,8 @@ export const appRouter = createTRPCRouter({
|
|||||||
lineage: lineageRouter,
|
lineage: lineageRouter,
|
||||||
misc: miscRouter,
|
misc: miscRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
blog: blogRouter
|
blog: blogRouter,
|
||||||
|
gitActivity: gitActivityRouter
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
237
src/server/api/routers/git-activity.ts
Normal file
237
src/server/api/routers/git-activity.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../utils";
|
||||||
|
import { env } from "~/env/server";
|
||||||
|
|
||||||
|
// Types for commits
|
||||||
|
interface GitCommit {
|
||||||
|
sha: string;
|
||||||
|
message: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
repo: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContributionDay {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gitActivityRouter = createTRPCRouter({
|
||||||
|
// Get recent commits from GitHub
|
||||||
|
getGitHubCommits: publicProcedure
|
||||||
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/users/MikeFreno/events`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.GITHUB_API_TOKEN}`,
|
||||||
|
Accept: "application/vnd.github.v3+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await response.json();
|
||||||
|
|
||||||
|
// Filter for push events and extract commits
|
||||||
|
const commits: GitCommit[] = [];
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.type === "PushEvent" && commits.length < input.limit) {
|
||||||
|
for (const commit of event.payload.commits || []) {
|
||||||
|
if (commits.length >= input.limit) break;
|
||||||
|
commits.push({
|
||||||
|
sha: commit.sha.substring(0, 7),
|
||||||
|
message: commit.message.split("\n")[0], // First line only
|
||||||
|
author: event.actor.login,
|
||||||
|
date: event.created_at,
|
||||||
|
repo: event.repo.name,
|
||||||
|
url: `https://github.com/${event.repo.name}/commit/${commit.sha}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commits;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching GitHub commits:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get recent commits from Gitea
|
||||||
|
getGiteaCommits: publicProcedure
|
||||||
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
// First, get user's repos
|
||||||
|
const reposResponse = await fetch(
|
||||||
|
`${env.GITEA_URL}/api/v1/user/repos`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${env.GITEA_TOKEN}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reposResponse.ok) {
|
||||||
|
throw new Error(`Gitea API error: ${reposResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = await reposResponse.json();
|
||||||
|
const commits: GitCommit[] = [];
|
||||||
|
|
||||||
|
// Get commits from each repo
|
||||||
|
for (const repo of repos) {
|
||||||
|
if (commits.length >= input.limit) break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commitsResponse = await fetch(
|
||||||
|
`${env.GITEA_URL}/api/v1/repos/${repo.owner.login}/${repo.name}/commits?limit=${input.limit}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${env.GITEA_TOKEN}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (commitsResponse.ok) {
|
||||||
|
const repoCommits = await commitsResponse.json();
|
||||||
|
for (const commit of repoCommits) {
|
||||||
|
if (commits.length >= input.limit) break;
|
||||||
|
commits.push({
|
||||||
|
sha: commit.sha.substring(0, 7),
|
||||||
|
message: commit.commit.message.split("\n")[0],
|
||||||
|
author: commit.commit.author.name,
|
||||||
|
date: commit.commit.author.date,
|
||||||
|
repo: `${repo.owner.login}/${repo.name}`,
|
||||||
|
url: `${env.GITEA_URL}/${repo.owner.login}/${repo.name}/commit/${commit.sha}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching commits for ${repo.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date and return top N
|
||||||
|
return commits
|
||||||
|
.sort(
|
||||||
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
|
)
|
||||||
|
.slice(0, input.limit);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Gitea commits:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get GitHub contribution activity (for heatmap)
|
||||||
|
getGitHubActivity: publicProcedure.query(async () => {
|
||||||
|
try {
|
||||||
|
const oneYearAgo = new Date();
|
||||||
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/users/MikeFreno/events?per_page=100`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.GITHUB_API_TOKEN}`,
|
||||||
|
Accept: "application/vnd.github.v3+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await response.json();
|
||||||
|
|
||||||
|
// Count contributions by day
|
||||||
|
const contributionsByDay = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
const date = new Date(event.created_at).toISOString().split("T")[0];
|
||||||
|
contributionsByDay.set(date, (contributionsByDay.get(date) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to array format
|
||||||
|
const contributions: ContributionDay[] = Array.from(
|
||||||
|
contributionsByDay.entries()
|
||||||
|
).map(([date, count]) => ({ date, count }));
|
||||||
|
|
||||||
|
return contributions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching GitHub activity:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get Gitea contribution activity (for heatmap)
|
||||||
|
getGiteaActivity: publicProcedure.query(async () => {
|
||||||
|
try {
|
||||||
|
// Get all user repos
|
||||||
|
const reposResponse = await fetch(`${env.GITEA_URL}/api/v1/user/repos`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${env.GITEA_TOKEN}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!reposResponse.ok) {
|
||||||
|
throw new Error(`Gitea API error: ${reposResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = await reposResponse.json();
|
||||||
|
const contributionsByDay = new Map<string, number>();
|
||||||
|
|
||||||
|
// Fetch commits from all repos
|
||||||
|
for (const repo of repos) {
|
||||||
|
try {
|
||||||
|
const commitsResponse = await fetch(
|
||||||
|
`${env.GITEA_URL}/api/v1/repos/${repo.owner.login}/${repo.name}/commits?limit=100`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${env.GITEA_TOKEN}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (commitsResponse.ok) {
|
||||||
|
const commits = await commitsResponse.json();
|
||||||
|
for (const commit of commits) {
|
||||||
|
const date = new Date(commit.commit.author.date)
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0];
|
||||||
|
contributionsByDay.set(
|
||||||
|
date,
|
||||||
|
(contributionsByDay.get(date) || 0) + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching commits for ${repo.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to array format
|
||||||
|
const contributions: ContributionDay[] = Array.from(
|
||||||
|
contributionsByDay.entries()
|
||||||
|
).map(([date, count]) => ({ date, count }));
|
||||||
|
|
||||||
|
return contributions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Gitea activity:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user