2025-09-10 21:32:50 +02:00

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>&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="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>
);
}