2025-09-11 00:00:17 +02:00

524 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { supabase } from '../../lib/supabase';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import AdminLayout from '../../components/AdminLayout';
// Verwenden Sie die gleiche Komponente wie für DevLog, aber mit anderen Tabellennamen
export default function BlogAdmin() {
const [entries, setEntries] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedEntry, setSelectedEntry] = useState(null);
const emptyEntry = {
date: new Date().toLocaleDateString('de-DE'),
title: '',
description: '',
slug: '',
image: '',
content: []
};
// Load data from Supabase on component mount
useEffect(() => {
loadEntries();
}, []);
const loadEntries = async () => {
try {
const { data, error } = await supabase
.from('blog_entries')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
setEntries(data || []);
} catch (error) {
console.error('Fehler beim Laden der Einträge:', error);
alert('Fehler beim Laden der Daten');
} finally {
setLoading(false);
}
};
const handleSave = async (entry, index = null) => {
const cleanEntry = { ...entry };
delete cleanEntry.index;
delete cleanEntry.editIndex;
delete cleanEntry.id;
let updatedEntries;
if (index !== null) {
const originalEntry = entries[index];
cleanEntry.id = originalEntry.id;
updatedEntries = entries.map((e, i) => i === index ? cleanEntry : e);
} else {
updatedEntries = [...entries, cleanEntry];
}
try {
const response = await fetch('/api/update-blog', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedEntries),
});
if (!response.ok) throw new Error('Fehler beim Speichern');
await loadEntries();
setSelectedEntry(null);
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Speichern der Änderungen');
}
};
const handleDelete = async (entryId, entryTitle) => {
if (!confirm(`Sind Sie sicher, dass Sie den Beitrag "${entryTitle}" löschen möchten?`)) {
return;
}
try {
const response = await fetch('/api/delete-blog', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: entryId }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Fehler beim Löschen');
}
await loadEntries();
alert('Beitrag erfolgreich gelöscht!');
} catch (error) {
console.error('Fehler beim Löschen:', error);
alert(`Fehler beim Löschen des Beitrags: ${error.message}`);
}
};
// Hier kommt der Rest des Codes vom DevLog Admin Panel
// (EntryForm Komponente und der Rest der Render-Logik)
// [Code ausgelassen der Übersichtlichkeit halber]
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: '' }]);
};
const handleContentChange = (index, value) => {
const newContent = [...contentList];
newContent[index].value = value;
setContentList(newContent);
setFormData({ ...formData, content: newContent });
};
const handleContentRemove = (index) => {
const newContent = contentList.filter((_, i) => i !== index);
setContentList(newContent);
setFormData({ ...formData, content: newContent });
};
const insertMarkdown = (index, markdownSyntax) => {
const textarea = document.getElementById(`content-${index}`);
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const before = text.substring(0, start);
const selection = text.substring(start, end);
const after = text.substring(end);
const newText = before + markdownSyntax.replace('{}', selection || '') + after;
handleContentChange(index, newText);
// Set cursor position after insertion
setTimeout(() => {
textarea.focus();
const newCursorPos = start + markdownSyntax.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
};
const MarkdownToolbar = ({ contentIndex }) => {
const buttonClass = "px-2 py-1 text-xs bg-white border rounded hover:bg-gray-50 flex items-center justify-center min-w-[32px]";
return (
<div className="flex flex-wrap gap-2 mb-2 p-2 bg-gray-50 rounded-lg">
{/* Text Formatierung */}
<div className="flex gap-2 items-center border-r pr-2">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '**{}**')}
className={buttonClass}
title="Fett"
>
<strong>B</strong>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '*{}*')}
className={buttonClass}
title="Kursiv"
>
<em>I</em>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '~~{}~~')}
className={buttonClass}
title="Durchgestrichen"
>
<span className="line-through">S</span>
</button>
</div>
{/* Überschriften */}
<div className="flex gap-2 items-center border-r pr-2">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '# {}')}
className={buttonClass}
title="Überschrift 1"
>
H1
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '## {}')}
className={buttonClass}
title="Überschrift 2"
>
H2
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '### {}')}
className={buttonClass}
title="Überschrift 3"
>
H3
</button>
</div>
{/* Listen */}
<div className="flex gap-2 items-center border-r pr-2">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '- {}')}
className={buttonClass}
title="Aufzählungsliste"
>
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '1. {}')}
className={buttonClass}
title="Nummerierte Liste"
>
1.
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '- [ ] {}')}
className={buttonClass}
title="Aufgabenliste"
>
</button>
</div>
{/* Code */}
<div className="flex gap-2 items-center border-r pr-2">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '`{}`')}
className={buttonClass}
title="Inline Code"
>
{'<>'}
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '```\n{}\n```')}
className={buttonClass}
title="Code Block"
>
{'</>'}
</button>
</div>
{/* Sonstiges */}
<div className="flex gap-2 items-center">
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '[{}](URL)')}
className={buttonClass}
title="Link"
>
🔗
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '![{}](URL)')}
className={buttonClass}
title="Bild"
>
🖼
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '> {}')}
className={buttonClass}
title="Zitat"
>
"
</button>
<button
type="button"
onClick={() => insertMarkdown(contentIndex, '---\n')}
className={buttonClass}
title="Horizontale Linie"
>
</button>
</div>
</div>
);
};
return (
<div className="space-y-4 bg-white p-6 rounded-lg shadow">
<div className="grid grid-cols-1 gap-4">
<input
type="text"
placeholder="Titel"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="Beschreibung"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="URL-Slug"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="Bild-URL"
value={formData.image}
onChange={(e) => setFormData({ ...formData, image: e.target.value })}
className="w-full p-2 border rounded"
/>
<div className="space-y-4">
<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 1</code></div>
<div><code>## Überschrift 2</code></div>
<div><code>[Link](URL)</code></div>
<div><code>![Bild](URL)</code></div>
</div>
</div>
</div>
</div>
<div className="space-y-4">
{contentList.map((content, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between items-center">
<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 whitespace-pre-wrap"
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 whitespace-pre-wrap">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({children}) => <p className="mb-4 whitespace-pre-wrap">{children}</p>,
code: ({node, inline, className, children, ...props}) => {
const match = /language-(\w+)/.exec(className || '');
return !inline ? (
<pre className="relative bg-[#1a1a1a] p-4 rounded-lg overflow-x-auto mb-4 text-gray-200">
<div className="absolute top-0 right-0 px-4 py-1 rounded-bl bg-gray-700 text-xs text-gray-300">
{match ? match[1] : 'code'}
</div>
<code
className={`${className} block font-mono text-sm`}
{...props}
>
{children}
</code>
</pre>
) : (
<code
className="bg-[#1a1a1a] text-gray-200 px-2 py-1 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
}
}}
>
{content.value || '*Keine Inhalte zum Anzeigen*'}
</ReactMarkdown>
</div>
</div>
)}
</div>
</div>
))}
<div className="flex space-x-2">
<button
onClick={() => handleContentAdd('text')}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
+ Text
</button>
<button
onClick={() => handleContentAdd('image')}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
+ Bild
</button>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 mt-4">
<button
onClick={() => setSelectedEntry(null)}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Abbrechen
</button>
<button
onClick={() => onSave(formData)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Speichern
</button>
</div>
</div>
);
};
if (loading) {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Blog Admin</h1>
<div className="flex justify-center items-center h-64">
<div className="text-xl">Lade Daten...</div>
</div>
</div>
);
}
return (
<AdminLayout>
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Blog Admin</h1>
{selectedEntry === null ? (
<div>
<button
onClick={() => setSelectedEntry(emptyEntry)}
className="mb-8 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
Neuen Beitrag erstellen
</button>
<div className="space-y-4">
{entries.map((entry, index) => (
<div key={index} className="bg-white p-4 rounded-lg shadow">
<h2 className="text-xl font-bold">{entry.title}</h2>
<p className="text-gray-600">{entry.date}</p>
<p className="mb-4">{entry.description}</p>
<div className="flex space-x-2">
<button
onClick={() => setSelectedEntry({ ...entry, editIndex: index })}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(entry.id, entry.title)}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Löschen
</button>
</div>
</div>
))}
</div>
</div>
) : (
<EntryForm
entry={selectedEntry}
onSave={(formData) => handleSave(formData, selectedEntry.editIndex)}
/>
)}
</div>
</AdminLayout>
);
}