Files
Portfolio/src/pages-vite/blog/index.tsx
T
damjan_savicandClaude Opus 4.5 b1ec7b4d61 Migrate from Vite to Next.js 15 with SSR
- Replace Vite + React Router with Next.js 15 App Router
- Implement i18n with next-intl (URL-based: /de, /en, /sr)
- Add SSR/SSG for all pages (48 static pages generated)
- Setup Supabase SSR client for auth
- Migrate all pages: Home, About, Portfolio, Blog, Contact, Login, Dashboard, Imprint, Privacy, Terms
- Add Docker support with standalone output
- Replace i18next with next-intl JSON translations
- Use next/image for optimized images

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 01:00:33 +01:00

186 lines
7.5 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { Loader2, ArrowRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import PageTransition from '../../components/PageTransition';
import SEO from '../../components/SEO';
import BlogPostCard from './components/BlogpostCard';
import type { BlogPost } from './utils/mdxLoader';
// Import aller Sprachversionen
import { blog as blogDe } from '../../i18n/locales/de/blog/index';
import { blog as blogEn } from '../../i18n/locales/en/blog/index';
import { blog as blogSr } from '../../i18n/locales/sr/blog/index';
const BlogPage: React.FC = () => {
const { i18n } = useTranslation();
// Wir erwarten hier Posts, bei denen coverImage garantiert ein string ist und slug vorhanden ist.
const [posts, setPosts] = useState<(BlogPost & { coverImage: string; slug: string })[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Ermittelt die aktuelle Blog-Konfiguration anhand der Sprache.
const getBlogConfig = () => {
switch (i18n.language) {
case 'en':
return blogEn;
case 'sr':
return blogSr;
case 'de':
default:
return blogDe;
}
};
const blog = getBlogConfig();
useEffect(() => {
const loadPosts = async () => {
setLoading(true);
try {
// Aus der aktuellen Sprachkonfiguration werden alle Posts extrahiert.
const currentBlog = getBlogConfig();
const blogPosts = Object.entries(currentBlog.posts).map(([slug, post]) => ({
...post,
slug,
// Falls coverImage nicht definiert ist, setzen wir einen leeren String als Fallback.
coverImage: post.coverImage ?? ''
})) as (BlogPost & { coverImage: string; slug: string })[];
setPosts(blogPosts);
} catch (error) {
console.error('Error loading blog posts:', error);
setError(
error instanceof Error
? error
: new Error(blog.ui.errors.posts)
);
} finally {
setLoading(false);
}
};
loadPosts();
}, [i18n.language]);
const schema = {
'@context': 'https://schema.org',
'@type': 'Blog',
name: blog.meta.title,
description: blog.meta.description
};
const metaDescription = blog.meta.description;
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center space-y-4"
>
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
<p className="text-zinc-400 text-sm">{blog.ui.loading.posts}</p>
</motion.div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col items-center space-y-6 px-4 text-center"
>
<div className="space-y-2">
<h3 className="text-lg font-medium text-white">{blog.ui.errors.posts}</h3>
<p className="text-zinc-400 text-sm max-w-md">{error.message}</p>
</div>
</motion.div>
</div>
);
}
return (
<PageTransition>
{/* SEO-Aufruf: Wir übergeben hier nur die Properties, die im SEOProps-Typ definiert sind */}
<SEO
title={blog.meta.title}
description={metaDescription}
schema={schema}
/>
<main className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
{/* Header Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-12 sm:mb-16 lg:mb-20"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="flex items-center gap-4 mb-8"
>
<ArrowRight className="h-5 w-5 text-zinc-500" />
<h2 className="text-white text-sm tracking-wider">BLOG</h2>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6"
>
{blog.meta.header.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="text-lg sm:text-xl text-zinc-400 max-w-2xl"
>
{blog.meta.header.subtitle}
</motion.p>
</motion.div>
{/* Blog Posts Grid */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8"
>
{posts.map((post, index) => (
<motion.div
key={post.slug}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 * (index + 4) }}
>
<BlogPostCard post={post} />
</motion.div>
))}
</motion.div>
{/* Empty State */}
{posts.length === 0 && !loading && !error && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="text-center py-12"
>
<p className="text-zinc-400">{blog.ui.notFound.message}</p>
</motion.div>
)}
</div>
</main>
</PageTransition>
);
};
export default BlogPage;