feat: Add professional portfolio images and Hero redesign
- Add 15 optimized WebP images for portfolio (21-53KB each) - Redesign Hero section with full-width background image - Add SSR support for Hero component - Update About section with new portrait image - Update Contact page with professional headshot - Add images to Services/Leistungen page - Add image optimization script (sharp) - Update translations for gallery section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
import React from 'react';
|
||||
|
||||
function parseInlineMarkdown(text: string): React.ReactNode {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let remaining = text;
|
||||
let keyIndex = 0;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
// Inline code
|
||||
const codeMatch = remaining.match(/^`([^`]+)`/);
|
||||
if (codeMatch) {
|
||||
parts.push(
|
||||
<code key={keyIndex++} className="bg-zinc-800 px-1.5 py-0.5 rounded text-[#697565] text-sm font-mono">
|
||||
{codeMatch[1]}
|
||||
</code>
|
||||
);
|
||||
remaining = remaining.slice(codeMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bold
|
||||
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
|
||||
if (boldMatch) {
|
||||
parts.push(<strong key={keyIndex++} className="font-semibold text-white">{boldMatch[1]}</strong>);
|
||||
remaining = remaining.slice(boldMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Italic
|
||||
const italicMatch = remaining.match(/^\*([^*]+)\*/);
|
||||
if (italicMatch) {
|
||||
parts.push(<em key={keyIndex++} className="italic">{italicMatch[1]}</em>);
|
||||
remaining = remaining.slice(italicMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Links
|
||||
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (linkMatch) {
|
||||
parts.push(
|
||||
<a key={keyIndex++} href={linkMatch[2]} className="text-[#697565] hover:text-[#8a9a7e] underline" target="_blank" rel="noopener noreferrer">
|
||||
{linkMatch[1]}
|
||||
</a>
|
||||
);
|
||||
remaining = remaining.slice(linkMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular character
|
||||
const nextSpecial = remaining.search(/[`*\[]/);
|
||||
if (nextSpecial === -1) {
|
||||
parts.push(remaining);
|
||||
break;
|
||||
} else if (nextSpecial === 0) {
|
||||
parts.push(remaining[0]);
|
||||
remaining = remaining.slice(1);
|
||||
} else {
|
||||
parts.push(remaining.slice(0, nextSpecial));
|
||||
remaining = remaining.slice(nextSpecial);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length === 1 ? parts[0] : <>{parts}</>;
|
||||
}
|
||||
|
||||
export function renderMarkdown(content: string): React.ReactNode {
|
||||
const lines = content.split('\n');
|
||||
const elements: React.ReactNode[] = [];
|
||||
let i = 0;
|
||||
let listItems: { type: 'ul' | 'ol'; items: React.ReactNode[] } | null = null;
|
||||
let codeBlock: { language: string; lines: string[] } | null = null;
|
||||
let tableRows: string[][] | null = null;
|
||||
|
||||
const flushList = () => {
|
||||
if (listItems) {
|
||||
if (listItems.type === 'ul') {
|
||||
elements.push(
|
||||
<ul key={`ul-${elements.length}`} className="list-disc list-inside space-y-2 my-4 text-zinc-300">
|
||||
{listItems.items}
|
||||
</ul>
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<ol key={`ol-${elements.length}`} className="list-decimal list-inside space-y-2 my-4 text-zinc-300">
|
||||
{listItems.items}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
listItems = null;
|
||||
}
|
||||
};
|
||||
|
||||
const flushTable = () => {
|
||||
if (tableRows && tableRows.length > 0) {
|
||||
const header = tableRows[0];
|
||||
const body = tableRows.slice(2); // Skip header separator row
|
||||
elements.push(
|
||||
<div key={`table-${elements.length}`} className="overflow-x-auto my-6">
|
||||
<table className="min-w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-700">
|
||||
{header.map((cell, idx) => (
|
||||
<th key={idx} className="px-4 py-2 text-left text-zinc-200 font-semibold">
|
||||
{cell.trim()}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{body.map((row, rowIdx) => (
|
||||
<tr key={rowIdx} className="border-b border-zinc-800">
|
||||
{row.map((cell, cellIdx) => (
|
||||
<td key={cellIdx} className="px-4 py-2 text-zinc-300">
|
||||
{cell.trim()}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
tableRows = null;
|
||||
}
|
||||
};
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Code block start/end
|
||||
if (line.startsWith('```')) {
|
||||
if (codeBlock) {
|
||||
// End code block
|
||||
elements.push(
|
||||
<pre key={`code-${elements.length}`} className="bg-zinc-900 border border-zinc-800 rounded-lg p-4 overflow-x-auto my-6">
|
||||
<code className="text-sm font-mono text-zinc-300">
|
||||
{codeBlock.lines.join('\n')}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
codeBlock = null;
|
||||
} else {
|
||||
// Start code block
|
||||
flushList();
|
||||
flushTable();
|
||||
const language = line.slice(3).trim();
|
||||
codeBlock = { language, lines: [] };
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inside code block
|
||||
if (codeBlock) {
|
||||
codeBlock.lines.push(line);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Table row
|
||||
if (line.startsWith('|') && line.endsWith('|')) {
|
||||
flushList();
|
||||
const cells = line.slice(1, -1).split('|');
|
||||
if (!tableRows) {
|
||||
tableRows = [];
|
||||
}
|
||||
tableRows.push(cells);
|
||||
i++;
|
||||
continue;
|
||||
} else if (tableRows) {
|
||||
flushTable();
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (line.startsWith('#### ')) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h4 key={`h4-${elements.length}`} className="text-lg font-semibold text-white mt-5 mb-2">
|
||||
{parseInlineMarkdown(line.slice(5))}
|
||||
</h4>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('### ')) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h3 key={`h3-${elements.length}`} className="text-xl font-semibold text-white mt-6 mb-3">
|
||||
{parseInlineMarkdown(line.slice(4))}
|
||||
</h3>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('## ')) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h2 key={`h2-${elements.length}`} className="text-2xl font-bold text-white mt-8 mb-4">
|
||||
{parseInlineMarkdown(line.slice(3))}
|
||||
</h2>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('# ')) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h1 key={`h1-${elements.length}`} className="text-3xl font-bold text-white mt-8 mb-4">
|
||||
{parseInlineMarkdown(line.slice(2))}
|
||||
</h1>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered list items
|
||||
if (line.match(/^[-*]\s/)) {
|
||||
if (!listItems || listItems.type !== 'ul') {
|
||||
flushList();
|
||||
listItems = { type: 'ul', items: [] };
|
||||
}
|
||||
listItems.items.push(
|
||||
<li key={`li-${listItems.items.length}`}>
|
||||
{parseInlineMarkdown(line.replace(/^[-*]\s/, ''))}
|
||||
</li>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list items
|
||||
if (line.match(/^\d+\.\s/)) {
|
||||
if (!listItems || listItems.type !== 'ol') {
|
||||
flushList();
|
||||
listItems = { type: 'ol', items: [] };
|
||||
}
|
||||
listItems.items.push(
|
||||
<li key={`li-${listItems.items.length}`}>
|
||||
{parseInlineMarkdown(line.replace(/^\d+\.\s/, ''))}
|
||||
</li>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Checkbox list items
|
||||
if (line.match(/^[-*]\s\[[ x]\]/i)) {
|
||||
if (!listItems || listItems.type !== 'ul') {
|
||||
flushList();
|
||||
listItems = { type: 'ul', items: [] };
|
||||
}
|
||||
const checked = line.match(/^[-*]\s\[x\]/i);
|
||||
const itemContent = line.replace(/^[-*]\s\[[ x]\]\s?/i, '');
|
||||
listItems.items.push(
|
||||
<li key={`li-${listItems.items.length}`} className="flex items-center gap-2">
|
||||
<span className={checked ? 'text-green-500' : 'text-zinc-500'}>
|
||||
{checked ? '✓' : '○'}
|
||||
</span>
|
||||
{parseInlineMarkdown(itemContent)}
|
||||
</li>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (line.match(/^[-*_]{3,}$/)) {
|
||||
flushList();
|
||||
elements.push(<hr key={`hr-${elements.length}`} className="border-zinc-700 my-8" />);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
if (line.startsWith('> ')) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<blockquote key={`quote-${elements.length}`} className="border-l-4 border-[#697565] pl-4 my-4 text-zinc-400 italic">
|
||||
{parseInlineMarkdown(line.slice(2))}
|
||||
</blockquote>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty line
|
||||
if (!line.trim()) {
|
||||
flushList();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular paragraph
|
||||
flushList();
|
||||
elements.push(
|
||||
<p key={`p-${elements.length}`} className="text-zinc-300 mb-4 leading-relaxed">
|
||||
{parseInlineMarkdown(line)}
|
||||
</p>
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
// Flush any remaining items
|
||||
flushList();
|
||||
flushTable();
|
||||
|
||||
return <div className="prose prose-invert prose-zinc max-w-none">{elements}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user