Files
mintel.me/apps/web/src/components/TableOfContents.tsx
Marc Mintel c3fa10dd53
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
fix(ui): apply user-requested fixes for Meme, TOC, ROI Calculator, and Gantt
2026-02-22 18:07:48 +01:00

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>
);
};