229 lines
10 KiB
JavaScript
229 lines
10 KiB
JavaScript
import { motion } from 'framer-motion';
|
|
import devlogData from '../../data/devlog.json';
|
|
import Image from 'next/image';
|
|
import { supabase } from '../../lib/supabase';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
|
|
export default function BlogPost({ blog }) {
|
|
|
|
if (!blog) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="py-20 text-center"
|
|
>
|
|
<h1 className="text-4xl font-bold mb-6">Beitrag nicht gefunden</h1>
|
|
<p className="text-xl text-gray-600">Der angeforderte Beitrag existiert nicht.</p>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
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="relative p-8 mb-16 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"
|
|
>
|
|
<h1 className="text-4xl font-bold mb-6">{blog.title}</h1>
|
|
<p className="text-xl text-gray-600 mb-4">{blog.date}</p>
|
|
</motion.div>
|
|
|
|
<div className="space-y-8">
|
|
{blog.content && blog.content.map((item, index) => {
|
|
// Handle both old format (type/value) and new format (direct content)
|
|
if (typeof item === 'string') {
|
|
return (
|
|
<div key={index} className="prose prose-lg max-w-none">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
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>,
|
|
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
|
|
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>,
|
|
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
|
|
li: ({children}) => <li className="ml-4">{children}</li>,
|
|
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
|
|
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
|
|
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-[#1e1e1e] text-white px-2 py-1 rounded text-sm font-mono"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</code>
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
{item}
|
|
</ReactMarkdown>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return item.type === 'text' ? (
|
|
<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"
|
|
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>,
|
|
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
|
|
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>,
|
|
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
|
|
li: ({children}) => <li className="ml-4">{children}</li>,
|
|
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
|
|
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
|
|
code: ({children}) => <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{children}</code>,
|
|
pre: ({children}) => <pre className="bg-gray-100 p-4 rounded overflow-x-auto mb-4">{children}</pre>
|
|
}}
|
|
>
|
|
{item.value}
|
|
</ReactMarkdown>
|
|
</div>
|
|
) : (
|
|
<div
|
|
key={index}
|
|
className="relative w-full h-64 rounded-lg overflow-hidden shadow-lg mb-8"
|
|
>
|
|
{/* Only render image if src is valid */}
|
|
{item.value && (item.value.startsWith('/') || item.value.startsWith('http')) && (
|
|
<Image
|
|
src={item.value}
|
|
alt={blog.title}
|
|
fill
|
|
className="object-cover"
|
|
priority
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* If no structured content, show description */}
|
|
{(!blog.content || blog.content.length === 0) && blog.description && (
|
|
<div 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"
|
|
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>,
|
|
h3: ({children}) => <h3 className="text-xl font-bold mb-2 text-gray-900">{children}</h3>,
|
|
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>,
|
|
ol: ({children}) => <ol className="list-decimal list-inside mb-4 space-y-2">{children}</ol>,
|
|
li: ({children}) => <li className="ml-4">{children}</li>,
|
|
a: ({href, children}) => <a href={href} className="text-pb-turquoise hover:underline">{children}</a>,
|
|
blockquote: ({children}) => <blockquote className="border-l-4 border-pb-turquoise pl-4 italic my-4">{children}</blockquote>,
|
|
code: ({children}) => <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{children}</code>,
|
|
pre: ({children}) => <pre className="bg-gray-100 p-4 rounded overflow-x-auto mb-4">{children}</pre>
|
|
}}
|
|
>
|
|
{blog.description}
|
|
</ReactMarkdown>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export async function getServerSideProps({ params }) {
|
|
const { slug } = params;
|
|
|
|
try {
|
|
// First, try to find the blog post in Supabase
|
|
const { data: supabaseBlog, error } = await supabase
|
|
.from('devlog_entries')
|
|
.select('*')
|
|
.eq('slug', slug)
|
|
.single();
|
|
|
|
if (supabaseBlog && !error) {
|
|
// Found in Supabase, format the data
|
|
const blog = {
|
|
id: supabaseBlog.id,
|
|
title: supabaseBlog.title,
|
|
date: supabaseBlog.date,
|
|
description: supabaseBlog.description,
|
|
slug: supabaseBlog.slug,
|
|
image: supabaseBlog.image,
|
|
content: supabaseBlog.content || []
|
|
};
|
|
|
|
return {
|
|
props: {
|
|
blog
|
|
}
|
|
};
|
|
}
|
|
|
|
// If not found in Supabase, try the static JSON file
|
|
const staticBlog = devlogData.find((entry) => entry.slug === slug);
|
|
|
|
if (staticBlog) {
|
|
return {
|
|
props: {
|
|
blog: staticBlog
|
|
}
|
|
};
|
|
}
|
|
|
|
// Blog post not found
|
|
return {
|
|
props: {
|
|
blog: null
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching blog post:', error);
|
|
|
|
// Fallback to static data
|
|
const staticBlog = devlogData.find((entry) => entry.slug === slug);
|
|
|
|
return {
|
|
props: {
|
|
blog: staticBlog || null
|
|
}
|
|
};
|
|
}
|
|
}
|