neue Seite Blog hinzugefügt

This commit is contained in:
michi 2025-09-10 23:32:12 +02:00
parent a587208eaf
commit 4721b59b10
21 changed files with 818 additions and 69 deletions

View File

@ -26,16 +26,6 @@
"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": []

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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":{"/":["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":[]}
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"]},"ampFirstPages":[]}

View File

@ -1,26 +1,6 @@
{
"sortedMiddleware": [
"/"
],
"middleware": {
"/": {
"files": [
"prerender-manifest.js",
"server/edge-runtime-webpack.js",
"server/middleware.js"
],
"name": "middleware",
"page": "/",
"matchers": [
{
"regexp": "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/admin(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?(.json)?[\\/#\\?]?$",
"originalSource": "/admin/:path*"
}
],
"wasm": [],
"assets": []
}
},
"sortedMiddleware": [],
"middleware": {},
"functions": {},
"version": 2
}

View File

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

View File

@ -1 +1 @@
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()
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":["static\u002Fchunks\u002Fpages\u002Findex.js"],"/_error":["static\u002Fchunks\u002Fpages\u002F_error.js"],sortedPages:["\u002F","\u002F_app","\u002F_error"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

File diff suppressed because one or more lines are too long

View File

@ -37,9 +37,33 @@ const AdminLayout = ({ children }) => {
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<h1 className="text-xl font-semibold text-gray-900">
Admin Panel
</h1>
<div className="flex items-center space-x-8">
<h1 className="text-xl font-semibold text-gray-900">
Admin Panel
</h1>
<nav className="flex space-x-4">
<button
onClick={() => router.push('/admin')}
className={`px-3 py-2 rounded-md text-sm font-medium ${
router.pathname === '/admin'
? 'bg-gray-100 text-gray-900'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
}`}
>
DevLog
</button>
<button
onClick={() => router.push('/admin/blog')}
className={`px-3 py-2 rounded-md text-sm font-medium ${
router.pathname === '/admin/blog'
? 'bg-gray-100 text-gray-900'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
}`}
>
Blog
</button>
</nav>
</div>
<button
onClick={handleLogout}
disabled={isLoggingOut}

View File

@ -11,6 +11,7 @@ export default function Navbar() {
{ name: 'Über uns', href: '/about' },
{ name: 'Projekte', href: '/projects' },
{ name: 'DevLog', href: '/devlog' },
{ name: 'Blog', href: '/blog' },
{ name: 'Kontakt', href: '/contact' },
];

524
pages/admin/blog.js Normal file
View File

@ -0,0 +1,524 @@
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">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="whitespace-pre-wrap"
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>
);
}

35
pages/api/delete-blog.js Normal file
View File

@ -0,0 +1,35 @@
import { supabase } from '../../lib/supabase';
export default async function handler(req, res) {
if (req.method !== 'DELETE') {
return res.status(405).json({ error: 'Method not allowed' });
}
const isAuthenticated = req.cookies.isAuthenticated === 'true';
if (!isAuthenticated) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { id } = req.body;
if (!id) {
return res.status(400).json({ error: 'ID ist erforderlich' });
}
try {
const { error } = await supabase
.from('blog_entries')
.delete()
.eq('id', id);
if (error) {
console.error('Supabase Lösch-Fehler:', error);
return res.status(500).json({ error: 'Fehler beim Löschen des Beitrags' });
}
return res.status(200).json({ message: 'Beitrag erfolgreich gelöscht' });
} catch (error) {
console.error('Server-Fehler:', error);
return res.status(500).json({ error: 'Interner Server-Fehler' });
}
}

55
pages/api/update-blog.js Normal file
View File

