Lil bit of cleanup
This commit is contained in:
parent
722ace7c74
commit
1a713fe1b6
18 changed files with 304 additions and 205 deletions
|
|
@ -16,6 +16,13 @@ export async function generateMetadata({ params }: CategoryPageProps): Promise<M
|
||||||
return {
|
return {
|
||||||
title: `${category} | ${metadata.title!.toString()}`,
|
title: `${category} | ${metadata.title!.toString()}`,
|
||||||
description: 'List of posts in this category',
|
description: 'List of posts in this category',
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
title: `${category} | ${metadata.title!.toString()}`,
|
||||||
|
description: 'List of posts in this category',
|
||||||
|
url: process.env.SITE_URL + `/categories/${categorySlug}`,
|
||||||
|
siteName: metadata.title!.toString(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
77
app/(withRecent)/categories/page.tsx
Normal file
77
app/(withRecent)/categories/page.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { getAllCategories } from '@/lib/categories'
|
||||||
|
import { getAllPostsMeta } from '@/lib/posts'
|
||||||
|
import { slugify } from '@/utils/slug'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { metadata } from '@/app/metadata'
|
||||||
|
import { dateFormatter } from '@/utils/time'
|
||||||
|
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
return {
|
||||||
|
title: `Categories | ${metadata.title!.toString()}`,
|
||||||
|
description: 'List of categories',
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
title: `Categories | ${metadata.title!.toString()}`,
|
||||||
|
description: 'List of categories',
|
||||||
|
url: process.env.SITE_URL + '/categories',
|
||||||
|
siteName: metadata.title!.toString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CategoriesPage() {
|
||||||
|
const categories = getAllCategories()
|
||||||
|
const posts = getAllPostsMeta()
|
||||||
|
const categoryNames = Object.keys(categories)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="max-w-3xl 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">
|
||||||
|
{categoryNames.map((category) => {
|
||||||
|
const categoryPosts = posts.filter((post) => post.category === category)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={category} className="pl-5">
|
||||||
|
<Link
|
||||||
|
href={`/categories/${slugify(category)}`}
|
||||||
|
className="group flex items-baseline gap-2 py-2 px-2 -ml-2 hover:bg-accent/10 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-bold group-hover:text-accent transition-colors">
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs text-neutral-600">({categoryPosts.length})</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<ul className="border-l border-neutral-800 ml-3 mb-3">
|
||||||
|
{categoryPosts.map((post) => (
|
||||||
|
<li key={post.slug} className="pl-5">
|
||||||
|
<Link
|
||||||
|
href={`/posts/${post.slug}`}
|
||||||
|
className="group flex items-baseline justify-between gap-3 py-1.5 px-2 -ml-2 border-l-2 border-transparent hover:bg-accent/10 hover:border-accent transition-colors"
|
||||||
|
>
|
||||||
|
<span className="group-hover:text-accent transition-colors truncate">
|
||||||
|
{post.title}
|
||||||
|
</span>
|
||||||
|
{post.date && (
|
||||||
|
<time
|
||||||
|
dateTime={new Date(post.date).toISOString()}
|
||||||
|
className="text-sm whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{dateFormatter.format(new Date(post.date))}
|
||||||
|
</time>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
app/(withRecent)/layout.tsx
Normal file
10
app/(withRecent)/layout.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
|
||||||
|
|
||||||
|
export default function WithRecentLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<RecentlyUpdatedPosts />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
app/(withRecent)/page.tsx
Normal file
41
app/(withRecent)/page.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { getAllPostsMeta } from '@/lib/posts'
|
||||||
|
import { Post } from '@/components/common/Post'
|
||||||
|
|
||||||
|
export default function BlogIndex() {
|
||||||
|
const posts = getAllPostsMeta()
|
||||||
|
|
||||||
|
const rest = posts.slice(1)
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < rest.length; i += 2) {
|
||||||
|
rows.push(rest.slice(i, i + 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
|
||||||
|
<section>
|
||||||
|
{posts.length === 0 ? (
|
||||||
|
<div className="rounded-xl border border-border p-6 text-fg-dim">
|
||||||
|
No published posts yet.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Post post={posts[0]} isLatest />
|
||||||
|
<div className="border-t border-border my-6" aria-hidden="true"></div>
|
||||||
|
<div className="flex flex-col gap-8 mt-4">
|
||||||
|
{rows.map((row) => {
|
||||||
|
const rowHasImage = row.some((p) => p.coverImage)
|
||||||
|
return (
|
||||||
|
<div key={row[0].slug} className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{row.map((post) => (
|
||||||
|
<Post post={post} key={post.slug} showImageSlot={rowHasImage} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { getAllCategories } from '@/lib/categories'
|
|
||||||
import { getAllPostsMeta } from '@/lib/posts'
|
|
||||||
import { slugify } from '@/utils/slug'
|
|
||||||
import type { Metadata } from 'next'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { metadata } from '@/app/metadata'
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
|
||||||
return { title: `Categories | ${metadata.title!.toString()}`, description: 'List of categories' }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CategoriesPage() {
|
|
||||||
const categories = getAllCategories()
|
|
||||||
const posts = getAllPostsMeta()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full">
|
|
||||||
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16 w-full">
|
|
||||||
<h1 className="text-3xl font-bold">Categories</h1>
|
|
||||||
<ul>
|
|
||||||
{Object.keys(categories).map((category) => (
|
|
||||||
<li key={category}>
|
|
||||||
<Link href={`/categories/${slugify(category)}`}>
|
|
||||||
{category} ({categories[category]})
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{posts
|
|
||||||
.filter((post) => post.category === category)
|
|
||||||
.map((post) => (
|
|
||||||
<li key={post.slug}>
|
|
||||||
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</main>
|
|
||||||
<div className="w-80" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -43,7 +43,8 @@ body {
|
||||||
@apply list-decimal;
|
@apply list-decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul,
|
||||||
|
ol {
|
||||||
@apply list-outside;
|
@apply list-outside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +152,7 @@ dd {
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a {
|
.prose a {
|
||||||
@apply text-accent;
|
@apply text-accent hover:underline focus:underline cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose ul:not(.task-list-container) {
|
.prose ul:not(.task-list-container) {
|
||||||
|
|
|
||||||
14
app/metadata.ts
Normal file
14
app/metadata.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Ephemeris",
|
||||||
|
description: "A blog about me and my interests",
|
||||||
|
creator: "hangerthem",
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
title: "Ephemeris",
|
||||||
|
description: "A blog about me and my interests",
|
||||||
|
url: process.env.SITE_URL,
|
||||||
|
siteName: "Ephemeris",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Ephemeris",
|
|
||||||
description: "A blog about me and my interests",
|
|
||||||
creator: "hangerthem",
|
|
||||||
}
|
|
||||||
63
app/page.tsx
63
app/page.tsx
|
|
@ -1,63 +0,0 @@
|
||||||
import { getAllPostsMeta } from '@/lib/posts'
|
|
||||||
import { Post } from '@/components/common/Post'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export default function BlogIndex() {
|
|
||||||
const posts = getAllPostsMeta()
|
|
||||||
|
|
||||||
const rest = posts.slice(1)
|
|
||||||
const rows = []
|
|
||||||
for (let i = 0; i < rest.length; i += 2) {
|
|
||||||
rows.push(rest.slice(i, i + 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
|
|
||||||
<section>
|
|
||||||
{posts.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-border p-6 text-fg-dim">
|
|
||||||
No published posts yet.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Post post={posts[0]} isLatest />
|
|
||||||
<div className="border-t border-border my-6" aria-hidden="true"></div>
|
|
||||||
<div className="flex flex-col gap-8 mt-4">
|
|
||||||
{rows.map((row) => {
|
|
||||||
const rowHasImage = row.some((p) => p.coverImage)
|
|
||||||
return (
|
|
||||||
<div key={row[0].slug} className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
{row.map((post) => (
|
|
||||||
<Post post={post} key={post.slug} showImageSlot={rowHasImage} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<aside className="w-80 hidden lg:block px-4 py-8 lg:py-16">
|
|
||||||
<div className="sticky top-8">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Recently updated</h2>
|
|
||||||
<ul className="flex flex-col gap-2 ml-2">
|
|
||||||
{posts
|
|
||||||
.toSorted(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.updated ?? b.date).getTime() - new Date(a.updated ?? a.date).getTime(),
|
|
||||||
)
|
|
||||||
.map((post) => (
|
|
||||||
<li key={post.slug} className="text-sm text-fg-dim">
|
|
||||||
<Link href={`/posts/${post.slug}`} className="hover:underline">
|
|
||||||
{post.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { metadata } from '@/app/metadata'
|
||||||
import { ImageWrapper } from '@/components/common/ImageWrapper'
|
import { ImageWrapper } from '@/components/common/ImageWrapper'
|
||||||
import { ShareLinks } from '@/components/common/ShareLinks'
|
import { ShareLinks } from '@/components/common/ShareLinks'
|
||||||
import { TableOfContents } from '@/components/common/TableOfContents'
|
import { TableOfContents } from '@/components/common/TableOfContents'
|
||||||
import { getAllPostSlugs, getPostBySlug } from '@/lib/posts'
|
import { getAllPostSlugs, getNextAndPreviousPosts, getPostBySlug } from '@/lib/posts'
|
||||||
|
import { slugify } from '@/utils/slug'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
|
@ -19,19 +20,33 @@ export async function generateMetadata({
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { slug } = await params
|
const { slug } = await params
|
||||||
const { meta } = await getPostBySlug(slug)
|
const { meta } = await getPostBySlug(slug)
|
||||||
return { title: `${meta.title} | ${metadata.title!.toString()}`, description: meta.description }
|
return {
|
||||||
|
title: `${meta.title} | ${metadata.title!.toString()}`,
|
||||||
|
description: meta.description,
|
||||||
|
openGraph: {
|
||||||
|
type: 'article',
|
||||||
|
title: `${meta.title} | ${metadata.title!.toString()}`,
|
||||||
|
description: meta.description,
|
||||||
|
url: process.env.SITE_URL + `/posts/${slug}`,
|
||||||
|
images: meta.coverImage ? [meta.coverImage] : undefined,
|
||||||
|
tags: meta.tags,
|
||||||
|
...(meta.updated && { modifiedTime: new Date(meta.updated).toISOString() }),
|
||||||
|
siteName: metadata.title!.toString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
|
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = await params
|
const { slug } = await params
|
||||||
const { meta, content, tableOfContents } = await getPostBySlug(slug)
|
const { meta, content, tableOfContents } = await getPostBySlug(slug)
|
||||||
|
const { next, previous } = getNextAndPreviousPosts(slug)
|
||||||
|
|
||||||
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 mx-auto px-4 py-8 lg:py-16">
|
||||||
{meta.category && (
|
{meta.category && (
|
||||||
<Link
|
<Link
|
||||||
href={`/categories/${meta.category.toLowerCase()}`}
|
href={`/categories/${slugify(meta.category)}`}
|
||||||
className="text-accent hover:underline font-sm mb-2 block"
|
className="text-accent hover:underline font-sm mb-2 block"
|
||||||
>
|
>
|
||||||
{meta.category}
|
{meta.category}
|
||||||
|
|
@ -99,6 +114,29 @@ export default async function Post({ params }: { params: Promise<{ slug: string
|
||||||
description={meta.description}
|
description={meta.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
{previous ? (
|
||||||
|
<Link
|
||||||
|
href={`/posts/${previous.slug}`}
|
||||||
|
className="text-accent hover:underline font-sm mb-2 block"
|
||||||
|
>
|
||||||
|
← {previous.title}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
{next ? (
|
||||||
|
<Link
|
||||||
|
href={`/posts/${next.slug}`}
|
||||||
|
className="text-accent hover:underline font-sm mb-2 block"
|
||||||
|
>
|
||||||
|
{next.title} →
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<aside className="hidden lg:block w-80">
|
<aside className="hidden lg:block w-80">
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,11 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { isValidElement, type ReactNode } from 'react'
|
|
||||||
import { CopyButton } from './CopyButton'
|
import { CopyButton } from './CopyButton'
|
||||||
|
import { getTextContent } from '@/lib/react-node-text'
|
||||||
|
|
||||||
type CodeWrapperProps = {
|
type CodeWrapperProps = {
|
||||||
language?: string
|
language?: string
|
||||||
children?: ReactNode
|
children?: React.ReactNode
|
||||||
}
|
|
||||||
|
|
||||||
function getTextContent(node: ReactNode): string {
|
|
||||||
if (node === null || typeof node === 'boolean') return ''
|
|
||||||
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
|
||||||
if (Array.isArray(node)) return node.map(getTextContent).join('')
|
|
||||||
if (isValidElement(node)) {
|
|
||||||
const props = node.props as { children?: ReactNode }
|
|
||||||
return getTextContent(props.children)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeWrapper({ language, children }: CodeWrapperProps) {
|
export function CodeWrapper({ language, children }: CodeWrapperProps) {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
import type { AnchorHTMLAttributes } from 'react';
|
import type { AnchorHTMLAttributes } from 'react'
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
|
|
||||||
type ExternalLinkProps = Omit<
|
type ExternalLinkProps = Omit<
|
||||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
'href' | 'rel' | 'target'
|
'href' | 'rel' | 'target'
|
||||||
> & {
|
> & {
|
||||||
href: string;
|
href: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export function ExternalLink({ className, ...props }: ExternalLinkProps) {
|
export function ExternalLink(props: ExternalLinkProps) {
|
||||||
return (
|
return <a {...props} target="_blank" rel="noopener noreferrer" />
|
||||||
<a
|
|
||||||
{...props}
|
|
||||||
className={cn(className, 'hover:underline focus:underline')}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
components/common/RecentlyUpdatedPosts.tsx
Normal file
27
components/common/RecentlyUpdatedPosts.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { getAllPostsMeta } from '@/lib/posts'
|
||||||
|
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">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Recently updated</h2>
|
||||||
|
<ul className="flex flex-col gap-2 ml-1">
|
||||||
|
{posts
|
||||||
|
.toSorted(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.updated ?? b.date).getTime() - new Date(a.updated ?? a.date).getTime(),
|
||||||
|
)
|
||||||
|
.map((post) => (
|
||||||
|
<li key={post.slug} className="text-sm text-fg-dim">
|
||||||
|
<Link href={`/posts/${post.slug}`} className="hover:underline">
|
||||||
|
{post.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
import { Folder, Home, Info, Rss, Tags } from 'lucide-react'
|
import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { ExternalLink } from '../common/ExternalLink'
|
||||||
|
|
||||||
type NavItem = {
|
type NavItem = {
|
||||||
href: string
|
href: string
|
||||||
label: string
|
label: string
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
target?: string
|
isExternal?: boolean
|
||||||
rel?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
|
|
@ -22,8 +22,13 @@ const navItems: NavItem[] = [
|
||||||
href: '/feed.xml',
|
href: '/feed.xml',
|
||||||
label: 'RSS Feed',
|
label: 'RSS Feed',
|
||||||
icon: <Rss className="w-4 h-4" />,
|
icon: <Rss className="w-4 h-4" />,
|
||||||
target: '_blank',
|
isExternal: true,
|
||||||
rel: 'noopener noreferrer',
|
},
|
||||||
|
{
|
||||||
|
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
|
||||||
|
label: 'Source Code',
|
||||||
|
icon: <GitBranch className="w-4 h-4" />,
|
||||||
|
isExternal: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -45,15 +50,20 @@ export function NavLinks() {
|
||||||
isActive(item.href) ? 'text-fg bg-fg/25' : 'text-fg-dim hover:text-fg hover:bg-fg/10',
|
isActive(item.href) ? 'text-fg bg-fg/25' : 'text-fg-dim hover:text-fg hover:bg-fg/10',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link
|
{item.isExternal ? (
|
||||||
href={item.href}
|
<ExternalLink
|
||||||
className="flex items-center gap-2 px-2 py-1"
|
href={item.href}
|
||||||
target={item.target}
|
className="flex items-center gap-2 px-2 py-1 hover:no-underline focus:no-underline"
|
||||||
rel={item.rel}
|
>
|
||||||
>
|
{item.icon}
|
||||||
{item.icon}
|
{item.label}
|
||||||
{item.label}
|
</ExternalLink>
|
||||||
</Link>
|
) : (
|
||||||
|
<Link href={item.href} className="flex items-center gap-2 px-2 py-1">
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ title: Self-Development and Gamification of Life
|
||||||
description: In this post, I explore the concept of self-development and how gamification can be used to enhance productivity and personal growth. Learn how to turn your life into a game and level up your skills!
|
description: In this post, I explore the concept of self-development and how gamification can be used to enhance productivity and personal growth. Learn how to turn your life into a game and level up your skills!
|
||||||
author: hangerthem
|
author: hangerthem
|
||||||
date: 2024-08-05 22:00:00 +0200
|
date: 2024-08-05 22:00:00 +0200
|
||||||
category: "Personal Development"
|
category: Personal Development
|
||||||
tags: [self-development, gamification, productivity, growth]
|
tags: [self-development, gamification, productivity, growth]
|
||||||
math: true
|
math: true
|
||||||
mermaid: true
|
mermaid: true
|
||||||
|
|
|
||||||
|
|
@ -264,12 +264,12 @@ $$
|
||||||
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the electrons, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the electrons, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
||||||
- **Covariant Derivative** ($D_\mu$):
|
- **Covariant Derivative** ($D_\mu$):
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
D_\mu = \partial_\mu - i e A_\mu
|
D_\mu = \partial_\mu - i e A_\mu
|
||||||
\label{eq:covariant_derivative_for_electrons}
|
\label{eq:covariant_derivative_for_electrons}
|
||||||
\end{equation}
|
\end{equation}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
- **Explanation**: This accounts for the interaction of electrons with the electromagnetic field. $e$ is the electric charge, and $A_\mu$ is the electromagnetic potential.
|
- **Explanation**: This accounts for the interaction of electrons with the electromagnetic field. $e$ is the electric charge, and $A_\mu$ is the electromagnetic potential.
|
||||||
|
|
||||||
|
|
@ -301,21 +301,21 @@ $$
|
||||||
|
|
||||||
- **$W$ Field Strength Tensor** ($W^a_{\mu\nu}$):
|
- **$W$ Field Strength Tensor** ($W^a_{\mu\nu}$):
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
W^a_{\mu\nu} = \partial_\mu W^a_\nu - \partial_\nu W^a_\mu + g \epsilon^{abc} W^b_\mu W^c_\nu
|
W^a_{\mu\nu} = \partial_\mu W^a_\nu - \partial_\nu W^a_\mu + g \epsilon^{abc} W^b_\mu W^c_\nu
|
||||||
\label{eq:w_field_strength_tensor}
|
\label{eq:w_field_strength_tensor}
|
||||||
\end{equation}
|
\end{equation}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
- **$B$ Field Strength Tensor** ($B_{\mu\nu}$):
|
- **$B$ Field Strength Tensor** ($B_{\mu\nu}$):
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
B_{\mu\nu} = \partial_\mu B_\nu - \partial_\nu B_\mu
|
B_{\mu\nu} = \partial_\mu B_\nu - \partial_\nu B_\mu
|
||||||
\label{eq:b_field_strength_tensor}
|
\label{eq:b_field_strength_tensor}
|
||||||
\end{equation}
|
\end{equation}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
- **Explanation**: These terms describe the field strengths of the $W$ and $B$ fields, which are associated with the weak and electromagnetic forces, respectively. The $W$ field strength tensor involves the non-Abelian field strength term, reflecting the non-Abelian nature of the weak force.
|
- **Explanation**: These terms describe the field strengths of the $W$ and $B$ fields, which are associated with the weak and electromagnetic forces, respectively. The $W$ field strength tensor involves the non-Abelian field strength term, reflecting the non-Abelian nature of the weak force.
|
||||||
|
|
||||||
|
|
@ -325,12 +325,12 @@ $$
|
||||||
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the leptons/quarks, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the leptons/quarks, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
||||||
- **Covariant Derivative** ($D_\mu$):
|
- **Covariant Derivative** ($D_\mu$):
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
D_\mu = \partial_\mu - i g' Y B_\mu - i g \frac{\tau^a}{2} W^a_\mu
|
D_\mu = \partial_\mu - i g' Y B_\mu - i g \frac{\tau^a}{2} W^a_\mu
|
||||||
\label{eq:covariant_derivative_for_leptons_quarks}
|
\label{eq:covariant_derivative_for_leptons_quarks}
|
||||||
\end{equation}
|
\end{equation}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
- **Explanation**: This accounts for the interaction of leptons/quarks with the $W$ and $B$ fields. $g'$ and $g$ are the coupling constants for the $U(1)$ and $SU(2)$ gauge groups, respectively. $Y$ is the weak hypercharge, and $\frac{\tau^a}{2}$ are the generators of the $SU(2)$ group.
|
- **Explanation**: This accounts for the interaction of leptons/quarks with the $W$ and $B$ fields. $g'$ and $g$ are the coupling constants for the $U(1)$ and $SU(2)$ gauge groups, respectively. $Y$ is the weak hypercharge, and $\frac{\tau^a}{2}$ are the generators of the $SU(2)$ group.
|
||||||
|
|
||||||
|
|
@ -375,12 +375,12 @@ $$
|
||||||
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the quarks, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
- **$i \gamma^\mu D_\mu$** - Represents the kinetic term of the quarks, where $\gamma^\mu$ are the gamma matrices associated with the Dirac equation and $D_\mu$ is the covariant derivative.
|
||||||
- **Covariant Derivative** ($D_\mu$):
|
- **Covariant Derivative** ($D_\mu$):
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
D_\mu = \partial_\mu - i g_s \frac{\lambda^a}{2} G^a_\mu
|
D_\mu = \partial_\mu - i g_s \frac{\lambda^a}{2} G^a_\mu
|
||||||
\label{eq:quark_covariant_derivative_qcd}
|
\label{eq:quark_covariant_derivative_qcd}
|
||||||
\end{equation}
|
\end{equation}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
- **Explanation**: This accounts for the interaction of quarks with gluons. $g_s$ is the strong coupling constant, and $\frac{\lambda^a}{2}$ are the generators of the SU(3) color gauge group. $G^a_\mu$ are the gluon fields.
|
- **Explanation**: This accounts for the interaction of quarks with gluons. $g_s$ is the strong coupling constant, and $\frac{\lambda^a}{2}$ are the generators of the SU(3) color gauge group. $G^a_\mu$ are the gluon fields.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,9 @@ $$
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[this is a link](https://example.com)
|
this is an [external link](https://example.com)
|
||||||
|
|
||||||
|
and this is an [internal link](/about)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD;
|
graph TD;
|
||||||
|
|
|
||||||
|
|
@ -18,27 +18,18 @@ import { ExternalLink } from '@/components/common/ExternalLink'
|
||||||
import readingTime from 'reading-time'
|
import readingTime from 'reading-time'
|
||||||
import { rehypeUnwrapImages } from './rehype-unwrap-images'
|
import { rehypeUnwrapImages } from './rehype-unwrap-images'
|
||||||
import { rehypeMermaidSsg } from './rehype-mermaid-ssg'
|
import { rehypeMermaidSsg } from './rehype-mermaid-ssg'
|
||||||
import { stripLeadingMatch } from './react-node-text'
|
import { getTextContent, stripLeadingMatch } from './react-node-text'
|
||||||
import { BlockquoteWrapper } from '@/components/common/BlockquoteWrapper'
|
import { BlockquoteWrapper } from '@/components/common/BlockquoteWrapper'
|
||||||
import type { NoteType } from '@/types/Note.type'
|
import type { NoteType } from '@/types/Note.type'
|
||||||
import { rehypeMark } from './rehype-mark'
|
import { rehypeMark } from './rehype-mark'
|
||||||
import { rehypeDefinition } from './rehype-definition'
|
import { rehypeDefinition } from './rehype-definition'
|
||||||
import { slugify } from '@/utils/slug'
|
import { slugify } from '@/utils/slug'
|
||||||
import { PostMeta } from '@/types/Post.type'
|
import { PostMeta } from '@/types/Post.type'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react'
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'content/posts')
|
const postsDirectory = path.join(process.cwd(), 'content/posts')
|
||||||
|
|
||||||
function getTextContent(node: ReactNode): string {
|
|
||||||
if (typeof node === 'string') return node
|
|
||||||
if (typeof node === 'number') return String(node)
|
|
||||||
if (Array.isArray(node)) return node.map(getTextContent).join('')
|
|
||||||
if (node && typeof node === 'object' && 'props' in node) {
|
|
||||||
const props = node.props as { children?: ReactNode }
|
|
||||||
return getTextContent(props.children)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllPostSlugs(): string[] {
|
export function getAllPostSlugs(): string[] {
|
||||||
return fs
|
return fs
|
||||||
.readdirSync(postsDirectory)
|
.readdirSync(postsDirectory)
|
||||||
|
|
@ -60,6 +51,17 @@ export function getAllPostsMeta(): PostMeta[] {
|
||||||
.toSorted((a, b) => (a.date < b.date ? 1 : -1))
|
.toSorted((a, b) => (a.date < b.date ? 1 : -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNextAndPreviousPosts(slug: string): {
|
||||||
|
next: PostMeta | null
|
||||||
|
previous: PostMeta | null
|
||||||
|
} {
|
||||||
|
const posts = getAllPostsMeta()
|
||||||
|
const index = posts.findIndex((post) => post.slug === slug)
|
||||||
|
const next = index < posts.length - 1 ? posts[index + 1] : null
|
||||||
|
const previous = index > 0 ? posts[index - 1] : null
|
||||||
|
return { next, previous }
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPostBySlug(slug: string) {
|
export async function getPostBySlug(slug: string) {
|
||||||
const fullPath = path.join(postsDirectory, `${slug}.md`)
|
const fullPath = path.join(postsDirectory, `${slug}.md`)
|
||||||
const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))
|
const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))
|
||||||
|
|
@ -98,9 +100,12 @@ export async function getPostBySlug(slug: string) {
|
||||||
img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />,
|
img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />,
|
||||||
a: ({ href, children }: { href: string; children: ReactNode }) =>
|
a: ({ href, children }: { href: string; children: ReactNode }) =>
|
||||||
href?.startsWith('http') ? (
|
href?.startsWith('http') ? (
|
||||||
<ExternalLink href={href}>{children}</ExternalLink>
|
<ExternalLink href={href}>
|
||||||
|
{children}
|
||||||
|
<ExternalLinkIcon className="inline-block w-3 h-3 ml-1" />
|
||||||
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<a href={href}>{children}</a>
|
<Link href={href}>{children}</Link>
|
||||||
),
|
),
|
||||||
blockquote: ({ children }: { children: ReactNode }) => {
|
blockquote: ({ children }: { children: ReactNode }) => {
|
||||||
const textContent = getTextContent(children).trimStart()
|
const textContent = getTextContent(children).trimStart()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue