Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Failing after 1m6s
Build & Deploy / 🧪 QA (push) Failing after 2m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
137 lines
5.9 KiB
TypeScript
137 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
interface TocItem {
|
|
label: string;
|
|
href: string;
|
|
subItems?: TocItem[];
|
|
}
|
|
|
|
interface TableOfContentsProps {
|
|
items?: TocItem[];
|
|
className?: string;
|
|
}
|
|
|
|
export const TableOfContents: React.FC<TableOfContentsProps> = ({ items, className = '' }) => {
|
|
// Falls props rein kommen, diese nutzen, ansonsten leeres Array
|
|
const initialItems = items || [];
|
|
const [dynamicItems, setDynamicItems] = useState<TocItem[]>(initialItems);
|
|
|
|
useEffect(() => {
|
|
if (initialItems.length > 0) return;
|
|
|
|
// Auto-discover headings if no items were passed
|
|
const headings = document.querySelectorAll('h2, h3');
|
|
const newItems: TocItem[] = [];
|
|
let currentH2: TocItem | null = null;
|
|
|
|
headings.forEach((heading) => {
|
|
// Ensure the heading has an ID
|
|
if (!heading.id) {
|
|
heading.id = heading.textContent?.toLowerCase().replace(/[^a-z0-9]+/g, '-') || `heading-${Math.random().toString(36).substring(2, 9)}`;
|
|
}
|
|
|
|
const item: TocItem = {
|
|
label: heading.textContent || '',
|
|
href: `#${heading.id}`,
|
|
};
|
|
|
|
if (heading.tagName === 'H2') {
|
|
currentH2 = { ...item, subItems: [] };
|
|
newItems.push(currentH2);
|
|
} else if (heading.tagName === 'H3' && currentH2) {
|
|
currentH2.subItems?.push(item);
|
|
} else if (heading.tagName === 'H3' && !currentH2) {
|
|
// If an H3 appears before any H2, just add it to root
|
|
newItems.push(item);
|
|
}
|
|
});
|
|
|
|
// Verhindern vom loop via prev state check
|
|
setDynamicItems(prev => {
|
|
if (JSON.stringify(prev) === JSON.stringify(newItems)) return prev;
|
|
return newItems;
|
|
});
|
|
|
|
// Wir brauchen hier keine deps, da wir nur einmal beim Mount des Beitrags scannen wollen.
|
|
// Sollte items sich ändern, ist das ein Bug der AI, aber als Fallback ok.
|
|
}, []);
|
|
|
|
const displayItems = dynamicItems;
|
|
|
|
if (displayItems.length === 0) return null;
|
|
|
|
// JSON-LD for SiteNavigationElement
|
|
const jsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@type": "ItemList",
|
|
"itemListElement": displayItems.map((item, index) => ({
|
|
"@type": "SiteNavigationElement",
|
|
"position": index + 1,
|
|
"name": item.label,
|
|
"url": `https://mintel.me${item.href}`
|
|
}))
|
|
};
|
|
|
|
return (
|
|
<div className={`not-prose my-12 border-l-2 border-slate-200 pl-6 ${className}`}>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
/>
|
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-6">
|
|
Inhaltsverzeichnis
|
|
</p>
|
|
<nav>
|
|
<ul className="space-y-3 m-0 list-none p-0">
|
|
{displayItems.map((item, index) => (
|
|
<li key={index} className="m-0 p-0">
|
|
<a
|
|
href={item.href}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
const targetId = item.href.substring(1);
|
|
const elem = document.getElementById(targetId);
|
|
if (elem) {
|
|
const y = elem.getBoundingClientRect().top + window.scrollY - 144;
|
|
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
window.history.pushState(null, '', item.href);
|
|
}
|
|
}}
|
|
className="text-slate-700 hover:text-blue-600 font-medium transition-colors no-underline block"
|
|
>
|
|
{item.label}
|
|
</a>
|
|
{item.subItems && item.subItems.length > 0 && (
|
|
<ul className="mt-2 ml-4 space-y-2 border-l-2 border-slate-200 pl-4 m-0 list-none">
|
|
{item.subItems.map((subItem, subIndex) => (
|
|
<li key={subIndex} className="m-0 p-0">
|
|
<a
|
|
href={subItem.href}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
const targetId = subItem.href.substring(1);
|
|
const elem = document.getElementById(targetId);
|
|
if (elem) {
|
|
const y = elem.getBoundingClientRect().top + window.scrollY - 144;
|
|
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
window.history.pushState(null, '', subItem.href);
|
|
}
|
|
}}
|
|
className="text-slate-500 hover:text-blue-500 text-sm transition-colors no-underline block"
|
|
>
|
|
{subItem.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
);
|
|
};
|