@ -0,0 +1,55 @@
import { supabaseAdmin } from '../../lib/supabase';
export default async function handler(req, res) {
if (req.method !== 'POST') {
console.log('Method not allowed:', req.method);
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const data = req.body;
if (!Array.isArray(data)) {
console.log('Invalid data format - not an array');
return res.status(400).json({ error: 'Data must be an array' });
}
const results = [];
for (const entry of data) {
const content = Array.isArray(entry.content) ? entry.content : [];
const cleanEntry = {
title: entry.title || '',
date: entry.date || '',
description: entry.description || '',
slug: entry.slug || '',
image: entry.image || '',
content: content,
updated_at: new Date().toISOString()
};
const { data: result, error } = await supabaseAdmin
.from('blog_entries')
.upsert(cleanEntry, {
onConflict: 'slug',
returning: 'minimal'
});
if (error) {
console.error('Supabase upsert error:', error);
throw error;
}
results.push(result);
}
res.status(200).json({
message: 'Blog entries updated successfully',
results
});
} catch (error) {
console.error('Error updating blog entries:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

92
pages/blog.js Normal file
View File

@ -0,0 +1,92 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { supabase } from '../lib/supabase';
export default function Blog({ blogData }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-16"
>
<h1 className="text-4xl font-bold mb-6">Blog</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Gedanken, Tutorials und Artikel über verschiedene Themen.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{blogData && blogData.map((post, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
className="relative p-8 bg-white before:content-[''] before:absolute before:top-0 before:left-0 before:w-8 before:h-8 before:border-t-2 before:border-l-2 before:border-pb-turquoise after:content-[''] after:absolute after:bottom-0 after:right-0 after:w-8 after:h-8 after:border-b-2 after:border-r-2 after:border-pb-turquoise"
>
{post.image && (
<div className="relative w-full h-48 mb-4 overflow-hidden">
<Image
src={post.image}
alt={post.title}
fill
priority={index < 2}
className="object-cover"
/>
</div>
)}
<Link href={`/blog/${post.slug}`} legacyBehavior>
<a className="block group">
<h3 className="text-xl font-bold mb-2 group-hover:text-pb-turquoise transition-colors">{post.title}</h3>
<p className="text-gray-600 mb-4">{post.date}</p>
<p className="text-gray-600 mb-4">{post.description}</p>
<span className="text-pb-turquoise group-hover:underline">Weiterlesen</span>
</a>
</Link>
</motion.div>
))}
</div>
</div>
</section>
</motion.div>
);
}
export async function getServerSideProps() {
try {
const { data: blogData, error } = await supabase
.from('blog_entries')
.select('*')
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching blog data:', error);
return {
props: {
blogData: []
}
};
}
return {
props: {
blogData: blogData || []
}
};
} catch (error) {
console.error('Error in getServerSideProps:', error);
return {
props: {
blogData: []
}
};
}
}

View File

@ -51,7 +51,7 @@ export default function BlogPost({ blog }) {
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 whitespace-pre-wrap">{children}</p>,
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>,
@ -62,7 +62,7 @@ export default function BlogPost({ blog }) {
code: ({node, inline, className, children, ...props}) => {
const match = /language-(\w+)/.exec(className || '');
return !inline ? (
<pre className="relative bg-[#1e1e1e] p-4 rounded-lg overflow-x-auto mb-4 text-white">
<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>
@ -94,7 +94,7 @@ export default function BlogPost({ blog }) {
<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 whitespace-pre-wrap"
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>,

View File

@ -0,0 +1,58 @@
create table public.blog_entries (
id bigint primary key generated always as identity,
author_id uuid not null references auth.users (id),
date text not null,
title text not null,
description text,
slug text unique not null,
image text,
content jsonb default '[]'::jsonb,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);
create index idx_blog_entries_author_id on public.blog_entries (author_id);
alter table public.blog_entries ENABLE row LEVEL SECURITY;
create policy "Public select" on public.blog_entries for
select
to anon using (true);
create policy "Authenticated insert" on public.blog_entries for INSERT to authenticated
with
check (
(
select
auth.uid ()
)::uuid = author_id
);
create policy "Author update" on public.blog_entries
for update
to authenticated using (
(
select
auth.uid ()
)::uuid = author_id
)
with
check (
(
select
auth.uid ()
)::uuid = author_id
);
create policy "Author delete" on public.blog_entries for DELETE to authenticated using (
(
select
auth.uid ()
)::uuid = author_id
);
grant select on public.blog_entries to anon;
grant INSERT, update, DELETE on public.blog_entries to authenticated;
grant USAGE, select on SEQUENCE public.blog_entries_id_seq to authenticated;