524 lines
18 KiB
JavaScript
524 lines
18 KiB
JavaScript
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, '')}
|
||
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></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>
|
||
);
|
||
}
|