Files
markdownblog/src/app/posts/[...slug]/page.tsx
rattatwinko 9e6802df91 gay sex.
if you link in Markdown it scrolls to the link in the page. you can see this in mdtest.md
2025-06-18 17:44:15 +02:00

152 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import Link from 'next/link';
interface Post {
slug: string;
title: string;
date: string;
tags: string[];
summary: string;
content: string;
createdAt: string;
}
export default function PostPage({ params }: { params: { slug: string[] } }) {
const [post, setPost] = useState<Post | null>(null);
// Join the slug array to get the full path
const slugPath = Array.isArray(params.slug) ? params.slug.join('/') : params.slug;
useEffect(() => {
// Initial load
loadPost();
// Set up polling for changes
const interval = setInterval(loadPost, 2000);
// Cleanup
return () => clearInterval(interval);
}, [slugPath]);
// On post load or update, scroll to anchor in hash if present
useEffect(() => {
// Scroll to anchor if hash is present
const scrollToHash = () => {
if (window.location.hash) {
const id = window.location.hash.substring(1);
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
// On initial load
scrollToHash();
// Listen for hash changes
window.addEventListener('hashchange', scrollToHash);
return () => {
window.removeEventListener('hashchange', scrollToHash);
};
}, [post]);
// Intercept anchor clicks in rendered markdown to ensure smooth scrolling to headings
useEffect(() => {
// Find the rendered markdown container
const prose = document.querySelector('.prose');
if (!prose) return;
/**
* Handles clicks on anchor links (e.g. Table of Contents links) inside the markdown.
* - If the link is an in-page anchor (starts with #), prevent default navigation.
* - Try to find an element with the corresponding id and scroll to it.
* - If not found, search all headings for one whose text matches the anchor (case-insensitive, ignoring spaces/punctuation).
* - If a match is found, scroll to that heading.
* - Update the URL hash without reloading the page.
*/
const handleClick = (e: Event) => {
if (!(e instanceof MouseEvent)) return;
let target = e.target as HTMLElement | null;
// Traverse up to find the closest anchor tag
while (target && target.tagName !== 'A') {
target = target.parentElement;
}
if (target && target.tagName === 'A' && target.getAttribute('href')?.startsWith('#')) {
e.preventDefault();
const id = target.getAttribute('href')!.slice(1);
let el = document.getElementById(id);
if (!el) {
// Try to find a heading whose text matches the id (case-insensitive, ignoring spaces/punctuation)
const headings = prose.querySelectorAll('h1, h2, h3, h4, h5, h6');
const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]+/g, '');
const normId = normalize(id);
const found = Array.from(headings).find(h => normalize(h.textContent || '') === normId);
el = (found as HTMLElement) || null;
}
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
history.replaceState(null, '', `#${id}`);
}
}
};
prose.addEventListener('click', handleClick);
return () => {
prose.removeEventListener('click', handleClick);
};
}, [post]);
const loadPost = async () => {
try {
const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`);
const data = await response.json();
setPost(data);
} catch (error) {
console.error('Fehler beim Laden des Beitrags:', error);
}
};
if (!post) {
return <div>Lädt...</div>;
}
return (
<article className="min-h-screen p-8 max-w-4xl mx-auto">
<Link href="/" className="text-blue-600 hover:underline mb-8 inline-block">
Zurück zu den Beiträgen
</Link>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600 mb-8">
{post.date ? (
<div>Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}</div>
) : (
<div className="flex flex-col items-center">
<div className="flex">
<span className="text-2xl animate-spin mr-2"></span>
<span className="text-2xl animate-spin-reverse"></span>
</div>
<div className="text-xl font-bold mt-2">In Bearbeitung</div>
</div>
)}
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
</div>
<div className="flex gap-2 mb-8">
{post.tags.map((tag) => (
<span
key={tag}
className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm"
>
{tag}
</span>
))}
</div>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}