420 lines
15 KiB
JavaScript
420 lines
15 KiB
JavaScript
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([]);
|
|
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('devlog_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) => {
|
|
// Entferne das 'index' und 'editIndex' Feld aus dem Entry
|
|
const cleanEntry = { ...entry };
|
|
delete cleanEntry.index;
|
|
delete cleanEntry.editIndex;
|
|
delete cleanEntry.id; // Remove id for new entries
|
|
|
|
let updatedEntries;
|
|
|
|
if (index !== null) {
|
|
// Bearbeite bestehenden Eintrag - keep the original id
|
|
const originalEntry = entries[index];
|
|
cleanEntry.id = originalEntry.id;
|
|
updatedEntries = entries.map((e, i) => i === index ? cleanEntry : e);
|
|
} else {
|
|
// Füge neuen Eintrag hinzu
|
|
updatedEntries = [...entries, cleanEntry];
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/update-devlog', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updatedEntries),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Fehler beim Speichern');
|
|
|
|
// Reload data from Supabase to get updated entries with IDs
|
|
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-devlog', {
|
|
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');
|
|
}
|
|
|
|
// Reload data from Supabase to get updated entries
|
|
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}`);
|
|
}
|
|
};
|
|
|
|
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) => {
|
|
setContentList(contentList.filter((_, i) => i !== index));
|
|
setFormData({
|
|
...formData,
|
|
content: contentList.filter((_, i) => i !== index)
|
|
});
|
|
};
|
|
|
|
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">
|
|
<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</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>> 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="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">
|
|
<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">DevLog Admin</h1>
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="text-xl">Lade Daten...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto p-8">
|
|
<h1 className="text-3xl font-bold mb-8">DevLog 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>
|
|
);
|
|
}
|