Added commit tracker/link to sidebar

This commit is contained in:
HangerThem 2026-08-03 17:34:05 +02:00
parent 9a2d25ed11
commit b8909cf9ce
12 changed files with 156 additions and 44 deletions

View file

@ -40,7 +40,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
if (!category) { if (!category) {
return ( return (
<main> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full">
<h1>Category not found</h1> <h1>Category not found</h1>
<Link href="/categories">Back to categories</Link> <Link href="/categories">Back to categories</Link>
</main> </main>
@ -48,7 +48,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
} }
return ( return (
<main> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full">
<h1>Category: {category}</h1> <h1>Category: {category}</h1>
<ul> <ul>
{posts.map((post) => ( {posts.map((post) => (

View file

@ -33,7 +33,7 @@ export default function CategoriesPage() {
const categoryNames = Object.keys(categories) const categoryNames = Object.keys(categories)
return ( return (
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16 w-full"> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full">
<div className="text-sm text-fg-dim mb-2">~/categories</div> <div className="text-sm text-fg-dim mb-2">~/categories</div>
<ul className="border-l border-neutral-800 ml-1"> <ul className="border-l border-neutral-800 ml-1">

View file

@ -11,7 +11,7 @@ export default function BlogIndex() {
} }
return ( return (
<main className="max-w-3xl mx-auto px-4 py-8 mb-16 lg:py-16"> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-16 lg:py-16">
<section> <section>
{posts.length === 0 ? ( {posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim"> <div className="rounded-xl border border-border p-6 text-fg-dim">

View file

@ -43,7 +43,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
return ( return (
<main className="flex w-full"> <main className="flex w-full">
<article className="max-w-3xl mx-auto px-4 py-8 lg:py-16"> <article className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16">
{meta.category && ( {meta.category && (
<Link <Link
href={`/categories/${slugify(meta.category)}`} href={`/categories/${slugify(meta.category)}`}
@ -139,9 +139,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
</div> </div>
</article> </article>
<aside className="hidden lg:block w-80">
<TableOfContents items={tableOfContents} /> <TableOfContents items={tableOfContents} />
</aside>
</main> </main>
) )
} }

View file

@ -9,7 +9,7 @@ type ImageWrapperProps = {
export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) { export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
return ( return (
<figure className={cn('flex flex-col items-center gap-1 mb-4', isListing ? 'h-60' : 'w-full')}> <figure className={cn('flex flex-col items-center gap-1 mb-4 w-full', isListing && 'h-60')}>
<div className="border border-border rounded-xl w-full overflow-hidden flex-1"> <div className="border border-border rounded-xl w-full overflow-hidden flex-1">
<div <div
className="flex items-center gap-2 px-2 h-6 border-b border-border w-full bg-fg/10" className="flex items-center gap-2 px-2 h-6 border-b border-border w-full bg-fg/10"

View file

@ -6,7 +6,11 @@ import { usePathname } from 'next/navigation'
import { ExternalLink } from '@/components/ui/ExternalLink' import { ExternalLink } from '@/components/ui/ExternalLink'
import { navItems } from '@/components/layout/navItems' import { navItems } from '@/components/layout/navItems'
export function NavLinks() { type NavLinksProps = {
gitOriginUrl: string | null
}
export function NavLinks({ gitOriginUrl }: NavLinksProps) {
const pathname = usePathname() const pathname = usePathname()
const isActive = (href: string) => { const isActive = (href: string) => {
@ -26,7 +30,11 @@ export function NavLinks() {
> >
{item.isExternal ? ( {item.isExternal ? (
<ExternalLink <ExternalLink
href={item.href} href={
item.href.includes('{gitOriginUrl}')
? item.href.replace('{gitOriginUrl}', gitOriginUrl ?? '')
: item.href
}
className="flex items-center gap-2 px-2 py-1 hover:no-underline focus:no-underline" className="flex items-center gap-2 px-2 py-1 hover:no-underline focus:no-underline"
> >
{item.icon} {item.icon}

View file

@ -1,19 +1,70 @@
import { metadata } from '@/app/metadata' import { metadata } from '@/app/metadata'
import { NotepadText } from 'lucide-react' import { NotepadText } from 'lucide-react'
import { NavLinks } from '@/components/layout/NavLinks' import { NavLinks } from '@/components/layout/NavLinks'
import { siBluesky, siGithub, siMatrix, siTelegram, type SimpleIcon } from 'simple-icons'
import { ExternalLink } from '@/components/ui/ExternalLink'
import { getGitCommitHash, getGitOriginUrl } from '@/utils/git'
type SocialLink = {
icon: SimpleIcon
url: string
}
const socialLinks: SocialLink[] = [
{
icon: siBluesky,
url: 'https://bsky.app/profile/hangerthem.com',
},
{
icon: siGithub,
url: 'https://github.com/hangerthem',
},
{
icon: siMatrix,
url: 'https://matrix.to/#/@hangerthem:hangerthem.com',
},
{
icon: siTelegram,
url: 'https://t.me/hangerthem',
},
]
export async function Sidebar() {
const gitCommitHash = (await getGitCommitHash())?.substring(0, 7)
const gitOriginUrl = await getGitOriginUrl()
export function Sidebar() {
return ( return (
<aside className="hidden lg:block w-64 shrink-0 px-4 bg-fg/5"> <aside className="hidden lg:block w-64 shrink-0 px-4 bg-fg/5">
<div className="sticky top-6"> <div className="sticky top-0 flex flex-col h-screen gap-4 py-4">
<div>
<h2 className="text-2xl font-bold mb-4 flex items-center gap-2"> <h2 className="text-2xl font-bold mb-4 flex items-center gap-2">
<NotepadText className="w-6 h-6" /> <NotepadText className="w-6 h-6" />
<span>{metadata.title!.toString()}</span> <span>{metadata.title!.toString()}</span>
</h2> </h2>
<ul className="space-y-2"> <ul className="space-y-2">
<NavLinks /> <NavLinks gitOriginUrl={gitOriginUrl} />
</ul> </ul>
</div> </div>
<div className="mt-auto flex gap-4 mx-auto">
{socialLinks.map(({ icon, url }) => (
<ExternalLink key={url} href={url}>
<svg
dangerouslySetInnerHTML={{ __html: icon.svg }}
className="inline-block w-6 h-6 mr-1 fill-fg-dim hover:fill-accent transition-colors"
/>
</ExternalLink>
))}
</div>
{gitCommitHash && gitOriginUrl && (
<div className="text-xs text-fg-dim mt-2 mx-auto">
<span>Commit: </span>
<ExternalLink href={gitOriginUrl + '/commit/' + gitCommitHash}>
{gitCommitHash}
</ExternalLink>
</div>
)}
</div>
</aside> </aside>
) )
} }

View file

@ -21,7 +21,7 @@ export const navItems: NavItem[] = [
isExternal: true, isExternal: true,
}, },
{ {
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com', href: '{gitOriginUrl}',
label: 'Source Code', label: 'Source Code',
icon: <GitBranch className={navIconsClassName} />, icon: <GitBranch className={navIconsClassName} />,
isExternal: true, isExternal: true,

View file

@ -1,8 +1,8 @@
import { ImageWrapper } from '@/components/content/ImageWrapper' import { ImageWrapper } from '@/components/content/ImageWrapper'
import { cn } from '@/utils/cn'
import { PostMeta } from '@/types/Post.type' import { PostMeta } from '@/types/Post.type'
import Link from 'next/link' import Link from 'next/link'
import { dateFormatter } from '@/utils/time' import { dateFormatter } from '@/utils/time'
import { cn } from '@/utils/cn'
type PostProps = { type PostProps = {
post: PostMeta post: PostMeta
@ -32,11 +32,11 @@ export function Post({ post, isLatest, showImageSlot }: PostProps) {
return ( return (
<article className={cn('flex flex-col gap-4', isLatest && 'items-center md:flex-row')}> <article className={cn('flex flex-col gap-4', isLatest && 'items-center md:flex-row')}>
{(isLatest || showImageSlot) && ( {(isLatest || showImageSlot) && (
<div className={isLatest && coverImage ? 'flex-1' : 'h-60'}> <div className={isLatest && coverImage ? 'flex-1 w-full' : 'h-60'}>
{coverImage && <ImageWrapper src={coverImage} alt={coverImageAlt ?? title} isListing />} {coverImage && <ImageWrapper src={coverImage} alt={coverImageAlt ?? title} isListing />}
</div> </div>
)} )}
<div className={cn('flex flex-col gap-1', isLatest && 'flex-1')}> <div className={cn('flex flex-col gap-1 w-full', isLatest && 'flex-1')}>
{isLatest && <span className="text-xs font-mono text-accent">Latest post</span>} {isLatest && <span className="text-xs font-mono text-accent">Latest post</span>}
{category && ( {category && (
<div className="font-mono text-xs flex gap-1 flex-wrap text-fg-dim mt-1 categories"> <div className="font-mono text-xs flex gap-1 flex-wrap text-fg-dim mt-1 categories">

View file

@ -4,8 +4,8 @@ import Link from 'next/link'
export function RecentlyUpdatedPosts() { export function RecentlyUpdatedPosts() {
const posts = getAllPostsMeta() const posts = getAllPostsMeta()
return ( return (
<aside className="w-80 hidden lg:block py-4 lg:py-12"> <aside className="w-80 xl:w-100 hidden lg:block">
<div className="sticky top-8 border-l border-border h-fit pl-4 py-4"> <div className="sticky top-12 border-l border-border h-fit pl-4 py-4">
<h2 className="text-xl font-bold mb-4">Recently updated</h2> <h2 className="text-xl font-bold mb-4">Recently updated</h2>
<ul className="flex flex-col gap-2 ml-1"> <ul className="flex flex-col gap-2 ml-1">
{posts {posts

View file

@ -38,6 +38,7 @@ export function TableOfContents({ items }: { items: TocItem[] }) {
if (items.length === 0) return null if (items.length === 0) return null
return ( return (
<aside className="hidden lg:block w-80 xl:w-100">
<nav aria-label="Table of contents" className="sticky top-20"> <nav aria-label="Table of contents" className="sticky top-20">
<h2 className="text-2xl font-bold mb-4">Table of Contents</h2> <h2 className="text-2xl font-bold mb-4">Table of Contents</h2>
<ul className="space-y-1 text-sm"> <ul className="space-y-1 text-sm">
@ -56,5 +57,6 @@ export function TableOfContents({ items }: { items: TocItem[] }) {
))} ))}
</ul> </ul>
</nav> </nav>
</aside>
) )
} }

53
utils/git.ts Normal file
View file

@ -0,0 +1,53 @@
import { exec } from 'child_process'
/**
* Get the current Git commit hash.
* @returns The current Git commit hash as a string, or null if an error occurs.
*/
export async function getGitCommitHash(): Promise<string | null> {
try {
const commitHash = (await new Promise((resolve, reject) => {
exec('git rev-parse HEAD', (error, stdout) => {
if (error) {
reject(error)
} else {
resolve(stdout.toString().trim())
}
})
})) as string
return commitHash
} catch (error) {
console.error('Error getting Git commit hash:', error)
return null
}
}
/**
* Get the Git origin URL.
* @returns The Git origin URL as a string, or null if an error occurs.
*/
export async function getGitOriginUrl(): Promise<string | null> {
try {
const originUrl = (await new Promise((resolve, reject) => {
exec('git config --get remote.origin.url', (error, stdout) => {
if (error) {
reject(error)
} else {
resolve(stdout.toString().trim())
}
})
})) as string
if (originUrl.startsWith('ssh://')) {
const httpsUrl = originUrl.replace(/^ssh:\/\/git@/, 'https://').replace(/\.git$/, '')
const urlWithoutPort = httpsUrl.replace(/:\d+/, '')
return urlWithoutPort
}
return originUrl
} catch (error) {
console.error('Error getting Git origin URL:', error)
return null
}
}