feat: git activity feed

This commit is contained in:
Yehoshua Sandler 2025-05-14 11:19:01 -05:00
parent fdbcce0dcd
commit 310e944633
5 changed files with 173 additions and 73 deletions

View File

@ -1,10 +1,10 @@
import configPromise from '@payload-config'
import { getPayload, type RequiredDataFromCollectionSlug } from 'payload'
import { draftMode } from "next/headers";
import { cache } from "react";
import { RenderBlocks } from "@/blocks/RenderBlocks";
import PageClient from "./page.client";
import { draftMode } from 'next/headers'
import { cache } from 'react'
import { RenderBlocks } from '@/blocks/RenderBlocks'
import PageClient from './page.client'
import GitProfile from '@/components/GitProfile'
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
@ -63,9 +63,10 @@ export default async function Page({ params: paramsPromise }: Args) {
//const { isEnabled: draft } = await draftMode()
//const url = '/' + slug
const page: RequiredDataFromCollectionSlug<'pages'> | null = await queryPageBySlug({
slug,
}) || null
const page: RequiredDataFromCollectionSlug<'pages'> | null =
(await queryPageBySlug({
slug,
})) || null
if (!page) {
return <div /> // return <PayloadRedirects url={url} />
@ -74,14 +75,14 @@ export default async function Page({ params: paramsPromise }: Args) {
const { layout } = page
return (
<article className="flex flex-col min-h-[100dvh] space-y-10" >
<article className="flex flex-col min-h-[100dvh] space-y-10">
<PageClient />
{/* Allows redirects for valid pages too */}
{/*<PayloadRedirects disableNotFound url={url} />*/}
{/*draft && <LivePreviewListener />*/}
<RenderBlocks blocks={layout} />
<GitProfile />
</article>
)
}

View File

@ -0,0 +1,47 @@
'use client'
import { CommitDetails, getGitActivity } from '@/serverActions/getGitActivity'
import { useMemo, useState } from 'react'
import { GitCommitCard } from '../git-commit-card'
import { Loader2 } from 'lucide-react'
const GitProfile = () => {
const [commits, setCommits] = useState<CommitDetails[]>([])
useMemo(async () => {
if (commits.length) return
const commitResponse = await getGitActivity()
setCommits(commitResponse || [])
}, [commits, setCommits])
return (
<div>
{!!commits.length ?
commits.map((c) => {
const refNameSplitArray = c.commitRefName.split('/')
const refName = refNameSplitArray[refNameSplitArray.length - 1]
return (
<GitCommitCard
key={c.id + c.repoName}
image={c.avatarUrl}
title={c.repoName}
description={c.commitMessage}
dates={new Date(c.commitTimestamp).toLocaleString()}
location={refName}
repoWebsite={c.repoWebsite}
/>
)
})
:
<div>
<span className="block text-center">. . . loading contributions . . .</span>
<Loader2 className='animate-spin size-14 mx-auto my-12' />
</div>
}
</div>
)
}
export default GitProfile

View File

@ -0,0 +1,70 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import Link from 'next/link'
interface Props {
title: string
description: string
dates: string
location: string
image?: string
repoWebsite?: string
links?: readonly {
icon: React.ReactNode
title: string
href: string
}[]
}
export function GitCommitCard({
title,
description,
dates,
location,
image,
repoWebsite,
links,
}: Props) {
return (
<div className="border-l border-l-purple-800 dark:border-l-purple-300">
<li className="relative ml-10 py-4 -top-4 list-none">
<div className="absolute -left-15 flex items-center justify-center bg-white rounded-full">
<Avatar className="border size-10 m-auto">
<AvatarImage src={image} alt={title} className="object-contain" />
<AvatarFallback>{title[0]}</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-1 flex-col justify-start ">
{dates && <time className="text-xs text-muted-foreground leading-none">{dates}</time>}
<div>
<Link target="_blank" href={repoWebsite || ''} className="font-semibold hover:text-lg hover:underline hover:text-purple-800 dark:hover:text-purple-300 transition-all">
{title}
</Link>
{location && (
<span className="text-sm italic ml-2 text-purple-800 dark:text-purple-300">
{`=> ${location}`}
</span>
)}
</div>
{description && (
<span className="prose dark:prose-invert text-sm text-muted-foreground mt-2">
{description}
</span>
)}
</div>
{links && links.length > 0 && (
<div className="mt-2 flex flex-row flex-wrap items-start gap-2">
{links?.map((link, idx) => (
<Link href={link.href} key={idx}>
<Badge key={idx} title={link.title} className="flex gap-2">
{link.icon}
{link.title}
</Badge>
</Link>
))}
</div>
)}
</li>
</div>
)
}

View File

@ -1,62 +0,0 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
interface Props {
title: string;
description: string;
dates: string;
location: string;
image?: string;
links?: readonly {
icon: React.ReactNode;
title: string;
href: string;
}[];
}
export function HackathonCard({
title,
description,
dates,
location,
image,
links,
}: Props) {
return (
<li className="relative ml-10 py-4">
<div className="absolute -left-16 top-2 flex items-center justify-center bg-white rounded-full">
<Avatar className="border size-12 m-auto">
<AvatarImage src={image} alt={title} className="object-contain" />
<AvatarFallback>{title[0]}</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-1 flex-col justify-start gap-1">
{dates && (
<time className="text-xs text-muted-foreground">{dates}</time>
)}
<h2 className="font-semibold leading-none">{title}</h2>
{location && (
<p className="text-sm text-muted-foreground">{location}</p>
)}
{description && (
<span className="prose dark:prose-invert text-sm text-muted-foreground">
{description}
</span>
)}
</div>
{links && links.length > 0 && (
<div className="mt-2 flex flex-row flex-wrap items-start gap-2">
{links?.map((link, idx) => (
<Link href={link.href} key={idx}>
<Badge key={idx} title={link.title} className="flex gap-2">
{link.icon}
{link.title}
</Badge>
</Link>
))}
</div>
)}
</li>
);
}

View File

@ -0,0 +1,44 @@
'use server'
type CommitContent = {
Commits: [any],
HeadCommit: {
Message: string,
Timestamp: string,
}
}
export type CommitDetails = {
id: number,
repoName: string,
repoUrl: string,
repoWebsite: string,
commitMessage: string,
commitTimestamp: string,
commitRefName: string,
avatarUrl: string,
}
export const getGitActivity = async (): Promise<CommitDetails[]> => {
const gitActivityResponse = await fetch('https://git.beitzah.net/api/v1/users/ysandler/activities/feeds?only-performed-by=true')
const gitActivityJson = await gitActivityResponse.json()
const commits = gitActivityJson
.filter((a: any) => a.op_type === 'commit_repo' && !!a.content)
.map((c: any) => {
const repo = c.repo
const commitContent = JSON.parse(c.content as string) as CommitContent
return {
id: c.id,
repoName: repo.full_name,
repoUrl: repo.url,
repoWebsite: repo.html_url,
commitMessage: commitContent.HeadCommit.Message,
commitTimestamp: commitContent.HeadCommit.Timestamp,
commitRefName: c.ref_name || '',
avatarUrl: repo.owner.avatar_url,
}
})
return commits
}