Rich Text implementirert

This commit is contained in:
michi 2025-09-10 21:32:50 +02:00
parent e5c238f88c
commit 5e66f4bdda
35 changed files with 1786 additions and 137 deletions

View File

@ -12,6 +12,11 @@
],
"rootMainFiles": [],
"pages": {
"/": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/index.js"
],
"/_app": [
"static/chunks/webpack.js",
"static/chunks/main.js",
@ -22,20 +27,15 @@
"static/chunks/main.js",
"static/chunks/pages/_error.js"
],
"/admin": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/admin.js"
],
"/admin/login": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/admin/login.js"
],
"/blog/[slug]": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/blog/[slug].js"
],
"/devlog": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/devlog.js"
]
},
"ampFirstPages": []

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,18 +1 @@
{
"..\\node_modules\\@supabase\\auth-js\\dist\\module\\lib\\helpers.js -> @supabase/node-fetch": {
"id": "..\\node_modules\\@supabase\\auth-js\\dist\\module\\lib\\helpers.js -> @supabase/node-fetch",
"files": []
},
"..\\node_modules\\@supabase\\functions-js\\dist\\module\\helper.js -> @supabase/node-fetch": {
"id": "..\\node_modules\\@supabase\\functions-js\\dist\\module\\helper.js -> @supabase/node-fetch",
"files": []
},
"..\\node_modules\\@supabase\\realtime-js\\dist\\module\\RealtimeClient.js -> @supabase/node-fetch": {
"id": "..\\node_modules\\@supabase\\realtime-js\\dist\\module\\RealtimeClient.js -> @supabase/node-fetch",
"files": []
},
"..\\node_modules\\@supabase\\storage-js\\dist\\module\\lib\\helpers.js -> @supabase/node-fetch": {
"id": "..\\node_modules\\@supabase\\storage-js\\dist\\module\\lib\\helpers.js -> @supabase/node-fetch",
"files": []
}
}
{}

View File

@ -1 +1 @@
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":["static/chunks/react-refresh.js"],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":[],"pages":{"/_app":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_app.js"],"/_error":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_error.js"],"/admin":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/admin.js"],"/admin/login":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/admin/login.js"],"/blog/[slug]":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/blog/[slug].js"]},"ampFirstPages":[]}
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":["static/chunks/react-refresh.js"],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":[],"pages":{"/":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/index.js"],"/_app":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_app.js"],"/_error":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_error.js"],"/blog/[slug]":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/blog/[slug].js"],"/devlog":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/devlog.js"]},"ampFirstPages":[]}

View File

