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) {
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>
<Link href="/categories">Back to categories</Link>
</main>
@ -48,7 +48,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
}
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>
<ul>
{posts.map((post) => (

View file

@ -33,7 +33,7 @@ export default function CategoriesPage() {
const categoryNames = Object.keys(categories)
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>
<ul className="border-l border-neutral-800 ml-1">

View file

@ -11,7 +11,7 @@ export default function BlogIndex() {
}
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>
{posts.length === 0 ? (
<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 (
<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 && (
<Link
href={`/categories/${slugify(meta.category)}`}
@ -139,9 +139,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
</div>
</article>
<aside className="hidden lg:block w-80">
<TableOfContents items={tableOfContents} />
</aside>
<TableOfContents items={tableOfContents} />
</main>
)
}

View file

@ -9,7 +9,7 @@ type ImageWrapperProps = {
export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
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="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 { navItems } from '@/components/layout/navItems'
export function NavLinks() {
type NavLinksProps = {
gitOriginUrl: string | null
}
export function NavLinks({ gitOriginUrl }: NavLinksProps) {
const pathname = usePathname()
const isActive = (href: string) => {
@ -26,7 +30,11 @@ export function NavLinks() {
>
{item.isExternal ? (
<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"
>
{item.icon}

View file

@ -1,18 +1,69 @@
import { metadata } from '@/app/metadata'
import { NotepadText } from 'lucide-react'
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 (
<aside className="hidden lg:block w-64 shrink-0 px-4 bg-fg/5">
<div className="sticky top-6">
<h2 className="text-2xl font-bold mb-4 flex items-center gap-2">
<NotepadText className="w-6 h-6" />
<span>{metadata.title!.toString()}</span>
</h2>
<ul className="space-y-2">
<NavLinks />
</ul>
<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">
<NotepadText className="w-6 h-6" />
<span>{metadata.title!.toString()}</span>
</h2>
<ul className="space-y-2">
<NavLinks gitOriginUrl={gitOriginUrl} />
</ul>
</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>
)

View file

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

View file

@ -1,8 +1,8 @@
import { ImageWrapper } from '@/components/content/ImageWrapper'
import { cn } from '@/utils/cn'
import { PostMeta } from '@/types/Post.type'
import Link from 'next/link'
import { dateFormatter } from '@/utils/time'
import { cn } from '@/utils/cn'
type PostProps = {
post: PostMeta
@ -32,11 +32,11 @@ export function Post({ post, isLatest, showImageSlot }: PostProps) {
return (
<article className={cn('flex flex-col gap-4', isLatest && 'items-center md:flex-row')}>
{(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 />}
</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>}
{category && (
<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() {
const posts = getAllPostsMeta()
return (
<aside className="w-80 hidden lg:block py-4 lg:py-12">
<div className="sticky top-8 border-l border-border h-fit pl-4 py-4">
<aside className="w-80 xl:w-100 hidden lg:block">
<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>
<ul className="flex flex-col gap-2 ml-1">
{posts

View file

@ -38,23 +38,25 @@ export function TableOfContents({ items }: { items: TocItem[] }) {
if (items.length === 0) return null
return (
<nav aria-label="Table of contents" className="sticky top-20">
<h2 className="text-2xl font-bold mb-4">Table of Contents</h2>
<ul className="space-y-1 text-sm">
{items.map((item) => (
<li key={item.id} className={indentByLevel[item.level] ?? 'pl-0'}>
<a
href={`#${item.id}`}
className={`block truncate transition-colors ${
activeId === item.id ? 'text-accent font-medium' : 'text-fg-dim hover:text-fg'
}`}
aria-current={activeId === item.id ? 'location' : undefined}
>
{item.text}
</a>
</li>
))}
</ul>
</nav>
<aside className="hidden lg:block w-80 xl:w-100">
<nav aria-label="Table of contents" className="sticky top-20">
<h2 className="text-2xl font-bold mb-4">Table of Contents</h2>
<ul className="space-y-1 text-sm">
{items.map((item) => (
<li key={item.id} className={indentByLevel[item.level] ?? 'pl-0'}>
<a
href={`#${item.id}`}
className={`block truncate transition-colors ${
activeId === item.id ? 'text-accent font-medium' : 'text-fg-dim hover:text-fg'
}`}
aria-current={activeId === item.id ? 'location' : undefined}
>
{item.text}
</a>
</li>
))}
</ul>
</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
}
}