fix(blog): optimize component share logic, typography, and modal layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
136
apps/web/src/components/TableOfContents.tsx
Normal file
136
apps/web/src/components/TableOfContents.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'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 p-8 bg-slate-50 border border-slate-200 rounded-2xl ${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 border-b border-slate-200 pb-2">
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user