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>
This commit is contained in:
2026-01-17 01:00:33 +01:00
co-authored by Claude Opus 4.5
parent a66f51b9a2
commit b1ec7b4d61
281 changed files with 8024 additions and 8901 deletions
+104
View File
@@ -0,0 +1,104 @@
import React, { useState, useRef } from 'react';
import { Share2, X, Twitter, Facebook, Linkedin, Link as LinkIcon } from 'lucide-react';
import { useOnClickOutside } from '../hooks/useOnClickOutside';
interface ShareMenuProps {
title: string;
url: string;
description: string;
}
const ShareMenu: React.FC<ShareMenuProps> = ({ title, url, description }) => {
const [isOpen, setIsOpen] = useState(false);
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied'>('idle');
const menuRef = useRef<HTMLDivElement>(null);
useOnClickOutside(menuRef, () => setIsOpen(false));
const shareLinks = [
{
name: 'Twitter',
icon: Twitter,
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
label: 'Share on Twitter'
},
{
name: 'Facebook',
icon: Facebook,
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
label: 'Share on Facebook'
},
{
name: 'LinkedIn',
icon: Linkedin,
href: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent(description)}`,
label: 'Share on LinkedIn'
},
];
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(url);
setCopyStatus('copied');
setTimeout(() => setCopyStatus('idle'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-gray-300 hover:text-white focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-black transition-colors p-2 rounded-full hover:bg-white/10"
aria-expanded={isOpen}
aria-haspopup="menu"
aria-label="Share this content"
>
{isOpen ? <X className="h-5 w-5" aria-hidden="true" /> : <Share2 className="h-5 w-5" aria-hidden="true" />}
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-48 rounded-lg bg-gray-900 shadow-lg ring-1 ring-black ring-opacity-5 z-50"
role="menu"
aria-orientation="vertical"
onKeyDown={handleKeyDown}
>
<div className="py-1">
{shareLinks.map((link) => (
<a
key={link.name}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
aria-label={link.label}
>
<link.icon className="h-4 w-4 mr-3" aria-hidden="true" />
{link.name}
</a>
))}
<button
onClick={copyToClipboard}
className="flex items-center w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
>
<LinkIcon className="h-4 w-4 mr-3" aria-hidden="true" />
{copyStatus === 'copied' ? 'Copied!' : 'Copy Link'}
</button>
</div>
</div>
)}
</div>
);
};
export default ShareMenu