@ -5,6 +5,7 @@
"middleware": {
"/": {
"files": [
"prerender-manifest.js",
"server/edge-runtime-webpack.js",
"server/middleware.js"
],
@ -12,8 +13,8 @@
"page": "/",
"matchers": [
{
"regexp": "^/.*$",
"originalSource": "/:path*"
"regexp": "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/admin(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?(.json)?[\\/#\\?]?$",
"originalSource": "/admin/:path*"
}
],
"wasm": [],

View File

@ -1 +1 @@
self.__REACT_LOADABLE_MANIFEST="{\"..\\\\node_modules\\\\@supabase\\\\auth-js\\\\dist\\\\module\\\\lib\\\\helpers.js -> @supabase/node-fetch\":{\"id\":\"..\\\\node_modules\\\\@supabase\\\\auth-js\\\\dist\\\\module\\\\lib\\\\helpers.js -> @supabase/node-fetch\",\"files\":[]},\"..\\\\node_modules\\\\@supabase\\\\functions-js\\\\dist\\\\module\\\\helper.js -> @supabase/node-fetch\":{\"id\":\"..\\\\node_modules\\\\@supabase\\\\functions-js\\\\dist\\\\module\\\\helper.js -> @supabase/node-fetch\",\"files\":[]},\"..\\\\node_modules\\\\@supabase\\\\realtime-js\\\\dist\\\\module\\\\RealtimeClient.js -> @supabase/node-fetch\":{\"id\":\"..\\\\node_modules\\\\@supabase\\\\realtime-js\\\\dist\\\\module\\\\RealtimeClient.js -> @supabase/node-fetch\",\"files\":[]},\"..\\\\node_modules\\\\@supabase\\\\storage-js\\\\dist\\\\module\\\\lib\\\\helpers.js -> @supabase/node-fetch\":{\"id\":\"..\\\\node_modules\\\\@supabase\\\\storage-js\\\\dist\\\\module\\\\lib\\\\helpers.js -> @supabase/node-fetch\",\"files\":[]}}"
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@ -2,8 +2,8 @@
"/_app": "pages/_app.js",
"/_error": "pages/_error.js",
"/_document": "pages/_document.js",
"/admin": "pages/admin.js",
"/devlog": "pages/devlog.js",
"/blog/[slug]": "pages/blog/[slug].js",
"/admin/login": "pages/admin/login.js",
"/blog/[slug]": "pages/blog/[slug].js"
"/devlog": "pages/devlog.js",
"/": "pages/index.js"
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static\u002Fchunks\u002Fpages\u002F_error.js"],"/admin":["static\u002Fchunks\u002Fpages\u002Fadmin.js"],"/admin/login":["static\u002Fchunks\u002Fpages\u002Fadmin\u002Flogin.js"],"/blog/[slug]":["static\u002Fchunks\u002Fpages\u002Fblog\u002F[slug].js"],sortedPages:["\u002F_app","\u002F_error","\u002Fadmin","\u002Fadmin\u002Flogin","\u002Fblog\u002F[slug]"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":["static\u002Fchunks\u002Fpages\u002Findex.js"],"/_error":["static\u002Fchunks\u002Fpages\u002F_error.js"],"/blog/[slug]":["static\u002Fchunks\u002Fpages\u002Fblog\u002F[slug].js"],"/devlog":["static\u002Fchunks\u002Fpages\u002Fdevlog.js"],sortedPages:["\u002F","\u002F_app","\u002F_error","\u002Fblog\u002F[slug]","\u002Fdevlog"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

File diff suppressed because one or more lines are too long

1502
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,9 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-leaflet": "^4.2.1"
"react-leaflet": "^4.2.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@types/node": "22.15.30",

View File

@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import { supabase } from '../../lib/supabase';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export default function AdminPanel() {
const [entries, setEntries] = useState([]);
@ -103,6 +105,8 @@ export default function AdminPanel() {
const EntryForm = ({ entry, onSave }) => {
const [formData, setFormData] = useState(entry);
const [contentList, setContentList] = useState(entry.content || []);
const [showPreview, setShowPreview] = useState(false);
const [activeContentIndex, setActiveContentIndex] = useState(null);
const handleContentAdd = (type) => {
setContentList([...contentList, { type, value: '' }]);
@ -123,6 +127,94 @@ export default function AdminPanel() {
});
};
const insertMarkdown = (index, markdownSyntax) => {
const textarea = document.getElementById(`content-${index}`);
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const beforeText = textarea.value.substring(0, start);
const afterText = textarea.value.substring(end);
let newText;
if (markdownSyntax.includes('{}')) {
newText = beforeText + markdownSyntax.replace('{}', selectedText || 'Text hier') + afterText;
} else {
newText = beforeText + markdownSyntax + afterText;
}
handleContentChange(index, newText);
// Focus back to textarea
setTimeout(() => {
textarea.focus();
const newCursorPos = start + markdownSyntax.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
}
};
const MarkdownToolbar = ({ contentIndex }) => (
<div className="flex flex-wrap gap-2 mb-2 p-2 bg-gray-100 rounded">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '**{}**')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Fett"
>
<strong>B</strong>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '*{}*')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Kursiv"
>
<em>I</em>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '# {}')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Überschrift 1"
>
H1
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '## {}')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Überschrift 2"
>
H2
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '### {}')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Überschrift 3"
>
H3
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '- {}')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Liste"
>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '[{}](URL)')}
className="px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50"
title="Link"
>
🔗
</button>
</div>
);
return (
<div className="space-y-4 bg-white p-6 rounded-lg shadow">
<div className="grid grid-cols-1 gap-4">
@ -156,21 +248,81 @@ export default function AdminPanel() {
/>
<div className="space-y-4">
<h3 className="font-bold">Inhalt</h3>
<div className="flex justify-between items-center">
<h3 className="font-bold">Inhalt</h3>
<div className="text-sm text-gray-600 mb-4 p-3 bg-gray-50 rounded border">
<strong className="block mb-2">📝 Markdown-Formatierung:</strong>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
<div><code>**fett**</code> <strong>fett</strong></div>
<div><code>*kursiv*</code> <em>kursiv</em></div>
<div><code># Überschrift</code> <span className="font-bold text-lg">Überschrift</span></div>
<div><code>## Unterüberschrift</code> <span className="font-bold">Unterüberschrift</span></div>
<div><code>[Link](URL)</code> <span className="text-pb-turquoise underline">Link</span></div>
<div><code>- Listenpunkt</code> Listenpunkt</div>
<div><code>1. Nummeriert</code> 1. Nummeriert</div>
<div><code>`Code`</code> <code className="bg-gray-200 px-1 rounded">Code</code></div>
<div><code>&gt; Zitat</code> <span className="italic border-l-2 border-pb-turquoise pl-2">Zitat</span></div>
<div><code>---</code> Trennlinie</div>
</div>
<div className="mt-2 pt-2 border-t border-gray-200">
<strong className="text-xs">💡 Tipp:</strong> <span className="text-xs">Nutze die Buttons oben oder schreibe Markdown direkt. Die Vorschau zeigt das Ergebnis.</span>
</div>
</div>
</div>
{contentList.map((content, index) => (
<div key={index} className="flex space-x-2">
<textarea
value={content.value}
onChange={(e) => handleContentChange(index, e.target.value)}
className="w-full p-2 border rounded"
rows="3"
/>
<button
onClick={() => handleContentRemove(index)}
className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600"
>
×
</button>
<div key={index} className="border rounded-lg p-4 space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">
{content.type === 'text' ? '📝 Text-Inhalt' : '🖼️ Bild-Inhalt'}
</span>
<div className="flex space-x-2">
{content.type === 'text' && (
<button
type="button"
onClick={() => setActiveContentIndex(activeContentIndex === index ? null : index)}
className={`px-3 py-1 text-xs rounded ${
activeContentIndex === index
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{activeContentIndex === index ? 'Vorschau ausblenden' : 'Vorschau anzeigen'}
</button>
)}
<button
onClick={() => handleContentRemove(index)}
className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-xs"
>
Löschen
</button>
</div>
</div>
{content.type === 'text' && <MarkdownToolbar contentIndex={index} />}
<div className={activeContentIndex === index ? 'grid grid-cols-2 gap-4' : ''}>
<div>
<textarea
id={`content-${index}`}
value={content.value}
onChange={(e) => handleContentChange(index, e.target.value)}
className="w-full p-3 border rounded-lg font-mono text-sm"
rows={content.type === 'text' ? "8" : "3"}
placeholder={content.type === 'text' ? 'Hier können Sie **Markdown** verwenden...' : 'Bild-URL eingeben...'}
/>
</div>
{content.type === 'text' && activeContentIndex === index && (
<div className="border rounded-lg p-3 bg-gray-50">
<div className="text-xs font-medium text-gray-600 mb-2">Vorschau:</div>
<div className="prose prose-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{content.value || '*Keine Inhalte zum Anzeigen*'}
</ReactMarkdown>
</div>
</div>
)}
</div>
</div>
))}
<div className="flex space-x-2">

View File

@ -2,6 +2,8 @@ import { motion } from 'framer-motion';
import devlogData from '../../data/devlog.json';
import Image from 'next/image';
import { supabase } from '../../lib/supabase';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export default function BlogPost({ blog }) {
@ -41,20 +43,60 @@ export default function BlogPost({ blog }) {
// Handle both old format (type/value) and new format (direct content)
if (typeof item === 'string') {
return (
<p key={index} className="text-gray-600 text-lg">
{item}
</p>
<div key={index} className="prose prose-lg max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="text-gray-700 text-base sm:text-lg leading-relaxed px-2 sm:px-0"
components={{
h1: ({children}) => <h1 className="text-3xl font-bold mb-4 text-gray-900">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-bold mb-3 text-gray-900">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
p: ({children}) => <p className="mb-4 sm:mb-6">{children}</p>,
strong: ({children}) => <strong className="font-bold text-gray-900">{children}</strong>,
em: ({children}) => <em className="italic">{children}</em>,
ul: ({children}) => <ul className="list-disc list-inside mb-4 space-y-2">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
li: ({children}) => <li className="ml-4">{children}</li>,
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
code: ({children}) => <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{children}</code>,
pre: ({children}) => <pre className="bg-gray-100 p-4 rounded overflow-x-auto mb-4">{children}</pre>
}}
>
{item}
</ReactMarkdown>
</div>
);
}
return item.type === 'text' ? (
<p key={index} className="text-gray-600 text-lg">
{item.value}
</p>
<div key={index} className="prose prose-lg max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="text-gray-700 text-base sm:text-lg leading-relaxed px-2 sm:px-0"
components={{
h1: ({children}) => <h1 className="text-3xl font-bold mb-4 text-gray-900">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-bold mb-3 text-gray-900">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
p: ({children}) => <p className="mb-4 sm:mb-6">{children}</p>,
strong: ({children}) => <strong className="font-bold text-gray-900">{children}</strong>,
em: ({children}) => <em className="italic">{children}</em>,
ul: ({children}) => <ul className="list-disc list-inside mb-4 space-y-2">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
li: ({children}) => <li className="ml-4">{children}</li>,
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
code: ({children}) => <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{children}</code>,
pre: ({children}) => <pre className="bg-gray-100 p-4 rounded overflow-x-auto mb-4">{children}</pre>
}}
>
{item.value}
</ReactMarkdown>
</div>
) : (
<div
key={index}
className="relative w-full h-64 rounded-lg overflow-hidden"
className="relative w-full h-64 rounded-lg overflow-hidden shadow-lg mb-8"
>
{/* Only render image if src is valid */}
{item.value && (item.value.startsWith('/') || item.value.startsWith('http')) && (
@ -72,9 +114,29 @@ export default function BlogPost({ blog }) {
{/* If no structured content, show description */}
{(!blog.content || blog.content.length === 0) && blog.description && (
<p className="text-gray-600 text-lg">
{blog.description}
</p>
<div className="prose prose-lg max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="text-gray-700 text-base sm:text-lg leading-relaxed px-2 sm:px-0"
components={{
h1: ({children}) => <h1 className="text-3xl font-bold mb-4 text-gray-900">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-bold mb-3 text-gray-900">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
p: ({children}) => <p className="mb-4 sm:mb-6">{children}</p>,
strong: ({children}) => <strong className="font-bold text-gray-900">{children}</strong>,
em: ({children}) => <em className="italic">{children}</em>,
ul: ({children}) => <ul className="list-disc list-inside mb-4 space-y-2">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
li: ({children}) => <li className="ml-4">{children}</li>,
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
code: ({children}) => <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{children}</code>,
pre: ({children}) => <pre className="bg-gray-100 p-4 rounded overflow-x-auto mb-4">{children}</pre>
}}
>
{blog.description}
</ReactMarkdown>
</div>
)}
</div>
</div>